From 2b024920847481592b1a13d4006d2a9fa8881d72 Mon Sep 17 00:00:00 2001 From: Michael Merickel Date: Sun, 14 Oct 2018 21:10:02 -0500 Subject: move package to src folder --- src/pyramid/__init__.py | 1 + src/pyramid/asset.py | 44 + src/pyramid/authentication.py | 1201 +++++++ src/pyramid/authorization.py | 146 + src/pyramid/compat.py | 281 ++ src/pyramid/config/__init__.py | 1409 ++++++++ src/pyramid/config/adapters.py | 326 ++ src/pyramid/config/assets.py | 396 +++ src/pyramid/config/factories.py | 245 ++ src/pyramid/config/i18n.py | 120 + src/pyramid/config/predicates.py | 2 + src/pyramid/config/rendering.py | 51 + src/pyramid/config/routes.py | 560 +++ src/pyramid/config/security.py | 265 ++ src/pyramid/config/settings.py | 107 + src/pyramid/config/testing.py | 167 + src/pyramid/config/tweens.py | 196 ++ src/pyramid/config/util.py | 281 ++ src/pyramid/config/views.py | 2327 +++++++++++++ src/pyramid/config/zca.py | 20 + src/pyramid/csrf.py | 336 ++ src/pyramid/decorator.py | 45 + src/pyramid/encode.py | 84 + src/pyramid/events.py | 289 ++ src/pyramid/exceptions.py | 127 + src/pyramid/httpexceptions.py | 1182 +++++++ src/pyramid/i18n.py | 397 +++ src/pyramid/interfaces.py | 1354 ++++++++ src/pyramid/location.py | 66 + src/pyramid/paster.py | 111 + src/pyramid/path.py | 436 +++ src/pyramid/predicates.py | 336 ++ src/pyramid/registry.py | 297 ++ src/pyramid/renderers.py | 529 +++ src/pyramid/request.py | 334 ++ src/pyramid/resource.py | 5 + src/pyramid/response.py | 211 ++ src/pyramid/router.py | 278 ++ src/pyramid/scaffolds/__init__.py | 65 + src/pyramid/scaffolds/alchemy/+dot+coveragerc_tmpl | 3 + .../scaffolds/alchemy/+package+/__init__.py | 12 + .../alchemy/+package+/models/__init__.py_tmpl | 74 + .../scaffolds/alchemy/+package+/models/meta.py | 16 + .../scaffolds/alchemy/+package+/models/mymodel.py | 18 + src/pyramid/scaffolds/alchemy/+package+/routes.py | 3 + .../alchemy/+package+/scripts/__init__.py | 1 + .../alchemy/+package+/scripts/initializedb.py | 45 + .../alchemy/+package+/static/pyramid-16x16.png | Bin 0 -> 1319 bytes .../scaffolds/alchemy/+package+/static/pyramid.png | Bin 0 -> 12901 bytes .../scaffolds/alchemy/+package+/static/theme.css | 154 + .../alchemy/+package+/templates/404.jinja2_tmpl | 8 + .../alchemy/+package+/templates/layout.jinja2_tmpl | 66 + .../+package+/templates/mytemplate.jinja2_tmpl | 8 + .../scaffolds/alchemy/+package+/tests.py_tmpl | 65 + .../scaffolds/alchemy/+package+/views/__init__.py | 0 .../alchemy/+package+/views/default.py_tmpl | 33 + .../alchemy/+package+/views/notfound.py_tmpl | 7 + src/pyramid/scaffolds/alchemy/CHANGES.txt_tmpl | 4 + src/pyramid/scaffolds/alchemy/MANIFEST.in_tmpl | 2 + src/pyramid/scaffolds/alchemy/README.txt_tmpl | 14 + src/pyramid/scaffolds/alchemy/development.ini_tmpl | 69 + src/pyramid/scaffolds/alchemy/production.ini_tmpl | 59 + src/pyramid/scaffolds/alchemy/pytest.ini_tmpl | 3 + src/pyramid/scaffolds/alchemy/setup.py_tmpl | 55 + src/pyramid/scaffolds/copydir.py | 301 ++ src/pyramid/scaffolds/starter/+dot+coveragerc_tmpl | 3 + .../scaffolds/starter/+package+/__init__.py | 12 + .../starter/+package+/static/pyramid-16x16.png | Bin 0 -> 1319 bytes .../scaffolds/starter/+package+/static/pyramid.png | Bin 0 -> 12901 bytes .../scaffolds/starter/+package+/static/theme.css | 152 + .../starter/+package+/templates/layout.jinja2_tmpl | 66 + .../+package+/templates/mytemplate.jinja2_tmpl | 8 + .../scaffolds/starter/+package+/tests.py_tmpl | 29 + .../scaffolds/starter/+package+/views.py_tmpl | 6 + src/pyramid/scaffolds/starter/CHANGES.txt_tmpl | 4 + src/pyramid/scaffolds/starter/MANIFEST.in_tmpl | 2 + src/pyramid/scaffolds/starter/README.txt_tmpl | 12 + src/pyramid/scaffolds/starter/development.ini_tmpl | 59 + src/pyramid/scaffolds/starter/production.ini_tmpl | 53 + src/pyramid/scaffolds/starter/pytest.ini_tmpl | 3 + src/pyramid/scaffolds/starter/setup.py_tmpl | 49 + src/pyramid/scaffolds/template.py | 172 + src/pyramid/scaffolds/tests.py | 75 + src/pyramid/scaffolds/zodb/+dot+coveragerc_tmpl | 3 + src/pyramid/scaffolds/zodb/+package+/__init__.py | 20 + src/pyramid/scaffolds/zodb/+package+/models.py | 14 + .../zodb/+package+/static/pyramid-16x16.png | Bin 0 -> 1319 bytes .../scaffolds/zodb/+package+/static/pyramid.png | Bin 0 -> 12901 bytes .../scaffolds/zodb/+package+/static/theme.css | 154 + .../zodb/+package+/templates/mytemplate.pt_tmpl | 67 + src/pyramid/scaffolds/zodb/+package+/tests.py_tmpl | 17 + src/pyramid/scaffolds/zodb/+package+/views.py_tmpl | 7 + src/pyramid/scaffolds/zodb/CHANGES.txt_tmpl | 4 + src/pyramid/scaffolds/zodb/MANIFEST.in_tmpl | 2 + src/pyramid/scaffolds/zodb/README.txt_tmpl | 12 + src/pyramid/scaffolds/zodb/development.ini_tmpl | 64 + src/pyramid/scaffolds/zodb/production.ini_tmpl | 59 + src/pyramid/scaffolds/zodb/pytest.ini_tmpl | 3 + src/pyramid/scaffolds/zodb/setup.py_tmpl | 53 + src/pyramid/scripting.py | 141 + src/pyramid/scripts/__init__.py | 1 + src/pyramid/scripts/common.py | 23 + src/pyramid/scripts/pcreate.py | 251 ++ src/pyramid/scripts/pdistreport.py | 43 + src/pyramid/scripts/prequest.py | 207 ++ src/pyramid/scripts/proutes.py | 416 +++ src/pyramid/scripts/pserve.py | 383 +++ src/pyramid/scripts/pshell.py | 270 ++ src/pyramid/scripts/ptweens.py | 109 + src/pyramid/scripts/pviews.py | 289 ++ src/pyramid/security.py | 427 +++ src/pyramid/session.py | 712 ++++ src/pyramid/settings.py | 33 + src/pyramid/static.py | 301 ++ src/pyramid/testing.py | 641 ++++ src/pyramid/tests/__init__.py | 3 + src/pyramid/tests/fixtures/dummy.ini | 4 + src/pyramid/tests/fixtures/manifest.json | 4 + src/pyramid/tests/fixtures/manifest2.json | 4 + src/pyramid/tests/fixtures/minimal.jpg | Bin 0 -> 631 bytes src/pyramid/tests/fixtures/minimal.pdf | Bin 0 -> 1054 bytes src/pyramid/tests/fixtures/minimal.txt | 1 + src/pyramid/tests/fixtures/minimal.xml | 1 + src/pyramid/tests/fixtures/nonminimal.txt | 1 + src/pyramid/tests/fixtures/static/.hiddenfile | 2 + src/pyramid/tests/fixtures/static/arcs.svg.tgz | 73 + src/pyramid/tests/fixtures/static/index.html | 1 + .../tests/fixtures/static/subdir/index.html | 1 + src/pyramid/tests/pkgs/__init__.py | 1 + src/pyramid/tests/pkgs/ccbugapp/__init__.py | 16 + src/pyramid/tests/pkgs/conflictapp/__init__.py | 24 + src/pyramid/tests/pkgs/conflictapp/included.py | 6 + src/pyramid/tests/pkgs/defpermbugapp/__init__.py | 26 + src/pyramid/tests/pkgs/eventonly/__init__.py | 64 + .../tests/pkgs/exceptionviewapp/__init__.py | 31 + src/pyramid/tests/pkgs/exceptionviewapp/models.py | 18 + src/pyramid/tests/pkgs/exceptionviewapp/views.py | 24 + src/pyramid/tests/pkgs/fixtureapp/__init__.py | 12 + src/pyramid/tests/pkgs/fixtureapp/models.py | 8 + .../tests/pkgs/fixtureapp/subpackage/__init__.py | 1 + src/pyramid/tests/pkgs/fixtureapp/views.py | 22 + src/pyramid/tests/pkgs/forbiddenapp/__init__.py | 24 + src/pyramid/tests/pkgs/forbiddenview/__init__.py | 31 + src/pyramid/tests/pkgs/hybridapp/__init__.py | 39 + src/pyramid/tests/pkgs/hybridapp/views.py | 39 + src/pyramid/tests/pkgs/includeapp1/__init__.py | 1 + src/pyramid/tests/pkgs/includeapp1/root.py | 10 + src/pyramid/tests/pkgs/includeapp1/three.py | 10 + src/pyramid/tests/pkgs/includeapp1/two.py | 9 + src/pyramid/tests/pkgs/localeapp/__init__.py | 1 + src/pyramid/tests/pkgs/localeapp/locale/GARBAGE | 1 + .../tests/pkgs/localeapp/locale/be/LC_MESSAGES | 1 + .../localeapp/locale/de/LC_MESSAGES/deformsite.mo | Bin 0 -> 543 bytes .../localeapp/locale/de/LC_MESSAGES/deformsite.po | 31 + .../locale/de_DE/LC_MESSAGES/deformsite.mo | Bin 0 -> 531 bytes .../locale/de_DE/LC_MESSAGES/deformsite.po | 26 + .../localeapp/locale/en/LC_MESSAGES/deformsite.mo | Bin 0 -> 543 bytes .../localeapp/locale/en/LC_MESSAGES/deformsite.po | 31 + src/pyramid/tests/pkgs/localeapp/locale2/GARBAGE | 1 + .../tests/pkgs/localeapp/locale2/be/LC_MESSAGES | 1 + .../localeapp/locale2/de/LC_MESSAGES/deformsite.mo | Bin 0 -> 543 bytes .../localeapp/locale2/de/LC_MESSAGES/deformsite.po | 31 + .../localeapp/locale2/en/LC_MESSAGES/deformsite.mo | Bin 0 -> 543 bytes .../localeapp/locale2/en/LC_MESSAGES/deformsite.po | 31 + src/pyramid/tests/pkgs/localeapp/locale3/GARBAGE | 1 + .../tests/pkgs/localeapp/locale3/be/LC_MESSAGES | 1 + .../localeapp/locale3/de/LC_MESSAGES/deformsite.mo | Bin 0 -> 543 bytes .../localeapp/locale3/de/LC_MESSAGES/deformsite.po | 31 + .../localeapp/locale3/en/LC_MESSAGES/deformsite.mo | Bin 0 -> 543 bytes .../localeapp/locale3/en/LC_MESSAGES/deformsite.po | 31 + src/pyramid/tests/pkgs/notfoundview/__init__.py | 30 + src/pyramid/tests/pkgs/permbugapp/__init__.py | 22 + src/pyramid/tests/pkgs/rendererscanapp/__init__.py | 9 + .../tests/pkgs/rendererscanapp/two/__init__.py | 6 + src/pyramid/tests/pkgs/restbugapp/__init__.py | 15 + src/pyramid/tests/pkgs/restbugapp/views.py | 15 + src/pyramid/tests/pkgs/static_abspath/__init__.py | 7 + .../tests/pkgs/static_assetspec/__init__.py | 3 + .../tests/pkgs/static_routeprefix/__init__.py | 7 + src/pyramid/tests/pkgs/staticpermapp/__init__.py | 25 + src/pyramid/tests/pkgs/subrequestapp/__init__.py | 52 + .../tests/pkgs/viewdecoratorapp/__init__.py | 3 + .../tests/pkgs/viewdecoratorapp/views/__init__.py | 1 + .../tests/pkgs/viewdecoratorapp/views/views.py | 12 + src/pyramid/tests/pkgs/wsgiapp2app/__init__.py | 17 + src/pyramid/tests/test_asset.py | 88 + src/pyramid/tests/test_authentication.py | 1738 ++++++++++ src/pyramid/tests/test_authorization.py | 259 ++ src/pyramid/tests/test_compat.py | 26 + src/pyramid/tests/test_config/__init__.py | 53 + .../tests/test_config/files/assets/dummy.txt | 1 + src/pyramid/tests/test_config/files/minimal.txt | 1 + .../tests/test_config/path/scanerror/__init__.py | 3 + .../test_config/path/scanerror/will_raise_error.py | 1 + src/pyramid/tests/test_config/pkgs/__init__.py | 2 + .../tests/test_config/pkgs/asset/__init__.py | 3 + .../test_config/pkgs/asset/subpackage/__init__.py | 1 + .../pkgs/asset/subpackage/templates/bar.pt | 0 .../tests/test_config/pkgs/scanextrakw/__init__.py | 14 + .../tests/test_config/pkgs/scannable/__init__.py | 96 + .../tests/test_config/pkgs/scannable/another.py | 69 + .../test_config/pkgs/scannable/pod/notinit.py | 6 + .../pkgs/scannable/subpackage/__init__.py | 6 + .../pkgs/scannable/subpackage/notinit.py | 6 + .../scannable/subpackage/subsubpackage/__init__.py | 6 + .../tests/test_config/pkgs/selfscan/__init__.py | 11 + .../tests/test_config/pkgs/selfscan/another.py | 6 + src/pyramid/tests/test_config/test_adapters.py | 365 ++ src/pyramid/tests/test_config/test_assets.py | 945 +++++ src/pyramid/tests/test_config/test_factories.py | 163 + src/pyramid/tests/test_config/test_i18n.py | 132 + src/pyramid/tests/test_config/test_init.py | 2068 +++++++++++ src/pyramid/tests/test_config/test_rendering.py | 34 + src/pyramid/tests/test_config/test_routes.py | 297 ++ src/pyramid/tests/test_config/test_security.py | 125 + src/pyramid/tests/test_config/test_settings.py | 582 ++++ src/pyramid/tests/test_config/test_testing.py | 205 ++ src/pyramid/tests/test_config/test_tweens.py | 410 +++ src/pyramid/tests/test_config/test_util.py | 497 +++ src/pyramid/tests/test_config/test_views.py | 3632 ++++++++++++++++++++ src/pyramid/tests/test_csrf.py | 420 +++ src/pyramid/tests/test_decorator.py | 26 + src/pyramid/tests/test_docs.py | 35 + src/pyramid/tests/test_encode.py | 86 + src/pyramid/tests/test_events.py | 332 ++ src/pyramid/tests/test_exceptions.py | 92 + src/pyramid/tests/test_httpexceptions.py | 482 +++ src/pyramid/tests/test_i18n.py | 508 +++ src/pyramid/tests/test_integration.py | 848 +++++ src/pyramid/tests/test_location.py | 40 + src/pyramid/tests/test_paster.py | 168 + src/pyramid/tests/test_path.py | 576 ++++ src/pyramid/tests/test_predicates.py | 556 +++ src/pyramid/tests/test_registry.py | 401 +++ src/pyramid/tests/test_renderers.py | 705 ++++ src/pyramid/tests/test_request.py | 588 ++++ src/pyramid/tests/test_response.py | 214 ++ src/pyramid/tests/test_router.py | 1410 ++++++++ src/pyramid/tests/test_scaffolds/__init__.py | 1 + .../fixture_scaffold/+package+/.badfile | 0 .../fixture_scaffold/+package+/__init__.py_tmpl | 12 + .../fixture_scaffold/+package+/resources.py | 3 + .../fixture_scaffold/+package+/static/favicon.ico | Bin 0 -> 1406 bytes .../fixture_scaffold/+package+/static/footerbg.png | Bin 0 -> 333 bytes .../fixture_scaffold/+package+/static/headerbg.png | Bin 0 -> 203 bytes .../fixture_scaffold/+package+/static/ie6.css | 8 + .../fixture_scaffold/+package+/static/middlebg.png | Bin 0 -> 2797 bytes .../fixture_scaffold/+package+/static/pylons.css | 65 + .../+package+/static/pyramid-small.png | Bin 0 -> 7044 bytes .../fixture_scaffold/+package+/static/pyramid.png | Bin 0 -> 33055 bytes .../+package+/static/transparent.gif | Bin 0 -> 49 bytes .../+package+/templates/mytemplate.pt_tmpl | 76 + .../+package+/test_no_content.py_tmpl | 0 .../fixture_scaffold/+package+/tests.py_tmpl | 16 + .../fixture_scaffold/+package+/views.py_tmpl | 2 + .../fixture_scaffold/CHANGES.txt_tmpl | 4 + .../fixture_scaffold/MANIFEST.in_tmpl | 2 + .../fixture_scaffold/README.txt_tmpl | 1 + .../fixture_scaffold/development.ini_tmpl | 45 + .../fixture_scaffold/production.ini_tmpl | 44 + .../test_scaffolds/fixture_scaffold/setup.py_tmpl | 38 + src/pyramid/tests/test_scaffolds/test_copydir.py | 455 +++ src/pyramid/tests/test_scaffolds/test_init.py | 21 + src/pyramid/tests/test_scaffolds/test_template.py | 155 + src/pyramid/tests/test_scripting.py | 221 ++ src/pyramid/tests/test_scripts/__init__.py | 1 + src/pyramid/tests/test_scripts/dummy.py | 190 + src/pyramid/tests/test_scripts/pystartup.txt | 3 + src/pyramid/tests/test_scripts/test_common.py | 13 + src/pyramid/tests/test_scripts/test_pcreate.py | 309 ++ src/pyramid/tests/test_scripts/test_pdistreport.py | 73 + src/pyramid/tests/test_scripts/test_prequest.py | 214 ++ src/pyramid/tests/test_scripts/test_proutes.py | 792 +++++ src/pyramid/tests/test_scripts/test_pserve.py | 131 + src/pyramid/tests/test_scripts/test_pshell.py | 398 +++ src/pyramid/tests/test_scripts/test_ptweens.py | 62 + src/pyramid/tests/test_scripts/test_pviews.py | 501 +++ src/pyramid/tests/test_security.py | 549 +++ src/pyramid/tests/test_session.py | 754 ++++ src/pyramid/tests/test_settings.py | 80 + src/pyramid/tests/test_static.py | 477 +++ src/pyramid/tests/test_testing.py | 689 ++++ src/pyramid/tests/test_threadlocal.py | 95 + src/pyramid/tests/test_traversal.py | 1221 +++++++ src/pyramid/tests/test_tweens.py | 88 + src/pyramid/tests/test_url.py | 1352 ++++++++ src/pyramid/tests/test_urldispatch.py | 539 +++ src/pyramid/tests/test_util.py | 1111 ++++++ src/pyramid/tests/test_view.py | 1071 ++++++ src/pyramid/tests/test_viewderivers.py | 1795 ++++++++++ src/pyramid/tests/test_wsgi.py | 130 + src/pyramid/threadlocal.py | 83 + src/pyramid/traversal.py | 760 ++++ src/pyramid/tweens.py | 48 + src/pyramid/url.py | 894 +++++ src/pyramid/urldispatch.py | 249 ++ src/pyramid/util.py | 651 ++++ src/pyramid/view.py | 761 ++++ src/pyramid/viewderivers.py | 472 +++ src/pyramid/wsgi.py | 85 + 300 files changed, 59180 insertions(+) create mode 100644 src/pyramid/__init__.py create mode 100644 src/pyramid/asset.py create mode 100644 src/pyramid/authentication.py create mode 100644 src/pyramid/authorization.py create mode 100644 src/pyramid/compat.py create mode 100644 src/pyramid/config/__init__.py create mode 100644 src/pyramid/config/adapters.py create mode 100644 src/pyramid/config/assets.py create mode 100644 src/pyramid/config/factories.py create mode 100644 src/pyramid/config/i18n.py create mode 100644 src/pyramid/config/predicates.py create mode 100644 src/pyramid/config/rendering.py create mode 100644 src/pyramid/config/routes.py create mode 100644 src/pyramid/config/security.py create mode 100644 src/pyramid/config/settings.py create mode 100644 src/pyramid/config/testing.py create mode 100644 src/pyramid/config/tweens.py create mode 100644 src/pyramid/config/util.py create mode 100644 src/pyramid/config/views.py create mode 100644 src/pyramid/config/zca.py create mode 100644 src/pyramid/csrf.py create mode 100644 src/pyramid/decorator.py create mode 100644 src/pyramid/encode.py create mode 100644 src/pyramid/events.py create mode 100644 src/pyramid/exceptions.py create mode 100644 src/pyramid/httpexceptions.py create mode 100644 src/pyramid/i18n.py create mode 100644 src/pyramid/interfaces.py create mode 100644 src/pyramid/location.py create mode 100644 src/pyramid/paster.py create mode 100644 src/pyramid/path.py create mode 100644 src/pyramid/predicates.py create mode 100644 src/pyramid/registry.py create mode 100644 src/pyramid/renderers.py create mode 100644 src/pyramid/request.py create mode 100644 src/pyramid/resource.py create mode 100644 src/pyramid/response.py create mode 100644 src/pyramid/router.py create mode 100644 src/pyramid/scaffolds/__init__.py create mode 100644 src/pyramid/scaffolds/alchemy/+dot+coveragerc_tmpl create mode 100644 src/pyramid/scaffolds/alchemy/+package+/__init__.py create mode 100644 src/pyramid/scaffolds/alchemy/+package+/models/__init__.py_tmpl create mode 100644 src/pyramid/scaffolds/alchemy/+package+/models/meta.py create mode 100644 src/pyramid/scaffolds/alchemy/+package+/models/mymodel.py create mode 100644 src/pyramid/scaffolds/alchemy/+package+/routes.py create mode 100644 src/pyramid/scaffolds/alchemy/+package+/scripts/__init__.py create mode 100644 src/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py create mode 100644 src/pyramid/scaffolds/alchemy/+package+/static/pyramid-16x16.png create mode 100644 src/pyramid/scaffolds/alchemy/+package+/static/pyramid.png create mode 100644 src/pyramid/scaffolds/alchemy/+package+/static/theme.css create mode 100644 src/pyramid/scaffolds/alchemy/+package+/templates/404.jinja2_tmpl create mode 100644 src/pyramid/scaffolds/alchemy/+package+/templates/layout.jinja2_tmpl create mode 100644 src/pyramid/scaffolds/alchemy/+package+/templates/mytemplate.jinja2_tmpl create mode 100644 src/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl create mode 100644 src/pyramid/scaffolds/alchemy/+package+/views/__init__.py create mode 100644 src/pyramid/scaffolds/alchemy/+package+/views/default.py_tmpl create mode 100644 src/pyramid/scaffolds/alchemy/+package+/views/notfound.py_tmpl create mode 100644 src/pyramid/scaffolds/alchemy/CHANGES.txt_tmpl create mode 100644 src/pyramid/scaffolds/alchemy/MANIFEST.in_tmpl create mode 100644 src/pyramid/scaffolds/alchemy/README.txt_tmpl create mode 100644 src/pyramid/scaffolds/alchemy/development.ini_tmpl create mode 100644 src/pyramid/scaffolds/alchemy/production.ini_tmpl create mode 100644 src/pyramid/scaffolds/alchemy/pytest.ini_tmpl create mode 100644 src/pyramid/scaffolds/alchemy/setup.py_tmpl create mode 100644 src/pyramid/scaffolds/copydir.py create mode 100644 src/pyramid/scaffolds/starter/+dot+coveragerc_tmpl create mode 100644 src/pyramid/scaffolds/starter/+package+/__init__.py create mode 100644 src/pyramid/scaffolds/starter/+package+/static/pyramid-16x16.png create mode 100644 src/pyramid/scaffolds/starter/+package+/static/pyramid.png create mode 100644 src/pyramid/scaffolds/starter/+package+/static/theme.css create mode 100644 src/pyramid/scaffolds/starter/+package+/templates/layout.jinja2_tmpl create mode 100644 src/pyramid/scaffolds/starter/+package+/templates/mytemplate.jinja2_tmpl create mode 100644 src/pyramid/scaffolds/starter/+package+/tests.py_tmpl create mode 100644 src/pyramid/scaffolds/starter/+package+/views.py_tmpl create mode 100644 src/pyramid/scaffolds/starter/CHANGES.txt_tmpl create mode 100644 src/pyramid/scaffolds/starter/MANIFEST.in_tmpl create mode 100644 src/pyramid/scaffolds/starter/README.txt_tmpl create mode 100644 src/pyramid/scaffolds/starter/development.ini_tmpl create mode 100644 src/pyramid/scaffolds/starter/production.ini_tmpl create mode 100644 src/pyramid/scaffolds/starter/pytest.ini_tmpl create mode 100644 src/pyramid/scaffolds/starter/setup.py_tmpl create mode 100644 src/pyramid/scaffolds/template.py create mode 100644 src/pyramid/scaffolds/tests.py create mode 100644 src/pyramid/scaffolds/zodb/+dot+coveragerc_tmpl create mode 100644 src/pyramid/scaffolds/zodb/+package+/__init__.py create mode 100644 src/pyramid/scaffolds/zodb/+package+/models.py create mode 100644 src/pyramid/scaffolds/zodb/+package+/static/pyramid-16x16.png create mode 100644 src/pyramid/scaffolds/zodb/+package+/static/pyramid.png create mode 100644 src/pyramid/scaffolds/zodb/+package+/static/theme.css create mode 100644 src/pyramid/scaffolds/zodb/+package+/templates/mytemplate.pt_tmpl create mode 100644 src/pyramid/scaffolds/zodb/+package+/tests.py_tmpl create mode 100644 src/pyramid/scaffolds/zodb/+package+/views.py_tmpl create mode 100644 src/pyramid/scaffolds/zodb/CHANGES.txt_tmpl create mode 100644 src/pyramid/scaffolds/zodb/MANIFEST.in_tmpl create mode 100644 src/pyramid/scaffolds/zodb/README.txt_tmpl create mode 100644 src/pyramid/scaffolds/zodb/development.ini_tmpl create mode 100644 src/pyramid/scaffolds/zodb/production.ini_tmpl create mode 100644 src/pyramid/scaffolds/zodb/pytest.ini_tmpl create mode 100644 src/pyramid/scaffolds/zodb/setup.py_tmpl create mode 100644 src/pyramid/scripting.py create mode 100644 src/pyramid/scripts/__init__.py create mode 100644 src/pyramid/scripts/common.py create mode 100644 src/pyramid/scripts/pcreate.py create mode 100644 src/pyramid/scripts/pdistreport.py create mode 100644 src/pyramid/scripts/prequest.py create mode 100644 src/pyramid/scripts/proutes.py create mode 100644 src/pyramid/scripts/pserve.py create mode 100644 src/pyramid/scripts/pshell.py create mode 100644 src/pyramid/scripts/ptweens.py create mode 100644 src/pyramid/scripts/pviews.py create mode 100644 src/pyramid/security.py create mode 100644 src/pyramid/session.py create mode 100644 src/pyramid/settings.py create mode 100644 src/pyramid/static.py create mode 100644 src/pyramid/testing.py create mode 100644 src/pyramid/tests/__init__.py create mode 100644 src/pyramid/tests/fixtures/dummy.ini create mode 100644 src/pyramid/tests/fixtures/manifest.json create mode 100644 src/pyramid/tests/fixtures/manifest2.json create mode 100644 src/pyramid/tests/fixtures/minimal.jpg create mode 100755 src/pyramid/tests/fixtures/minimal.pdf create mode 100644 src/pyramid/tests/fixtures/minimal.txt create mode 100644 src/pyramid/tests/fixtures/minimal.xml create mode 100644 src/pyramid/tests/fixtures/nonminimal.txt create mode 100644 src/pyramid/tests/fixtures/static/.hiddenfile create mode 100644 src/pyramid/tests/fixtures/static/arcs.svg.tgz create mode 100644 src/pyramid/tests/fixtures/static/index.html create mode 100644 src/pyramid/tests/fixtures/static/subdir/index.html create mode 100644 src/pyramid/tests/pkgs/__init__.py create mode 100644 src/pyramid/tests/pkgs/ccbugapp/__init__.py create mode 100644 src/pyramid/tests/pkgs/conflictapp/__init__.py create mode 100644 src/pyramid/tests/pkgs/conflictapp/included.py create mode 100644 src/pyramid/tests/pkgs/defpermbugapp/__init__.py create mode 100644 src/pyramid/tests/pkgs/eventonly/__init__.py create mode 100644 src/pyramid/tests/pkgs/exceptionviewapp/__init__.py create mode 100644 src/pyramid/tests/pkgs/exceptionviewapp/models.py create mode 100644 src/pyramid/tests/pkgs/exceptionviewapp/views.py create mode 100644 src/pyramid/tests/pkgs/fixtureapp/__init__.py create mode 100644 src/pyramid/tests/pkgs/fixtureapp/models.py create mode 100644 src/pyramid/tests/pkgs/fixtureapp/subpackage/__init__.py create mode 100644 src/pyramid/tests/pkgs/fixtureapp/views.py create mode 100644 src/pyramid/tests/pkgs/forbiddenapp/__init__.py create mode 100644 src/pyramid/tests/pkgs/forbiddenview/__init__.py create mode 100644 src/pyramid/tests/pkgs/hybridapp/__init__.py create mode 100644 src/pyramid/tests/pkgs/hybridapp/views.py create mode 100644 src/pyramid/tests/pkgs/includeapp1/__init__.py create mode 100644 src/pyramid/tests/pkgs/includeapp1/root.py create mode 100644 src/pyramid/tests/pkgs/includeapp1/three.py create mode 100644 src/pyramid/tests/pkgs/includeapp1/two.py create mode 100644 src/pyramid/tests/pkgs/localeapp/__init__.py create mode 100644 src/pyramid/tests/pkgs/localeapp/locale/GARBAGE create mode 100644 src/pyramid/tests/pkgs/localeapp/locale/be/LC_MESSAGES create mode 100644 src/pyramid/tests/pkgs/localeapp/locale/de/LC_MESSAGES/deformsite.mo create mode 100644 src/pyramid/tests/pkgs/localeapp/locale/de/LC_MESSAGES/deformsite.po create mode 100644 src/pyramid/tests/pkgs/localeapp/locale/de_DE/LC_MESSAGES/deformsite.mo create mode 100644 src/pyramid/tests/pkgs/localeapp/locale/de_DE/LC_MESSAGES/deformsite.po create mode 100644 src/pyramid/tests/pkgs/localeapp/locale/en/LC_MESSAGES/deformsite.mo create mode 100644 src/pyramid/tests/pkgs/localeapp/locale/en/LC_MESSAGES/deformsite.po create mode 100644 src/pyramid/tests/pkgs/localeapp/locale2/GARBAGE create mode 100644 src/pyramid/tests/pkgs/localeapp/locale2/be/LC_MESSAGES create mode 100644 src/pyramid/tests/pkgs/localeapp/locale2/de/LC_MESSAGES/deformsite.mo create mode 100644 src/pyramid/tests/pkgs/localeapp/locale2/de/LC_MESSAGES/deformsite.po create mode 100644 src/pyramid/tests/pkgs/localeapp/locale2/en/LC_MESSAGES/deformsite.mo create mode 100644 src/pyramid/tests/pkgs/localeapp/locale2/en/LC_MESSAGES/deformsite.po create mode 100644 src/pyramid/tests/pkgs/localeapp/locale3/GARBAGE create mode 100644 src/pyramid/tests/pkgs/localeapp/locale3/be/LC_MESSAGES create mode 100644 src/pyramid/tests/pkgs/localeapp/locale3/de/LC_MESSAGES/deformsite.mo create mode 100644 src/pyramid/tests/pkgs/localeapp/locale3/de/LC_MESSAGES/deformsite.po create mode 100644 src/pyramid/tests/pkgs/localeapp/locale3/en/LC_MESSAGES/deformsite.mo create mode 100644 src/pyramid/tests/pkgs/localeapp/locale3/en/LC_MESSAGES/deformsite.po create mode 100644 src/pyramid/tests/pkgs/notfoundview/__init__.py create mode 100644 src/pyramid/tests/pkgs/permbugapp/__init__.py create mode 100644 src/pyramid/tests/pkgs/rendererscanapp/__init__.py create mode 100644 src/pyramid/tests/pkgs/rendererscanapp/two/__init__.py create mode 100644 src/pyramid/tests/pkgs/restbugapp/__init__.py create mode 100644 src/pyramid/tests/pkgs/restbugapp/views.py create mode 100644 src/pyramid/tests/pkgs/static_abspath/__init__.py create mode 100644 src/pyramid/tests/pkgs/static_assetspec/__init__.py create mode 100644 src/pyramid/tests/pkgs/static_routeprefix/__init__.py create mode 100644 src/pyramid/tests/pkgs/staticpermapp/__init__.py create mode 100644 src/pyramid/tests/pkgs/subrequestapp/__init__.py create mode 100644 src/pyramid/tests/pkgs/viewdecoratorapp/__init__.py create mode 100644 src/pyramid/tests/pkgs/viewdecoratorapp/views/__init__.py create mode 100644 src/pyramid/tests/pkgs/viewdecoratorapp/views/views.py create mode 100644 src/pyramid/tests/pkgs/wsgiapp2app/__init__.py create mode 100644 src/pyramid/tests/test_asset.py create mode 100644 src/pyramid/tests/test_authentication.py create mode 100644 src/pyramid/tests/test_authorization.py create mode 100644 src/pyramid/tests/test_compat.py create mode 100644 src/pyramid/tests/test_config/__init__.py create mode 100644 src/pyramid/tests/test_config/files/assets/dummy.txt create mode 100644 src/pyramid/tests/test_config/files/minimal.txt create mode 100644 src/pyramid/tests/test_config/path/scanerror/__init__.py create mode 100644 src/pyramid/tests/test_config/path/scanerror/will_raise_error.py create mode 100644 src/pyramid/tests/test_config/pkgs/__init__.py create mode 100644 src/pyramid/tests/test_config/pkgs/asset/__init__.py create mode 100644 src/pyramid/tests/test_config/pkgs/asset/subpackage/__init__.py create mode 100644 src/pyramid/tests/test_config/pkgs/asset/subpackage/templates/bar.pt create mode 100644 src/pyramid/tests/test_config/pkgs/scanextrakw/__init__.py create mode 100644 src/pyramid/tests/test_config/pkgs/scannable/__init__.py create mode 100644 src/pyramid/tests/test_config/pkgs/scannable/another.py create mode 100644 src/pyramid/tests/test_config/pkgs/scannable/pod/notinit.py create mode 100644 src/pyramid/tests/test_config/pkgs/scannable/subpackage/__init__.py create mode 100644 src/pyramid/tests/test_config/pkgs/scannable/subpackage/notinit.py create mode 100644 src/pyramid/tests/test_config/pkgs/scannable/subpackage/subsubpackage/__init__.py create mode 100644 src/pyramid/tests/test_config/pkgs/selfscan/__init__.py create mode 100644 src/pyramid/tests/test_config/pkgs/selfscan/another.py create mode 100644 src/pyramid/tests/test_config/test_adapters.py create mode 100644 src/pyramid/tests/test_config/test_assets.py create mode 100644 src/pyramid/tests/test_config/test_factories.py create mode 100644 src/pyramid/tests/test_config/test_i18n.py create mode 100644 src/pyramid/tests/test_config/test_init.py create mode 100644 src/pyramid/tests/test_config/test_rendering.py create mode 100644 src/pyramid/tests/test_config/test_routes.py create mode 100644 src/pyramid/tests/test_config/test_security.py create mode 100644 src/pyramid/tests/test_config/test_settings.py create mode 100644 src/pyramid/tests/test_config/test_testing.py create mode 100644 src/pyramid/tests/test_config/test_tweens.py create mode 100644 src/pyramid/tests/test_config/test_util.py create mode 100644 src/pyramid/tests/test_config/test_views.py create mode 100644 src/pyramid/tests/test_csrf.py create mode 100644 src/pyramid/tests/test_decorator.py create mode 100644 src/pyramid/tests/test_docs.py create mode 100644 src/pyramid/tests/test_encode.py create mode 100644 src/pyramid/tests/test_events.py create mode 100644 src/pyramid/tests/test_exceptions.py create mode 100644 src/pyramid/tests/test_httpexceptions.py create mode 100644 src/pyramid/tests/test_i18n.py create mode 100644 src/pyramid/tests/test_integration.py create mode 100644 src/pyramid/tests/test_location.py create mode 100644 src/pyramid/tests/test_paster.py create mode 100644 src/pyramid/tests/test_path.py create mode 100644 src/pyramid/tests/test_predicates.py create mode 100644 src/pyramid/tests/test_registry.py create mode 100644 src/pyramid/tests/test_renderers.py create mode 100644 src/pyramid/tests/test_request.py create mode 100644 src/pyramid/tests/test_response.py create mode 100644 src/pyramid/tests/test_router.py create mode 100644 src/pyramid/tests/test_scaffolds/__init__.py create mode 100644 src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/.badfile create mode 100644 src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/__init__.py_tmpl create mode 100644 src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/resources.py create mode 100644 src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/static/favicon.ico create mode 100644 src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/static/footerbg.png create mode 100644 src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/static/headerbg.png create mode 100644 src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/static/ie6.css create mode 100644 src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/static/middlebg.png create mode 100644 src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/static/pylons.css create mode 100644 src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/static/pyramid-small.png create mode 100644 src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/static/pyramid.png create mode 100644 src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/static/transparent.gif create mode 100644 src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/templates/mytemplate.pt_tmpl create mode 100644 src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/test_no_content.py_tmpl create mode 100644 src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/tests.py_tmpl create mode 100644 src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/views.py_tmpl create mode 100644 src/pyramid/tests/test_scaffolds/fixture_scaffold/CHANGES.txt_tmpl create mode 100644 src/pyramid/tests/test_scaffolds/fixture_scaffold/MANIFEST.in_tmpl create mode 100644 src/pyramid/tests/test_scaffolds/fixture_scaffold/README.txt_tmpl create mode 100644 src/pyramid/tests/test_scaffolds/fixture_scaffold/development.ini_tmpl create mode 100644 src/pyramid/tests/test_scaffolds/fixture_scaffold/production.ini_tmpl create mode 100644 src/pyramid/tests/test_scaffolds/fixture_scaffold/setup.py_tmpl create mode 100644 src/pyramid/tests/test_scaffolds/test_copydir.py create mode 100644 src/pyramid/tests/test_scaffolds/test_init.py create mode 100644 src/pyramid/tests/test_scaffolds/test_template.py create mode 100644 src/pyramid/tests/test_scripting.py create mode 100644 src/pyramid/tests/test_scripts/__init__.py create mode 100644 src/pyramid/tests/test_scripts/dummy.py create mode 100644 src/pyramid/tests/test_scripts/pystartup.txt create mode 100644 src/pyramid/tests/test_scripts/test_common.py create mode 100644 src/pyramid/tests/test_scripts/test_pcreate.py create mode 100644 src/pyramid/tests/test_scripts/test_pdistreport.py create mode 100644 src/pyramid/tests/test_scripts/test_prequest.py create mode 100644 src/pyramid/tests/test_scripts/test_proutes.py create mode 100644 src/pyramid/tests/test_scripts/test_pserve.py create mode 100644 src/pyramid/tests/test_scripts/test_pshell.py create mode 100644 src/pyramid/tests/test_scripts/test_ptweens.py create mode 100644 src/pyramid/tests/test_scripts/test_pviews.py create mode 100644 src/pyramid/tests/test_security.py create mode 100644 src/pyramid/tests/test_session.py create mode 100644 src/pyramid/tests/test_settings.py create mode 100644 src/pyramid/tests/test_static.py create mode 100644 src/pyramid/tests/test_testing.py create mode 100644 src/pyramid/tests/test_threadlocal.py create mode 100644 src/pyramid/tests/test_traversal.py create mode 100644 src/pyramid/tests/test_tweens.py create mode 100644 src/pyramid/tests/test_url.py create mode 100644 src/pyramid/tests/test_urldispatch.py create mode 100644 src/pyramid/tests/test_util.py create mode 100644 src/pyramid/tests/test_view.py create mode 100644 src/pyramid/tests/test_viewderivers.py create mode 100644 src/pyramid/tests/test_wsgi.py create mode 100644 src/pyramid/threadlocal.py create mode 100644 src/pyramid/traversal.py create mode 100644 src/pyramid/tweens.py create mode 100644 src/pyramid/url.py create mode 100644 src/pyramid/urldispatch.py create mode 100644 src/pyramid/util.py create mode 100644 src/pyramid/view.py create mode 100644 src/pyramid/viewderivers.py create mode 100644 src/pyramid/wsgi.py (limited to 'src') diff --git a/src/pyramid/__init__.py b/src/pyramid/__init__.py new file mode 100644 index 000000000..5bb534f79 --- /dev/null +++ b/src/pyramid/__init__.py @@ -0,0 +1 @@ +# package diff --git a/src/pyramid/asset.py b/src/pyramid/asset.py new file mode 100644 index 000000000..9d7a3ee63 --- /dev/null +++ b/src/pyramid/asset.py @@ -0,0 +1,44 @@ +import os +import pkg_resources + +from pyramid.compat import string_types + +from pyramid.path import ( + package_path, + package_name, + ) + +def resolve_asset_spec(spec, pname='__main__'): + if pname and not isinstance(pname, string_types): + pname = pname.__name__ # as package + if os.path.isabs(spec): + return None, spec + filename = spec + if ':' in spec: + pname, filename = spec.split(':', 1) + elif pname is None: + pname, filename = None, spec + return pname, filename + +def asset_spec_from_abspath(abspath, package): + """ Try to convert an absolute path to a resource in a package to + a resource specification if possible; otherwise return the + absolute path. """ + if getattr(package, '__name__', None) == '__main__': + return abspath + pp = package_path(package) + os.path.sep + if abspath.startswith(pp): + relpath = abspath[len(pp):] + return '%s:%s' % (package_name(package), + relpath.replace(os.path.sep, '/')) + return abspath + +# bw compat only; use pyramid.path.AssetResolver().resolve(spec).abspath() +def abspath_from_asset_spec(spec, pname='__main__'): + if pname is None: + return spec + pname, filename = resolve_asset_spec(spec, pname) + if pname is None: + return filename + return pkg_resources.resource_filename(pname, filename) + diff --git a/src/pyramid/authentication.py b/src/pyramid/authentication.py new file mode 100644 index 000000000..a9604e336 --- /dev/null +++ b/src/pyramid/authentication.py @@ -0,0 +1,1201 @@ +import binascii +from codecs import utf_8_decode +from codecs import utf_8_encode +from collections import namedtuple +import hashlib +import base64 +import re +import time as time_mod +import warnings + +from zope.interface import implementer + +from webob.cookies import CookieProfile + +from pyramid.compat import ( + long, + text_type, + binary_type, + url_unquote, + url_quote, + bytes_, + ascii_native_, + native_, + ) + +from pyramid.interfaces import ( + IAuthenticationPolicy, + IDebugLogger, + ) + +from pyramid.security import ( + Authenticated, + Everyone, + ) + +from pyramid.util import strings_differ +from pyramid.util import SimpleSerializer + +VALID_TOKEN = re.compile(r"^[A-Za-z][A-Za-z0-9+_-]*$") + + +class CallbackAuthenticationPolicy(object): + """ Abstract class """ + + debug = False + callback = None + + def _log(self, msg, methodname, request): + logger = request.registry.queryUtility(IDebugLogger) + if logger: + cls = self.__class__ + classname = cls.__module__ + '.' + cls.__name__ + methodname = classname + '.' + methodname + logger.debug(methodname + ': ' + msg) + + def _clean_principal(self, princid): + if princid in (Authenticated, Everyone): + princid = None + return princid + + def authenticated_userid(self, request): + """ Return the authenticated userid or ``None``. + + If no callback is registered, this will be the same as + ``unauthenticated_userid``. + + If a ``callback`` is registered, this will return the userid if + and only if the callback returns a value that is not ``None``. + + """ + debug = self.debug + userid = self.unauthenticated_userid(request) + if userid is None: + debug and self._log( + 'call to unauthenticated_userid returned None; returning None', + 'authenticated_userid', + request) + return None + if self._clean_principal(userid) is None: + debug and self._log( + ('use of userid %r is disallowed by any built-in Pyramid ' + 'security policy, returning None' % userid), + 'authenticated_userid', + request) + return None + + if self.callback is None: + debug and self._log( + 'there was no groupfinder callback; returning %r' % (userid,), + 'authenticated_userid', + request) + return userid + callback_ok = self.callback(userid, request) + if callback_ok is not None: # is not None! + debug and self._log( + 'groupfinder callback returned %r; returning %r' % ( + callback_ok, userid), + 'authenticated_userid', + request + ) + return userid + debug and self._log( + 'groupfinder callback returned None; returning None', + 'authenticated_userid', + request + ) + + def effective_principals(self, request): + """ A list of effective principals derived from request. + + This will return a list of principals including, at least, + :data:`pyramid.security.Everyone`. If there is no authenticated + userid, or the ``callback`` returns ``None``, this will be the + only principal: + + .. code-block:: python + + return [Everyone] + + If the ``callback`` does not return ``None`` and an authenticated + userid is found, then the principals will include + :data:`pyramid.security.Authenticated`, the ``authenticated_userid`` + and the list of principals returned by the ``callback``: + + .. code-block:: python + + extra_principals = callback(userid, request) + return [Everyone, Authenticated, userid] + extra_principals + + """ + debug = self.debug + effective_principals = [Everyone] + userid = self.unauthenticated_userid(request) + + if userid is None: + debug and self._log( + 'unauthenticated_userid returned %r; returning %r' % ( + userid, effective_principals), + 'effective_principals', + request + ) + return effective_principals + + if self._clean_principal(userid) is None: + debug and self._log( + ('unauthenticated_userid returned disallowed %r; returning %r ' + 'as if it was None' % (userid, effective_principals)), + 'effective_principals', + request + ) + return effective_principals + + if self.callback is None: + debug and self._log( + 'groupfinder callback is None, so groups is []', + 'effective_principals', + request) + groups = [] + else: + groups = self.callback(userid, request) + debug and self._log( + 'groupfinder callback returned %r as groups' % (groups,), + 'effective_principals', + request) + + if groups is None: # is None! + debug and self._log( + 'returning effective principals: %r' % ( + effective_principals,), + 'effective_principals', + request + ) + return effective_principals + + effective_principals.append(Authenticated) + effective_principals.append(userid) + effective_principals.extend(groups) + + debug and self._log( + 'returning effective principals: %r' % ( + effective_principals,), + 'effective_principals', + request + ) + return effective_principals + + +@implementer(IAuthenticationPolicy) +class RepozeWho1AuthenticationPolicy(CallbackAuthenticationPolicy): + """ A :app:`Pyramid` :term:`authentication policy` which + obtains data from the :mod:`repoze.who` 1.X WSGI 'API' (the + ``repoze.who.identity`` key in the WSGI environment). + + Constructor Arguments + + ``identifier_name`` + + Default: ``auth_tkt``. The :mod:`repoze.who` plugin name that + performs remember/forget. Optional. + + ``callback`` + + Default: ``None``. A callback passed the :mod:`repoze.who` identity + and the :term:`request`, expected to return ``None`` if the user + represented by the identity doesn't exist or a sequence of principal + identifiers (possibly empty) representing groups if the user does + exist. If ``callback`` is None, the userid will be assumed to exist + with no group principals. + + Objects of this class implement the interface described by + :class:`pyramid.interfaces.IAuthenticationPolicy`. + """ + + def __init__(self, identifier_name='auth_tkt', callback=None): + self.identifier_name = identifier_name + self.callback = callback + + def _get_identity(self, request): + return request.environ.get('repoze.who.identity') + + def _get_identifier(self, request): + plugins = request.environ.get('repoze.who.plugins') + if plugins is None: + return None + identifier = plugins[self.identifier_name] + return identifier + + def authenticated_userid(self, request): + """ Return the authenticated userid or ``None``. + + If no callback is registered, this will be the same as + ``unauthenticated_userid``. + + If a ``callback`` is registered, this will return the userid if + and only if the callback returns a value that is not ``None``. + + """ + identity = self._get_identity(request) + + if identity is None: + self.debug and self._log( + 'repoze.who identity is None, returning None', + 'authenticated_userid', + request) + return None + + userid = identity['repoze.who.userid'] + + if userid is None: + self.debug and self._log( + 'repoze.who.userid is None, returning None' % userid, + 'authenticated_userid', + request) + return None + + if self._clean_principal(userid) is None: + self.debug and self._log( + ('use of userid %r is disallowed by any built-in Pyramid ' + 'security policy, returning None' % userid), + 'authenticated_userid', + request) + return None + + if self.callback is None: + return userid + + if self.callback(identity, request) is not None: # is not None! + return userid + + def unauthenticated_userid(self, request): + """ Return the ``repoze.who.userid`` key from the detected identity.""" + identity = self._get_identity(request) + if identity is None: + return None + return identity['repoze.who.userid'] + + def effective_principals(self, request): + """ A list of effective principals derived from the identity. + + This will return a list of principals including, at least, + :data:`pyramid.security.Everyone`. If there is no identity, or + the ``callback`` returns ``None``, this will be the only principal. + + If the ``callback`` does not return ``None`` and an identity is + found, then the principals will include + :data:`pyramid.security.Authenticated`, the ``authenticated_userid`` + and the list of principals returned by the ``callback``. + + """ + effective_principals = [Everyone] + identity = self._get_identity(request) + + if identity is None: + self.debug and self._log( + ('repoze.who identity was None; returning %r' % + effective_principals), + 'effective_principals', + request + ) + return effective_principals + + if self.callback is None: + groups = [] + else: + groups = self.callback(identity, request) + + if groups is None: # is None! + self.debug and self._log( + ('security policy groups callback returned None; returning %r' % + effective_principals), + 'effective_principals', + request + ) + return effective_principals + + userid = identity['repoze.who.userid'] + + if userid is None: + self.debug and self._log( + ('repoze.who.userid was None; returning %r' % + effective_principals), + 'effective_principals', + request + ) + return effective_principals + + if self._clean_principal(userid) is None: + self.debug and self._log( + ('unauthenticated_userid returned disallowed %r; returning %r ' + 'as if it was None' % (userid, effective_principals)), + 'effective_principals', + request + ) + return effective_principals + + effective_principals.append(Authenticated) + effective_principals.append(userid) + effective_principals.extend(groups) + return effective_principals + + def remember(self, request, userid, **kw): + """ Store the ``userid`` as ``repoze.who.userid``. + + The identity to authenticated to :mod:`repoze.who` + will contain the given userid as ``userid``, and + provide all keyword arguments as additional identity + keys. Useful keys could be ``max_age`` or ``userdata``. + """ + identifier = self._get_identifier(request) + if identifier is None: + return [] + environ = request.environ + identity = kw + identity['repoze.who.userid'] = userid + return identifier.remember(environ, identity) + + def forget(self, request): + """ Forget the current authenticated user. + + Return headers that, if included in a response, will delete the + cookie responsible for tracking the current user. + + """ + identifier = self._get_identifier(request) + if identifier is None: + return [] + identity = self._get_identity(request) + return identifier.forget(request.environ, identity) + +@implementer(IAuthenticationPolicy) +class RemoteUserAuthenticationPolicy(CallbackAuthenticationPolicy): + """ A :app:`Pyramid` :term:`authentication policy` which + obtains data from the ``REMOTE_USER`` WSGI environment variable. + + Constructor Arguments + + ``environ_key`` + + Default: ``REMOTE_USER``. The key in the WSGI environ which + provides the userid. + + ``callback`` + + Default: ``None``. A callback passed the userid and the request, + expected to return None if the userid doesn't exist or a sequence of + principal identifiers (possibly empty) representing groups if the + user does exist. If ``callback`` is None, the userid will be assumed + to exist with no group principals. + + ``debug`` + + Default: ``False``. If ``debug`` is ``True``, log messages to the + Pyramid debug logger about the results of various authentication + steps. The output from debugging is useful for reporting to maillist + or IRC channels when asking for support. + + Objects of this class implement the interface described by + :class:`pyramid.interfaces.IAuthenticationPolicy`. + """ + + def __init__(self, environ_key='REMOTE_USER', callback=None, debug=False): + self.environ_key = environ_key + self.callback = callback + self.debug = debug + + def unauthenticated_userid(self, request): + """ The ``REMOTE_USER`` value found within the ``environ``.""" + return request.environ.get(self.environ_key) + + def remember(self, request, userid, **kw): + """ A no-op. The ``REMOTE_USER`` does not provide a protocol for + remembering the user. This will be application-specific and can + be done somewhere else or in a subclass.""" + return [] + + def forget(self, request): + """ A no-op. The ``REMOTE_USER`` does not provide a protocol for + forgetting the user. This will be application-specific and can + be done somewhere else or in a subclass.""" + return [] + +@implementer(IAuthenticationPolicy) +class AuthTktAuthenticationPolicy(CallbackAuthenticationPolicy): + """A :app:`Pyramid` :term:`authentication policy` which + obtains data from a Pyramid "auth ticket" cookie. + + Constructor Arguments + + ``secret`` + + The secret (a string) used for auth_tkt cookie signing. This value + should be unique across all values provided to Pyramid for various + subsystem secrets (see :ref:`admonishment_against_secret_sharing`). + Required. + + ``callback`` + + Default: ``None``. A callback passed the userid and the + request, expected to return ``None`` if the userid doesn't + exist or a sequence of principal identifiers (possibly empty) if + the user does exist. If ``callback`` is ``None``, the userid + will be assumed to exist with no principals. Optional. + + ``cookie_name`` + + Default: ``auth_tkt``. The cookie name used + (string). Optional. + + ``secure`` + + Default: ``False``. Only send the cookie back over a secure + conn. Optional. + + ``include_ip`` + + Default: ``False``. Make the requesting IP address part of + the authentication data in the cookie. Optional. + + For IPv6 this option is not recommended. The ``mod_auth_tkt`` + specification does not specify how to handle IPv6 addresses, so using + this option in combination with IPv6 addresses may cause an + incompatible cookie. It ties the authentication ticket to that + individual's IPv6 address. + + ``timeout`` + + Default: ``None``. Maximum number of seconds which a newly + issued ticket will be considered valid. After this amount of + time, the ticket will expire (effectively logging the user + out). If this value is ``None``, the ticket never expires. + Optional. + + ``reissue_time`` + + Default: ``None``. If this parameter is set, it represents the number + of seconds that must pass before an authentication token cookie is + automatically reissued as the result of a request which requires + authentication. The duration is measured as the number of seconds + since the last auth_tkt cookie was issued and 'now'. If this value is + ``0``, a new ticket cookie will be reissued on every request which + requires authentication. + + A good rule of thumb: if you want auto-expired cookies based on + inactivity: set the ``timeout`` value to 1200 (20 mins) and set the + ``reissue_time`` value to perhaps a tenth of the ``timeout`` value + (120 or 2 mins). It's nonsensical to set the ``timeout`` value lower + than the ``reissue_time`` value, as the ticket will never be reissued + if so. However, such a configuration is not explicitly prevented. + + Optional. + + ``max_age`` + + Default: ``None``. The max age of the auth_tkt cookie, in + seconds. This differs from ``timeout`` inasmuch as ``timeout`` + represents the lifetime of the ticket contained in the cookie, + while this value represents the lifetime of the cookie itself. + When this value is set, the cookie's ``Max-Age`` and + ``Expires`` settings will be set, allowing the auth_tkt cookie + to last between browser sessions. It is typically nonsensical + to set this to a value that is lower than ``timeout`` or + ``reissue_time``, although it is not explicitly prevented. + Optional. + + ``path`` + + Default: ``/``. The path for which the auth_tkt cookie is valid. + May be desirable if the application only serves part of a domain. + Optional. + + ``http_only`` + + Default: ``False``. Hide cookie from JavaScript by setting the + HttpOnly flag. Not honored by all browsers. + Optional. + + ``wild_domain`` + + Default: ``True``. An auth_tkt cookie will be generated for the + wildcard domain. If your site is hosted as ``example.com`` this + will make the cookie available for sites underneath ``example.com`` + such as ``www.example.com``. + Optional. + + ``parent_domain`` + + Default: ``False``. An auth_tkt cookie will be generated for the + parent domain of the current site. For example if your site is + hosted under ``www.example.com`` a cookie will be generated for + ``.example.com``. This can be useful if you have multiple sites + sharing the same domain. This option supercedes the ``wild_domain`` + option. + Optional. + + ``domain`` + + Default: ``None``. If provided the auth_tkt cookie will only be + set for this domain. This option is not compatible with ``wild_domain`` + and ``parent_domain``. + Optional. + + ``hashalg`` + + Default: ``sha512`` (the literal string). + + Any hash algorithm supported by Python's ``hashlib.new()`` function + can be used as the ``hashalg``. + + Cookies generated by different instances of AuthTktAuthenticationPolicy + using different ``hashalg`` options are not compatible. Switching the + ``hashalg`` will imply that all existing users with a valid cookie will + be required to re-login. + + Optional. + + ``debug`` + + Default: ``False``. If ``debug`` is ``True``, log messages to the + Pyramid debug logger about the results of various authentication + steps. The output from debugging is useful for reporting to maillist + or IRC channels when asking for support. + + ``samesite`` + + Default: ``'Lax'``. The 'samesite' option of the session cookie. Set + the value to ``None`` to turn off the samesite option. + + This option is available as of :app:`Pyramid` 1.10. + + .. versionchanged:: 1.4 + + Added the ``hashalg`` option, defaulting to ``sha512``. + + .. versionchanged:: 1.5 + + Added the ``domain`` option. + + Added the ``parent_domain`` option. + + .. versionchanged:: 1.10 + + Added the ``samesite`` option and made the default ``'Lax'``. + + Objects of this class implement the interface described by + :class:`pyramid.interfaces.IAuthenticationPolicy`. + + """ + + def __init__(self, + secret, + callback=None, + cookie_name='auth_tkt', + secure=False, + include_ip=False, + timeout=None, + reissue_time=None, + max_age=None, + path="/", + http_only=False, + wild_domain=True, + debug=False, + hashalg='sha512', + parent_domain=False, + domain=None, + samesite='Lax', + ): + self.cookie = AuthTktCookieHelper( + secret, + cookie_name=cookie_name, + secure=secure, + include_ip=include_ip, + timeout=timeout, + reissue_time=reissue_time, + max_age=max_age, + http_only=http_only, + path=path, + wild_domain=wild_domain, + hashalg=hashalg, + parent_domain=parent_domain, + domain=domain, + samesite=samesite, + ) + self.callback = callback + self.debug = debug + + def unauthenticated_userid(self, request): + """ The userid key within the auth_tkt cookie.""" + result = self.cookie.identify(request) + if result: + return result['userid'] + + def remember(self, request, userid, **kw): + """ Accepts the following kw args: ``max_age=, + ``tokens=``. + + Return a list of headers which will set appropriate cookies on + the response. + + """ + return self.cookie.remember(request, userid, **kw) + + def forget(self, request): + """ A list of headers which will delete appropriate cookies.""" + return self.cookie.forget(request) + +def b64encode(v): + return base64.b64encode(bytes_(v)).strip().replace(b'\n', b'') + +def b64decode(v): + return base64.b64decode(bytes_(v)) + +# this class licensed under the MIT license (stolen from Paste) +class AuthTicket(object): + """ + This class represents an authentication token. You must pass in + the shared secret, the userid, and the IP address. Optionally you + can include tokens (a list of strings, representing role names), + 'user_data', which is arbitrary data available for your own use in + later scripts. Lastly, you can override the cookie name and + timestamp. + + Once you provide all the arguments, use .cookie_value() to + generate the appropriate authentication ticket. + + Usage:: + + token = AuthTicket('sharedsecret', 'username', + os.environ['REMOTE_ADDR'], tokens=['admin']) + val = token.cookie_value() + + """ + + def __init__(self, secret, userid, ip, tokens=(), user_data='', + time=None, cookie_name='auth_tkt', secure=False, + hashalg='md5'): + self.secret = secret + self.userid = userid + self.ip = ip + self.tokens = ','.join(tokens) + self.user_data = user_data + if time is None: + self.time = time_mod.time() + else: + self.time = time + self.cookie_name = cookie_name + self.secure = secure + self.hashalg = hashalg + + def digest(self): + return calculate_digest( + self.ip, self.time, self.secret, self.userid, self.tokens, + self.user_data, self.hashalg) + + def cookie_value(self): + v = '%s%08x%s!' % (self.digest(), int(self.time), + url_quote(self.userid)) + if self.tokens: + v += self.tokens + '!' + v += self.user_data + return v + +# this class licensed under the MIT license (stolen from Paste) +class BadTicket(Exception): + """ + Exception raised when a ticket can't be parsed. If we get far enough to + determine what the expected digest should have been, expected is set. + This should not be shown by default, but can be useful for debugging. + """ + def __init__(self, msg, expected=None): + self.expected = expected + Exception.__init__(self, msg) + +# this function licensed under the MIT license (stolen from Paste) +def parse_ticket(secret, ticket, ip, hashalg='md5'): + """ + Parse the ticket, returning (timestamp, userid, tokens, user_data). + + If the ticket cannot be parsed, a ``BadTicket`` exception will be raised + with an explanation. + """ + ticket = native_(ticket).strip('"') + digest_size = hashlib.new(hashalg).digest_size * 2 + digest = ticket[:digest_size] + try: + timestamp = int(ticket[digest_size:digest_size + 8], 16) + except ValueError as e: + raise BadTicket('Timestamp is not a hex integer: %s' % e) + try: + userid, data = ticket[digest_size + 8:].split('!', 1) + except ValueError: + raise BadTicket('userid is not followed by !') + userid = url_unquote(userid) + if '!' in data: + tokens, user_data = data.split('!', 1) + else: # pragma: no cover (never generated) + # @@: Is this the right order? + tokens = '' + user_data = data + + expected = calculate_digest(ip, timestamp, secret, + userid, tokens, user_data, hashalg) + + # Avoid timing attacks (see + # http://seb.dbzteam.org/crypto/python-oauth-timing-hmac.pdf) + if strings_differ(expected, digest): + raise BadTicket('Digest signature is not correct', + expected=(expected, digest)) + + tokens = tokens.split(',') + + return (timestamp, userid, tokens, user_data) + +# this function licensed under the MIT license (stolen from Paste) +def calculate_digest(ip, timestamp, secret, userid, tokens, user_data, + hashalg='md5'): + secret = bytes_(secret, 'utf-8') + userid = bytes_(userid, 'utf-8') + tokens = bytes_(tokens, 'utf-8') + user_data = bytes_(user_data, 'utf-8') + hash_obj = hashlib.new(hashalg) + + # Check to see if this is an IPv6 address + if ':' in ip: + ip_timestamp = ip + str(int(timestamp)) + ip_timestamp = bytes_(ip_timestamp) + else: + # encode_ip_timestamp not required, left in for backwards compatibility + ip_timestamp = encode_ip_timestamp(ip, timestamp) + + hash_obj.update(ip_timestamp + secret + userid + b'\0' + + tokens + b'\0' + user_data) + digest = hash_obj.hexdigest() + hash_obj2 = hashlib.new(hashalg) + hash_obj2.update(bytes_(digest) + secret) + return hash_obj2.hexdigest() + +# this function licensed under the MIT license (stolen from Paste) +def encode_ip_timestamp(ip, timestamp): + ip_chars = ''.join(map(chr, map(int, ip.split('.')))) + t = int(timestamp) + ts = ((t & 0xff000000) >> 24, + (t & 0xff0000) >> 16, + (t & 0xff00) >> 8, + t & 0xff) + ts_chars = ''.join(map(chr, ts)) + return bytes_(ip_chars + ts_chars) + +class AuthTktCookieHelper(object): + """ + A helper class for use in third-party authentication policy + implementations. See + :class:`pyramid.authentication.AuthTktAuthenticationPolicy` for the + meanings of the constructor arguments. + """ + parse_ticket = staticmethod(parse_ticket) # for tests + AuthTicket = AuthTicket # for tests + BadTicket = BadTicket # for tests + now = None # for tests + + userid_type_decoders = { + 'int':int, + 'unicode':lambda x: utf_8_decode(x)[0], # bw compat for old cookies + 'b64unicode': lambda x: utf_8_decode(b64decode(x))[0], + 'b64str': lambda x: b64decode(x), + } + + userid_type_encoders = { + int: ('int', str), + long: ('int', str), + text_type: ('b64unicode', lambda x: b64encode(utf_8_encode(x)[0])), + binary_type: ('b64str', lambda x: b64encode(x)), + } + + def __init__(self, + secret, + cookie_name='auth_tkt', + secure=False, + include_ip=False, + timeout=None, + reissue_time=None, + max_age=None, + http_only=False, + path="/", + wild_domain=True, + hashalg='md5', + parent_domain=False, + domain=None, + samesite='Lax', + ): + + serializer = SimpleSerializer() + + self.cookie_profile = CookieProfile( + cookie_name=cookie_name, + secure=secure, + max_age=max_age, + httponly=http_only, + path=path, + serializer=serializer, + samesite=samesite, + ) + + self.secret = secret + self.cookie_name = cookie_name + self.secure = secure + self.include_ip = include_ip + self.timeout = timeout if timeout is None else int(timeout) + self.reissue_time = reissue_time if reissue_time is None else int(reissue_time) + self.max_age = max_age if max_age is None else int(max_age) + self.wild_domain = wild_domain + self.parent_domain = parent_domain + self.domain = domain + self.hashalg = hashalg + + def _get_cookies(self, request, value, max_age=None): + cur_domain = request.domain + + domains = [] + if self.domain: + domains.append(self.domain) + else: + if self.parent_domain and cur_domain.count('.') > 1: + domains.append('.' + cur_domain.split('.', 1)[1]) + else: + domains.append(None) + domains.append(cur_domain) + if self.wild_domain: + domains.append('.' + cur_domain) + + profile = self.cookie_profile(request) + + kw = {} + kw['domains'] = domains + if max_age is not None: + kw['max_age'] = max_age + + headers = profile.get_headers(value, **kw) + return headers + + def identify(self, request): + """ Return a dictionary with authentication information, or ``None`` + if no valid auth_tkt is attached to ``request``""" + environ = request.environ + cookie = request.cookies.get(self.cookie_name) + + if cookie is None: + return None + + if self.include_ip: + remote_addr = environ['REMOTE_ADDR'] + else: + remote_addr = '0.0.0.0' + + try: + timestamp, userid, tokens, user_data = self.parse_ticket( + self.secret, cookie, remote_addr, self.hashalg) + except self.BadTicket: + return None + + now = self.now # service tests + + if now is None: + now = time_mod.time() + + if self.timeout and ( (timestamp + self.timeout) < now ): + # the auth_tkt data has expired + return None + + userid_typename = 'userid_type:' + user_data_info = user_data.split('|') + for datum in filter(None, user_data_info): + if datum.startswith(userid_typename): + userid_type = datum[len(userid_typename):] + decoder = self.userid_type_decoders.get(userid_type) + if decoder: + userid = decoder(userid) + + reissue = self.reissue_time is not None + + if reissue and not hasattr(request, '_authtkt_reissued'): + if ( (now - timestamp) > self.reissue_time ): + # See https://github.com/Pylons/pyramid/issues#issue/108 + tokens = list(filter(None, tokens)) + headers = self.remember(request, userid, max_age=self.max_age, + tokens=tokens) + def reissue_authtkt(request, response): + if not hasattr(request, '_authtkt_reissue_revoked'): + for k, v in headers: + response.headerlist.append((k, v)) + request.add_response_callback(reissue_authtkt) + request._authtkt_reissued = True + + environ['REMOTE_USER_TOKENS'] = tokens + environ['REMOTE_USER_DATA'] = user_data + environ['AUTH_TYPE'] = 'cookie' + + identity = {} + identity['timestamp'] = timestamp + identity['userid'] = userid + identity['tokens'] = tokens + identity['userdata'] = user_data + return identity + + def forget(self, request): + """ Return a set of expires Set-Cookie headers, which will destroy + any existing auth_tkt cookie when attached to a response""" + request._authtkt_reissue_revoked = True + return self._get_cookies(request, None) + + def remember(self, request, userid, max_age=None, tokens=()): + """ Return a set of Set-Cookie headers; when set into a response, + these headers will represent a valid authentication ticket. + + ``max_age`` + The max age of the auth_tkt cookie, in seconds. When this value is + set, the cookie's ``Max-Age`` and ``Expires`` settings will be set, + allowing the auth_tkt cookie to last between browser sessions. If + this value is ``None``, the ``max_age`` value provided to the + helper itself will be used as the ``max_age`` value. Default: + ``None``. + + ``tokens`` + A sequence of strings that will be placed into the auth_tkt tokens + field. Each string in the sequence must be of the Python ``str`` + type and must match the regex ``^[A-Za-z][A-Za-z0-9+_-]*$``. + Tokens are available in the returned identity when an auth_tkt is + found in the request and unpacked. Default: ``()``. + """ + max_age = self.max_age if max_age is None else int(max_age) + + environ = request.environ + + if self.include_ip: + remote_addr = environ['REMOTE_ADDR'] + else: + remote_addr = '0.0.0.0' + + user_data = '' + + encoding_data = self.userid_type_encoders.get(type(userid)) + + if encoding_data: + encoding, encoder = encoding_data + else: + warnings.warn( + "userid is of type {}, and is not supported by the " + "AuthTktAuthenticationPolicy. Explicitly converting to string " + "and storing as base64. Subsequent requests will receive a " + "string as the userid, it will not be decoded back to the type " + "provided.".format(type(userid)), RuntimeWarning + ) + encoding, encoder = self.userid_type_encoders.get(text_type) + userid = str(userid) + + userid = encoder(userid) + user_data = 'userid_type:%s' % encoding + + new_tokens = [] + for token in tokens: + if isinstance(token, text_type): + try: + token = ascii_native_(token) + except UnicodeEncodeError: + raise ValueError("Invalid token %r" % (token,)) + if not (isinstance(token, str) and VALID_TOKEN.match(token)): + raise ValueError("Invalid token %r" % (token,)) + new_tokens.append(token) + tokens = tuple(new_tokens) + + if hasattr(request, '_authtkt_reissued'): + request._authtkt_reissue_revoked = True + + ticket = self.AuthTicket( + self.secret, + userid, + remote_addr, + tokens=tokens, + user_data=user_data, + cookie_name=self.cookie_name, + secure=self.secure, + hashalg=self.hashalg + ) + + cookie_value = ticket.cookie_value() + return self._get_cookies(request, cookie_value, max_age) + +@implementer(IAuthenticationPolicy) +class SessionAuthenticationPolicy(CallbackAuthenticationPolicy): + """ A :app:`Pyramid` authentication policy which gets its data from the + configured :term:`session`. For this authentication policy to work, you + will have to follow the instructions in the :ref:`sessions_chapter` to + configure a :term:`session factory`. + + Constructor Arguments + + ``prefix`` + + A prefix used when storing the authentication parameters in the + session. Defaults to 'auth.'. Optional. + + ``callback`` + + Default: ``None``. A callback passed the userid and the + request, expected to return ``None`` if the userid doesn't + exist or a sequence of principal identifiers (possibly empty) if + the user does exist. If ``callback`` is ``None``, the userid + will be assumed to exist with no principals. Optional. + + ``debug`` + + Default: ``False``. If ``debug`` is ``True``, log messages to the + Pyramid debug logger about the results of various authentication + steps. The output from debugging is useful for reporting to maillist + or IRC channels when asking for support. + + """ + + def __init__(self, prefix='auth.', callback=None, debug=False): + self.callback = callback + self.prefix = prefix or '' + self.userid_key = prefix + 'userid' + self.debug = debug + + def remember(self, request, userid, **kw): + """ Store a userid in the session.""" + request.session[self.userid_key] = userid + return [] + + def forget(self, request): + """ Remove the stored userid from the session.""" + if self.userid_key in request.session: + del request.session[self.userid_key] + return [] + + def unauthenticated_userid(self, request): + return request.session.get(self.userid_key) + + +@implementer(IAuthenticationPolicy) +class BasicAuthAuthenticationPolicy(CallbackAuthenticationPolicy): + """ A :app:`Pyramid` authentication policy which uses HTTP standard basic + authentication protocol to authenticate users. To use this policy you will + need to provide a callback which checks the supplied user credentials + against your source of login data. + + Constructor Arguments + + ``check`` + + A callback function passed a username, password and request, in that + order as positional arguments. Expected to return ``None`` if the + userid doesn't exist or a sequence of principal identifiers (possibly + empty) if the user does exist. + + ``realm`` + + Default: ``"Realm"``. The Basic Auth Realm string. Usually displayed to + the user by the browser in the login dialog. + + ``debug`` + + Default: ``False``. If ``debug`` is ``True``, log messages to the + Pyramid debug logger about the results of various authentication + steps. The output from debugging is useful for reporting to maillist + or IRC channels when asking for support. + + **Issuing a challenge** + + Regular browsers will not send username/password credentials unless they + first receive a challenge from the server. The following recipe will + register a view that will send a Basic Auth challenge to the user whenever + there is an attempt to call a view which results in a Forbidden response:: + + from pyramid.httpexceptions import HTTPUnauthorized + from pyramid.security import forget + from pyramid.view import forbidden_view_config + + @forbidden_view_config() + def forbidden_view(request): + if request.authenticated_userid is None: + response = HTTPUnauthorized() + response.headers.update(forget(request)) + return response + return HTTPForbidden() + """ + def __init__(self, check, realm='Realm', debug=False): + self.check = check + self.realm = realm + self.debug = debug + + def unauthenticated_userid(self, request): + """ The userid parsed from the ``Authorization`` request header.""" + credentials = extract_http_basic_credentials(request) + if credentials: + return credentials.username + + def remember(self, request, userid, **kw): + """ A no-op. Basic authentication does not provide a protocol for + remembering the user. Credentials are sent on every request. + + """ + return [] + + def forget(self, request): + """ Returns challenge headers. This should be attached to a response + to indicate that credentials are required.""" + return [('WWW-Authenticate', 'Basic realm="%s"' % self.realm)] + + def callback(self, username, request): + # Username arg is ignored. Unfortunately + # extract_http_basic_credentials winds up getting called twice when + # authenticated_userid is called. Avoiding that, however, + # winds up duplicating logic from the superclass. + credentials = extract_http_basic_credentials(request) + if credentials: + username, password = credentials + return self.check(username, password, request) + + +HTTPBasicCredentials = namedtuple( + 'HTTPBasicCredentials', ['username', 'password']) + + +def extract_http_basic_credentials(request): + """ A helper function for extraction of HTTP Basic credentials + from a given :term:`request`. + + Returns a :class:`.HTTPBasicCredentials` 2-tuple with ``username`` and + ``password`` attributes or ``None`` if no credentials could be found. + + """ + authorization = request.headers.get('Authorization') + if not authorization: + return None + + try: + authmeth, auth = authorization.split(' ', 1) + except ValueError: # not enough values to unpack + return None + + if authmeth.lower() != 'basic': + return None + + try: + authbytes = b64decode(auth.strip()) + except (TypeError, binascii.Error): # can't decode + return None + + # try utf-8 first, then latin-1; see discussion in + # https://github.com/Pylons/pyramid/issues/898 + try: + auth = authbytes.decode('utf-8') + except UnicodeDecodeError: + auth = authbytes.decode('latin-1') + + try: + username, password = auth.split(':', 1) + except ValueError: # not enough values to unpack + return None + + return HTTPBasicCredentials(username, password) diff --git a/src/pyramid/authorization.py b/src/pyramid/authorization.py new file mode 100644 index 000000000..4845762ef --- /dev/null +++ b/src/pyramid/authorization.py @@ -0,0 +1,146 @@ +from zope.interface import implementer + +from pyramid.interfaces import IAuthorizationPolicy + +from pyramid.location import lineage + +from pyramid.compat import is_nonstr_iter + +from pyramid.security import ( + ACLAllowed, + ACLDenied, + Allow, + Deny, + Everyone, + ) + +@implementer(IAuthorizationPolicy) +class ACLAuthorizationPolicy(object): + """ An :term:`authorization policy` which consults an :term:`ACL` + object attached to a :term:`context` to determine authorization + information about a :term:`principal` or multiple principals. + If the context is part of a :term:`lineage`, the context's parents + are consulted for ACL information too. The following is true + about this security policy. + + - When checking whether the 'current' user is permitted (via the + ``permits`` method), the security policy consults the + ``context`` for an ACL first. If no ACL exists on the context, + or one does exist but the ACL does not explicitly allow or deny + access for any of the effective principals, consult the + context's parent ACL, and so on, until the lineage is exhausted + or we determine that the policy permits or denies. + + During this processing, if any :data:`pyramid.security.Deny` + ACE is found matching any principal in ``principals``, stop + processing by returning an + :class:`pyramid.security.ACLDenied` instance (equals + ``False``) immediately. If any + :data:`pyramid.security.Allow` ACE is found matching any + principal, stop processing by returning an + :class:`pyramid.security.ACLAllowed` instance (equals + ``True``) immediately. If we exhaust the context's + :term:`lineage`, and no ACE has explicitly permitted or denied + access, return an instance of + :class:`pyramid.security.ACLDenied` (equals ``False``). + + - When computing principals allowed by a permission via the + :func:`pyramid.security.principals_allowed_by_permission` + method, we compute the set of principals that are explicitly + granted the ``permission`` in the provided ``context``. We do + this by walking 'up' the object graph *from the root* to the + context. During this walking process, if we find an explicit + :data:`pyramid.security.Allow` ACE for a principal that + matches the ``permission``, the principal is included in the + allow list. However, if later in the walking process that + principal is mentioned in any :data:`pyramid.security.Deny` + ACE for the permission, the principal is removed from the allow + list. If a :data:`pyramid.security.Deny` to the principal + :data:`pyramid.security.Everyone` is encountered during the + walking process that matches the ``permission``, the allow list + is cleared for all principals encountered in previous ACLs. The + walking process ends after we've processed the any ACL directly + attached to ``context``; a set of principals is returned. + + Objects of this class implement the + :class:`pyramid.interfaces.IAuthorizationPolicy` interface. + """ + + def permits(self, context, principals, permission): + """ Return an instance of + :class:`pyramid.security.ACLAllowed` instance if the policy + permits access, return an instance of + :class:`pyramid.security.ACLDenied` if not.""" + + acl = '' + + for location in lineage(context): + try: + acl = location.__acl__ + except AttributeError: + continue + + if acl and callable(acl): + acl = acl() + + for ace in acl: + ace_action, ace_principal, ace_permissions = ace + if ace_principal in principals: + if not is_nonstr_iter(ace_permissions): + ace_permissions = [ace_permissions] + if permission in ace_permissions: + if ace_action == Allow: + return ACLAllowed(ace, acl, permission, + principals, location) + else: + return ACLDenied(ace, acl, permission, + principals, location) + + # default deny (if no ACL in lineage at all, or if none of the + # principals were mentioned in any ACE we found) + return ACLDenied( + '', + acl, + permission, + principals, + context) + + def principals_allowed_by_permission(self, context, permission): + """ Return the set of principals explicitly granted the + permission named ``permission`` according to the ACL directly + attached to the ``context`` as well as inherited ACLs based on + the :term:`lineage`.""" + allowed = set() + + for location in reversed(list(lineage(context))): + # NB: we're walking *up* the object graph from the root + try: + acl = location.__acl__ + except AttributeError: + continue + + allowed_here = set() + denied_here = set() + + if acl and callable(acl): + acl = acl() + + for ace_action, ace_principal, ace_permissions in acl: + if not is_nonstr_iter(ace_permissions): + ace_permissions = [ace_permissions] + if (ace_action == Allow) and (permission in ace_permissions): + if ace_principal not in denied_here: + allowed_here.add(ace_principal) + if (ace_action == Deny) and (permission in ace_permissions): + denied_here.add(ace_principal) + if ace_principal == Everyone: + # clear the entire allowed set, as we've hit a + # deny of Everyone ala (Deny, Everyone, ALL) + allowed = set() + break + elif ace_principal in allowed: + allowed.remove(ace_principal) + + allowed.update(allowed_here) + + return allowed diff --git a/src/pyramid/compat.py b/src/pyramid/compat.py new file mode 100644 index 000000000..a7f9c1287 --- /dev/null +++ b/src/pyramid/compat.py @@ -0,0 +1,281 @@ +import inspect +import platform +import sys +import types + +WIN = platform.system() == 'Windows' + +try: # pragma: no cover + import __pypy__ + PYPY = True +except: # pragma: no cover + __pypy__ = None + PYPY = False + +try: + import cPickle as pickle +except ImportError: # pragma: no cover + import pickle + +try: + from functools import lru_cache +except ImportError: + from repoze.lru import lru_cache + +# PY3 is left as bw-compat but PY2 should be used for most checks. +PY2 = sys.version_info[0] == 2 +PY3 = sys.version_info[0] == 3 + +if PY2: + string_types = basestring, + integer_types = (int, long) + class_types = (type, types.ClassType) + text_type = unicode + binary_type = str + long = long +else: + string_types = str, + integer_types = int, + class_types = type, + text_type = str + binary_type = bytes + long = int + +def text_(s, encoding='latin-1', errors='strict'): + """ If ``s`` is an instance of ``binary_type``, return + ``s.decode(encoding, errors)``, otherwise return ``s``""" + if isinstance(s, binary_type): + return s.decode(encoding, errors) + return s + +def bytes_(s, encoding='latin-1', errors='strict'): + """ If ``s`` is an instance of ``text_type``, return + ``s.encode(encoding, errors)``, otherwise return ``s``""" + if isinstance(s, text_type): + return s.encode(encoding, errors) + return s + +if PY2: + def ascii_native_(s): + if isinstance(s, text_type): + s = s.encode('ascii') + return str(s) +else: + def ascii_native_(s): + if isinstance(s, text_type): + s = s.encode('ascii') + return str(s, 'ascii', 'strict') + +ascii_native_.__doc__ = """ +Python 3: If ``s`` is an instance of ``text_type``, return +``s.encode('ascii')``, otherwise return ``str(s, 'ascii', 'strict')`` + +Python 2: If ``s`` is an instance of ``text_type``, return +``s.encode('ascii')``, otherwise return ``str(s)`` +""" + + +if PY2: + def native_(s, encoding='latin-1', errors='strict'): + """ If ``s`` is an instance of ``text_type``, return + ``s.encode(encoding, errors)``, otherwise return ``str(s)``""" + if isinstance(s, text_type): + return s.encode(encoding, errors) + return str(s) +else: + def native_(s, encoding='latin-1', errors='strict'): + """ If ``s`` is an instance of ``text_type``, return + ``s``, otherwise return ``str(s, encoding, errors)``""" + if isinstance(s, text_type): + return s + return str(s, encoding, errors) + +native_.__doc__ = """ +Python 3: If ``s`` is an instance of ``text_type``, return ``s``, otherwise +return ``str(s, encoding, errors)`` + +Python 2: If ``s`` is an instance of ``text_type``, return +``s.encode(encoding, errors)``, otherwise return ``str(s)`` +""" + +if PY2: + import urlparse + from urllib import quote as url_quote + from urllib import quote_plus as url_quote_plus + from urllib import unquote as url_unquote + from urllib import urlencode as url_encode + from urllib2 import urlopen as url_open + + def url_unquote_text(v, encoding='utf-8', errors='replace'): # pragma: no cover + v = url_unquote(v) + return v.decode(encoding, errors) + + def url_unquote_native(v, encoding='utf-8', errors='replace'): # pragma: no cover + return native_(url_unquote_text(v, encoding, errors)) +else: + from urllib import parse + urlparse = parse + from urllib.parse import quote as url_quote + from urllib.parse import quote_plus as url_quote_plus + from urllib.parse import unquote as url_unquote + from urllib.parse import urlencode as url_encode + from urllib.request import urlopen as url_open + url_unquote_text = url_unquote + url_unquote_native = url_unquote + + +if PY2: # pragma: no cover + def exec_(code, globs=None, locs=None): + """Execute code in a namespace.""" + if globs is None: + frame = sys._getframe(1) + globs = frame.f_globals + if locs is None: + locs = frame.f_locals + del frame + elif locs is None: + locs = globs + exec("""exec code in globs, locs""") + + exec_("""def reraise(tp, value, tb=None): + raise tp, value, tb +""") + +else: # pragma: no cover + import builtins + exec_ = getattr(builtins, "exec") + + def reraise(tp, value, tb=None): + if value is None: + value = tp + if value.__traceback__ is not tb: + raise value.with_traceback(tb) + raise value + + del builtins + + +if PY2: # pragma: no cover + def iteritems_(d): + return d.iteritems() + + def itervalues_(d): + return d.itervalues() + + def iterkeys_(d): + return d.iterkeys() +else: # pragma: no cover + def iteritems_(d): + return d.items() + + def itervalues_(d): + return d.values() + + def iterkeys_(d): + return d.keys() + + +if PY2: + map_ = map +else: + def map_(*arg): + return list(map(*arg)) + +if PY2: + def is_nonstr_iter(v): + return hasattr(v, '__iter__') +else: + def is_nonstr_iter(v): + if isinstance(v, str): + return False + return hasattr(v, '__iter__') + +if PY2: + im_func = 'im_func' + im_self = 'im_self' +else: + im_func = '__func__' + im_self = '__self__' + +try: + import configparser +except ImportError: + import ConfigParser as configparser + +try: + from http.cookies import SimpleCookie +except ImportError: + from Cookie import SimpleCookie + +if PY2: + from cgi import escape +else: + from html import escape + +if PY2: + input_ = raw_input +else: + input_ = input + +if PY2: + from io import BytesIO as NativeIO +else: + from io import StringIO as NativeIO + +# "json" is not an API; it's here to support older pyramid_debugtoolbar +# versions which attempt to import it +import json + +if PY2: + def decode_path_info(path): + return path.decode('utf-8') +else: + # see PEP 3333 for why we encode WSGI PATH_INFO to latin-1 before + # decoding it to utf-8 + def decode_path_info(path): + return path.encode('latin-1').decode('utf-8') + +if PY2: + from urlparse import unquote as unquote_to_bytes + + def unquote_bytes_to_wsgi(bytestring): + return unquote_to_bytes(bytestring) +else: + # see PEP 3333 for why we decode the path to latin-1 + from urllib.parse import unquote_to_bytes + + def unquote_bytes_to_wsgi(bytestring): + return unquote_to_bytes(bytestring).decode('latin-1') + + +def is_bound_method(ob): + return inspect.ismethod(ob) and getattr(ob, im_self, None) is not None + +# support annotations and keyword-only arguments in PY3 +if PY2: + from inspect import getargspec +else: + from inspect import getfullargspec as getargspec + +if PY2: + from itertools import izip_longest as zip_longest +else: + from itertools import zip_longest + +def is_unbound_method(fn): + """ + This consistently verifies that the callable is bound to a + class. + """ + is_bound = is_bound_method(fn) + + if not is_bound and inspect.isroutine(fn): + spec = getargspec(fn) + has_self = len(spec.args) > 0 and spec.args[0] == 'self' + + if PY2 and inspect.ismethod(fn): + return True + elif inspect.isfunction(fn) and has_self: + return True + + return False diff --git a/src/pyramid/config/__init__.py b/src/pyramid/config/__init__.py new file mode 100644 index 000000000..2f4e133f0 --- /dev/null +++ b/src/pyramid/config/__init__.py @@ -0,0 +1,1409 @@ +import inspect +import itertools +import logging +import operator +import os +import sys +import threading +import venusian + +from webob.exc import WSGIHTTPException as WebobWSGIHTTPException + +from pyramid.interfaces import ( + IDebugLogger, + IExceptionResponse, + IPredicateList, + PHASE0_CONFIG, + PHASE1_CONFIG, + PHASE2_CONFIG, + PHASE3_CONFIG, + ) + +from pyramid.asset import resolve_asset_spec + +from pyramid.authorization import ACLAuthorizationPolicy + +from pyramid.compat import ( + text_, + reraise, + string_types, + ) + +from pyramid.events import ApplicationCreated + +from pyramid.exceptions import ( + ConfigurationConflictError, + ConfigurationError, + ConfigurationExecutionError, + ) + +from pyramid.httpexceptions import default_exceptionresponse_view + +from pyramid.path import ( + caller_package, + package_of, + ) + +from pyramid.registry import ( + Introspectable, + Introspector, + Registry, + undefer, + ) + +from pyramid.router import Router + +from pyramid.settings import aslist + +from pyramid.threadlocal import manager + +from pyramid.util import ( + WeakOrderedSet, + object_description, + ) + +from pyramid.config.util import ( + ActionInfo, + PredicateList, + action_method, + not_, +) + +from pyramid.config.adapters import AdaptersConfiguratorMixin +from pyramid.config.assets import AssetsConfiguratorMixin +from pyramid.config.factories import FactoriesConfiguratorMixin +from pyramid.config.i18n import I18NConfiguratorMixin +from pyramid.config.rendering import RenderingConfiguratorMixin +from pyramid.config.routes import RoutesConfiguratorMixin +from pyramid.config.security import SecurityConfiguratorMixin +from pyramid.config.settings import SettingsConfiguratorMixin +from pyramid.config.testing import TestingConfiguratorMixin +from pyramid.config.tweens import TweensConfiguratorMixin +from pyramid.config.views import ViewsConfiguratorMixin +from pyramid.config.zca import ZCAConfiguratorMixin + +from pyramid.path import DottedNameResolver + +empty = text_('') +_marker = object() + +not_ = not_ # api + +PHASE0_CONFIG = PHASE0_CONFIG # api +PHASE1_CONFIG = PHASE1_CONFIG # api +PHASE2_CONFIG = PHASE2_CONFIG # api +PHASE3_CONFIG = PHASE3_CONFIG # api + +class Configurator( + TestingConfiguratorMixin, + TweensConfiguratorMixin, + SecurityConfiguratorMixin, + ViewsConfiguratorMixin, + RoutesConfiguratorMixin, + ZCAConfiguratorMixin, + I18NConfiguratorMixin, + RenderingConfiguratorMixin, + AssetsConfiguratorMixin, + SettingsConfiguratorMixin, + FactoriesConfiguratorMixin, + AdaptersConfiguratorMixin, + ): + """ + A Configurator is used to configure a :app:`Pyramid` + :term:`application registry`. + + The Configurator lifecycle can be managed by using a context manager to + automatically handle calling :meth:`pyramid.config.Configurator.begin` and + :meth:`pyramid.config.Configurator.end` as well as + :meth:`pyramid.config.Configurator.commit`. + + .. code-block:: python + + with Configurator(settings=settings) as config: + config.add_route('home', '/') + app = config.make_wsgi_app() + + If the ``registry`` argument is not ``None``, it must + be an instance of the :class:`pyramid.registry.Registry` class + representing the registry to configure. If ``registry`` is ``None``, the + configurator will create a :class:`pyramid.registry.Registry` instance + itself; it will also perform some default configuration that would not + otherwise be done. After its construction, the configurator may be used + to add further configuration to the registry. + + .. warning:: If ``registry`` is assigned the above-mentioned class + instance, all other constructor arguments are ignored, + with the exception of ``package``. + + If the ``package`` argument is passed, it must be a reference to a Python + :term:`package` (e.g. ``sys.modules['thepackage']``) or a :term:`dotted + Python name` to the same. This value is used as a basis to convert + relative paths passed to various configuration methods, such as methods + which accept a ``renderer`` argument, into absolute paths. If ``None`` + is passed (the default), the package is assumed to be the Python package + in which the *caller* of the ``Configurator`` constructor lives. + + If the ``root_package`` is passed, it will propagate through the + configuration hierarchy as a way for included packages to locate + resources relative to the package in which the main ``Configurator`` was + created. If ``None`` is passed (the default), the ``root_package`` will + be derived from the ``package`` argument. The ``package`` attribute is + always pointing at the package being included when using :meth:`.include`, + whereas the ``root_package`` does not change. + + If the ``settings`` argument is passed, it should be a Python dictionary + representing the :term:`deployment settings` for this application. These + are later retrievable using the + :attr:`pyramid.registry.Registry.settings` attribute (aka + ``request.registry.settings``). + + If the ``root_factory`` argument is passed, it should be an object + representing the default :term:`root factory` for your application or a + :term:`dotted Python name` to the same. If it is ``None``, a default + root factory will be used. + + If ``authentication_policy`` is passed, it should be an instance + of an :term:`authentication policy` or a :term:`dotted Python + name` to the same. + + If ``authorization_policy`` is passed, it should be an instance of + an :term:`authorization policy` or a :term:`dotted Python name` to + the same. + + .. note:: A ``ConfigurationError`` will be raised when an + authorization policy is supplied without also supplying an + authentication policy (authorization requires authentication). + + If ``renderers`` is ``None`` (the default), a default set of + :term:`renderer` factories is used. Else, it should be a list of + tuples representing a set of renderer factories which should be + configured into this application, and each tuple representing a set of + positional values that should be passed to + :meth:`pyramid.config.Configurator.add_renderer`. + + If ``debug_logger`` is not passed, a default debug logger that logs to a + logger will be used (the logger name will be the package name of the + *caller* of this configurator). If it is passed, it should be an + instance of the :class:`logging.Logger` (PEP 282) standard library class + or a Python logger name. The debug logger is used by :app:`Pyramid` + itself to log warnings and authorization debugging information. + + If ``locale_negotiator`` is passed, it should be a :term:`locale + negotiator` implementation or a :term:`dotted Python name` to + same. See :ref:`custom_locale_negotiator`. + + If ``request_factory`` is passed, it should be a :term:`request + factory` implementation or a :term:`dotted Python name` to the same. + See :ref:`changing_the_request_factory`. By default it is ``None``, + which means use the default request factory. + + If ``response_factory`` is passed, it should be a :term:`response + factory` implementation or a :term:`dotted Python name` to the same. + See :ref:`changing_the_response_factory`. By default it is ``None``, + which means use the default response factory. + + If ``default_permission`` is passed, it should be a + :term:`permission` string to be used as the default permission for + all view configuration registrations performed against this + Configurator. An example of a permission string:``'view'``. + Adding a default permission makes it unnecessary to protect each + view configuration with an explicit permission, unless your + application policy requires some exception for a particular view. + By default, ``default_permission`` is ``None``, meaning that view + configurations which do not explicitly declare a permission will + always be executable by entirely anonymous users (any + authorization policy in effect is ignored). + + .. seealso:: + + See also :ref:`setting_a_default_permission`. + + If ``session_factory`` is passed, it should be an object which + implements the :term:`session factory` interface. If a nondefault + value is passed, the ``session_factory`` will be used to create a + session object when ``request.session`` is accessed. Note that + the same outcome can be achieved by calling + :meth:`pyramid.config.Configurator.set_session_factory`. By + default, this argument is ``None``, indicating that no session + factory will be configured (and thus accessing ``request.session`` + will throw an error) unless ``set_session_factory`` is called later + during configuration. + + If ``autocommit`` is ``True``, every method called on the configurator + will cause an immediate action, and no configuration conflict detection + will be used. If ``autocommit`` is ``False``, most methods of the + configurator will defer their action until + :meth:`pyramid.config.Configurator.commit` is called. When + :meth:`pyramid.config.Configurator.commit` is called, the actions implied + by the called methods will be checked for configuration conflicts unless + ``autocommit`` is ``True``. If a conflict is detected, a + ``ConfigurationConflictError`` will be raised. Calling + :meth:`pyramid.config.Configurator.make_wsgi_app` always implies a final + commit. + + If ``default_view_mapper`` is passed, it will be used as the default + :term:`view mapper` factory for view configurations that don't otherwise + specify one (see :class:`pyramid.interfaces.IViewMapperFactory`). If + ``default_view_mapper`` is not passed, a superdefault view mapper will be + used. + + If ``exceptionresponse_view`` is passed, it must be a :term:`view + callable` or ``None``. If it is a view callable, it will be used as an + exception view callable when an :term:`exception response` is raised. If + ``exceptionresponse_view`` is ``None``, no exception response view will + be registered, and all raised exception responses will be bubbled up to + Pyramid's caller. By + default, the ``pyramid.httpexceptions.default_exceptionresponse_view`` + function is used as the ``exceptionresponse_view``. + + If ``route_prefix`` is passed, all routes added with + :meth:`pyramid.config.Configurator.add_route` will have the specified path + prepended to their pattern. + + If ``introspection`` is passed, it must be a boolean value. If it's + ``True``, introspection values during actions will be kept for use + for tools like the debug toolbar. If it's ``False``, introspection + values provided by registrations will be ignored. By default, it is + ``True``. + + .. versionadded:: 1.1 + The ``exceptionresponse_view`` argument. + + .. versionadded:: 1.2 + The ``route_prefix`` argument. + + .. versionadded:: 1.3 + The ``introspection`` argument. + + .. versionadded:: 1.6 + The ``root_package`` argument. + The ``response_factory`` argument. + + .. versionadded:: 1.9 + The ability to use the configurator as a context manager with the + ``with``-statement to make threadlocal configuration available for + further configuration with an implicit commit. + """ + manager = manager # for testing injection + venusian = venusian # for testing injection + _ainfo = None + basepath = None + includepath = () + info = '' + object_description = staticmethod(object_description) + introspectable = Introspectable + inspect = inspect + + def __init__(self, + registry=None, + package=None, + settings=None, + root_factory=None, + authentication_policy=None, + authorization_policy=None, + renderers=None, + debug_logger=None, + locale_negotiator=None, + request_factory=None, + response_factory=None, + default_permission=None, + session_factory=None, + default_view_mapper=None, + autocommit=False, + exceptionresponse_view=default_exceptionresponse_view, + route_prefix=None, + introspection=True, + root_package=None, + ): + if package is None: + package = caller_package() + if root_package is None: + root_package = package + name_resolver = DottedNameResolver(package) + self.name_resolver = name_resolver + self.package_name = name_resolver.get_package_name() + self.package = name_resolver.get_package() + self.root_package = root_package + self.registry = registry + self.autocommit = autocommit + self.route_prefix = route_prefix + self.introspection = introspection + if registry is None: + registry = Registry(self.package_name) + self.registry = registry + self.setup_registry( + settings=settings, + root_factory=root_factory, + authentication_policy=authentication_policy, + authorization_policy=authorization_policy, + renderers=renderers, + debug_logger=debug_logger, + locale_negotiator=locale_negotiator, + request_factory=request_factory, + response_factory=response_factory, + default_permission=default_permission, + session_factory=session_factory, + default_view_mapper=default_view_mapper, + exceptionresponse_view=exceptionresponse_view, + ) + + def setup_registry(self, + settings=None, + root_factory=None, + authentication_policy=None, + authorization_policy=None, + renderers=None, + debug_logger=None, + locale_negotiator=None, + request_factory=None, + response_factory=None, + default_permission=None, + session_factory=None, + default_view_mapper=None, + exceptionresponse_view=default_exceptionresponse_view, + ): + """ When you pass a non-``None`` ``registry`` argument to the + :term:`Configurator` constructor, no initial setup is performed + against the registry. This is because the registry you pass in may + have already been initialized for use under :app:`Pyramid` via a + different configurator. However, in some circumstances (such as when + you want to use a global registry instead of a registry created as a + result of the Configurator constructor), or when you want to reset + the initial setup of a registry, you *do* want to explicitly + initialize the registry associated with a Configurator for use under + :app:`Pyramid`. Use ``setup_registry`` to do this initialization. + + ``setup_registry`` configures settings, a root factory, security + policies, renderers, a debug logger, a locale negotiator, and various + other settings using the configurator's current registry, as per the + descriptions in the Configurator constructor.""" + + registry = self.registry + + self._fix_registry() + + self._set_settings(settings) + + if isinstance(debug_logger, string_types): + debug_logger = logging.getLogger(debug_logger) + + if debug_logger is None: + debug_logger = logging.getLogger(self.package_name) + + registry.registerUtility(debug_logger, IDebugLogger) + + self.add_default_response_adapters() + self.add_default_renderers() + self.add_default_accept_view_order() + self.add_default_view_predicates() + self.add_default_view_derivers() + self.add_default_route_predicates() + self.add_default_tweens() + self.add_default_security() + + if exceptionresponse_view is not None: + exceptionresponse_view = self.maybe_dotted(exceptionresponse_view) + self.add_view(exceptionresponse_view, context=IExceptionResponse) + self.add_view(exceptionresponse_view,context=WebobWSGIHTTPException) + + # commit below because: + # + # - the default exceptionresponse_view requires the superdefault view + # mapper, so we need to configure it before adding default_view_mapper + # + # - superdefault renderers should be overrideable without requiring + # the user to commit before calling config.add_renderer + + self.commit() + + # self.commit() should not be called within this method after this + # point because the following registrations should be treated as + # analogues of methods called by the user after configurator + # construction. Rationale: user-supplied implementations should be + # preferred rather than add-on author implementations with the help of + # automatic conflict resolution. + + if authentication_policy and not authorization_policy: + authorization_policy = ACLAuthorizationPolicy() # default + + if authorization_policy: + self.set_authorization_policy(authorization_policy) + + if authentication_policy: + self.set_authentication_policy(authentication_policy) + + if default_view_mapper is not None: + self.set_view_mapper(default_view_mapper) + + if renderers: + for name, renderer in renderers: + self.add_renderer(name, renderer) + + if root_factory is not None: + self.set_root_factory(root_factory) + + if locale_negotiator: + self.set_locale_negotiator(locale_negotiator) + + if request_factory: + self.set_request_factory(request_factory) + + if response_factory: + self.set_response_factory(response_factory) + + if default_permission: + self.set_default_permission(default_permission) + + if session_factory is not None: + self.set_session_factory(session_factory) + + tweens = aslist(registry.settings.get('pyramid.tweens', [])) + for factory in tweens: + self._add_tween(factory, explicit=True) + + includes = aslist(registry.settings.get('pyramid.includes', [])) + for inc in includes: + self.include(inc) + + def _make_spec(self, path_or_spec): + package, filename = resolve_asset_spec(path_or_spec, self.package_name) + if package is None: + return filename # absolute filename + return '%s:%s' % (package, filename) + + def _fix_registry(self): + """ Fix up a ZCA component registry that is not a + pyramid.registry.Registry by adding analogues of ``has_listeners``, + ``notify``, ``queryAdapterOrSelf``, and ``registerSelfAdapter`` + through monkey-patching.""" + + _registry = self.registry + + if not hasattr(_registry, 'notify'): + def notify(*events): + [ _ for _ in _registry.subscribers(events, None) ] + _registry.notify = notify + + if not hasattr(_registry, 'has_listeners'): + _registry.has_listeners = True + + if not hasattr(_registry, 'queryAdapterOrSelf'): + def queryAdapterOrSelf(object, interface, default=None): + if not interface.providedBy(object): + return _registry.queryAdapter(object, interface, + default=default) + return object + _registry.queryAdapterOrSelf = queryAdapterOrSelf + + if not hasattr(_registry, 'registerSelfAdapter'): + def registerSelfAdapter(required=None, provided=None, + name=empty, info=empty, event=True): + return _registry.registerAdapter(lambda x: x, + required=required, + provided=provided, name=name, + info=info, event=event) + _registry.registerSelfAdapter = registerSelfAdapter + + if not hasattr(_registry, '_lock'): + _registry._lock = threading.Lock() + + if not hasattr(_registry, '_clear_view_lookup_cache'): + def _clear_view_lookup_cache(): + _registry._view_lookup_cache = {} + _registry._clear_view_lookup_cache = _clear_view_lookup_cache + + + # API + + def _get_introspector(self): + introspector = getattr(self.registry, 'introspector', _marker) + if introspector is _marker: + introspector = Introspector() + self._set_introspector(introspector) + return introspector + + def _set_introspector(self, introspector): + self.registry.introspector = introspector + + def _del_introspector(self): + del self.registry.introspector + + introspector = property( + _get_introspector, _set_introspector, _del_introspector + ) + + def get_predlist(self, name): + predlist = self.registry.queryUtility(IPredicateList, name=name) + if predlist is None: + predlist = PredicateList() + self.registry.registerUtility(predlist, IPredicateList, name=name) + return predlist + + + def _add_predicate(self, type, name, factory, weighs_more_than=None, + weighs_less_than=None): + factory = self.maybe_dotted(factory) + discriminator = ('%s option' % type, name) + intr = self.introspectable( + '%s predicates' % type, + discriminator, + '%s predicate named %s' % (type, name), + '%s predicate' % type) + intr['name'] = name + intr['factory'] = factory + intr['weighs_more_than'] = weighs_more_than + intr['weighs_less_than'] = weighs_less_than + def register(): + predlist = self.get_predlist(type) + predlist.add(name, factory, weighs_more_than=weighs_more_than, + weighs_less_than=weighs_less_than) + self.action(discriminator, register, introspectables=(intr,), + order=PHASE1_CONFIG) # must be registered early + + @property + def action_info(self): + info = self.info # usually a ZCML action (ParserInfo) if self.info + if not info: + # Try to provide more accurate info for conflict reports + if self._ainfo: + info = self._ainfo[0] + else: + info = ActionInfo(None, 0, '', '') + return info + + def action(self, discriminator, callable=None, args=(), kw=None, order=0, + introspectables=(), **extra): + """ Register an action which will be executed when + :meth:`pyramid.config.Configurator.commit` is called (or executed + immediately if ``autocommit`` is ``True``). + + .. warning:: This method is typically only used by :app:`Pyramid` + framework extension authors, not by :app:`Pyramid` application + developers. + + The ``discriminator`` uniquely identifies the action. It must be + given, but it can be ``None``, to indicate that the action never + conflicts. It must be a hashable value. + + The ``callable`` is a callable object which performs the task + associated with the action when the action is executed. It is + optional. + + ``args`` and ``kw`` are tuple and dict objects respectively, which + are passed to ``callable`` when this action is executed. Both are + optional. + + ``order`` is a grouping mechanism; an action with a lower order will + be executed before an action with a higher order (has no effect when + autocommit is ``True``). + + ``introspectables`` is a sequence of :term:`introspectable` objects + (or the empty sequence if no introspectable objects are associated + with this action). If this configurator's ``introspection`` + attribute is ``False``, these introspectables will be ignored. + + ``extra`` provides a facility for inserting extra keys and values + into an action dictionary. + """ + # catch nonhashable discriminators here; most unit tests use + # autocommit=False, which won't catch unhashable discriminators + assert hash(discriminator) + + if kw is None: + kw = {} + + autocommit = self.autocommit + action_info = self.action_info + + if not self.introspection: + # if we're not introspecting, ignore any introspectables passed + # to us + introspectables = () + + if autocommit: + # callables can depend on the side effects of resolving a + # deferred discriminator + self.begin() + try: + undefer(discriminator) + if callable is not None: + callable(*args, **kw) + for introspectable in introspectables: + introspectable.register(self.introspector, action_info) + finally: + self.end() + + else: + action = extra + action.update( + dict( + discriminator=discriminator, + callable=callable, + args=args, + kw=kw, + order=order, + info=action_info, + includepath=self.includepath, + introspectables=introspectables, + ) + ) + self.action_state.action(**action) + + def _get_action_state(self): + registry = self.registry + try: + state = registry.action_state + except AttributeError: + state = ActionState() + registry.action_state = state + return state + + def _set_action_state(self, state): + self.registry.action_state = state + + action_state = property(_get_action_state, _set_action_state) + + _ctx = action_state # bw compat + + def commit(self): + """ + Commit any pending configuration actions. If a configuration + conflict is detected in the pending configuration actions, this method + will raise a :exc:`ConfigurationConflictError`; within the traceback + of this error will be information about the source of the conflict, + usually including file names and line numbers of the cause of the + configuration conflicts. + + .. warning:: + You should think very carefully before manually invoking + ``commit()``. Especially not as part of any reusable configuration + methods. Normally it should only be done by an application author at + the end of configuration in order to override certain aspects of an + addon. + + """ + self.begin() + try: + self.action_state.execute_actions(introspector=self.introspector) + finally: + self.end() + self.action_state = ActionState() # old actions have been processed + + def include(self, callable, route_prefix=None): + """Include a configuration callable, to support imperative + application extensibility. + + .. warning:: In versions of :app:`Pyramid` prior to 1.2, this + function accepted ``*callables``, but this has been changed + to support only a single callable. + + A configuration callable should be a callable that accepts a single + argument named ``config``, which will be an instance of a + :term:`Configurator`. However, be warned that it will not be the same + configurator instance on which you call this method. The + code which runs as a result of calling the callable should invoke + methods on the configurator passed to it which add configuration + state. The return value of a callable will be ignored. + + Values allowed to be presented via the ``callable`` argument to + this method: any callable Python object or any :term:`dotted Python + name` which resolves to a callable Python object. It may also be a + Python :term:`module`, in which case, the module will be searched for + a callable named ``includeme``, which will be treated as the + configuration callable. + + For example, if the ``includeme`` function below lives in a module + named ``myapp.myconfig``: + + .. code-block:: python + :linenos: + + # myapp.myconfig module + + def my_view(request): + from pyramid.response import Response + return Response('OK') + + def includeme(config): + config.add_view(my_view) + + You might cause it to be included within your Pyramid application like + so: + + .. code-block:: python + :linenos: + + from pyramid.config import Configurator + + def main(global_config, **settings): + config = Configurator() + config.include('myapp.myconfig.includeme') + + Because the function is named ``includeme``, the function name can + also be omitted from the dotted name reference: + + .. code-block:: python + :linenos: + + from pyramid.config import Configurator + + def main(global_config, **settings): + config = Configurator() + config.include('myapp.myconfig') + + Included configuration statements will be overridden by local + configuration statements if an included callable causes a + configuration conflict by registering something with the same + configuration parameters. + + If the ``route_prefix`` is supplied, it must be a string. Any calls + to :meth:`pyramid.config.Configurator.add_route` within the included + callable will have their pattern prefixed with the value of + ``route_prefix``. This can be used to help mount a set of routes at a + different location than the included callable's author intended, while + still maintaining the same route names. For example: + + .. code-block:: python + :linenos: + + from pyramid.config import Configurator + + def included(config): + config.add_route('show_users', '/show') + + def main(global_config, **settings): + config = Configurator() + config.include(included, route_prefix='/users') + + In the above configuration, the ``show_users`` route will have an + effective route pattern of ``/users/show``, instead of ``/show`` + because the ``route_prefix`` argument will be prepended to the + pattern. + + .. versionadded:: 1.2 + The ``route_prefix`` parameter. + + .. versionchanged:: 1.9 + The included function is wrapped with a call to + :meth:`pyramid.config.Configurator.begin` and + :meth:`pyramid.config.Configurator.end` while it is executed. + + """ + # """ <-- emacs + + action_state = self.action_state + + c = self.maybe_dotted(callable) + module = self.inspect.getmodule(c) + if module is c: + try: + c = getattr(module, 'includeme') + except AttributeError: + raise ConfigurationError( + "module %r has no attribute 'includeme'" % (module.__name__) + ) + + spec = module.__name__ + ':' + c.__name__ + sourcefile = self.inspect.getsourcefile(c) + + if sourcefile is None: + raise ConfigurationError( + 'No source file for module %r (.py file must exist, ' + 'refusing to use orphan .pyc or .pyo file).' % module.__name__) + + + if action_state.processSpec(spec): + with self.route_prefix_context(route_prefix): + configurator = self.__class__( + registry=self.registry, + package=package_of(module), + root_package=self.root_package, + autocommit=self.autocommit, + route_prefix=self.route_prefix, + ) + configurator.basepath = os.path.dirname(sourcefile) + configurator.includepath = self.includepath + (spec,) + + self.begin() + try: + c(configurator) + finally: + self.end() + + def add_directive(self, name, directive, action_wrap=True): + """ + Add a directive method to the configurator. + + .. warning:: This method is typically only used by :app:`Pyramid` + framework extension authors, not by :app:`Pyramid` application + developers. + + Framework extenders can add directive methods to a configurator by + instructing their users to call ``config.add_directive('somename', + 'some.callable')``. This will make ``some.callable`` accessible as + ``config.somename``. ``some.callable`` should be a function which + accepts ``config`` as a first argument, and arbitrary positional and + keyword arguments following. It should use config.action as + necessary to perform actions. Directive methods can then be invoked + like 'built-in' directives such as ``add_view``, ``add_route``, etc. + + The ``action_wrap`` argument should be ``True`` for directives which + perform ``config.action`` with potentially conflicting + discriminators. ``action_wrap`` will cause the directive to be + wrapped in a decorator which provides more accurate conflict + cause information. + + ``add_directive`` does not participate in conflict detection, and + later calls to ``add_directive`` will override earlier calls. + """ + c = self.maybe_dotted(directive) + if not hasattr(self.registry, '_directives'): + self.registry._directives = {} + self.registry._directives[name] = (c, action_wrap) + + def __getattr__(self, name): + # allow directive extension names to work + directives = getattr(self.registry, '_directives', {}) + c = directives.get(name) + if c is None: + raise AttributeError(name) + c, action_wrap = c + if action_wrap: + c = action_method(c) + # Create a bound method (works on both Py2 and Py3) + # http://stackoverflow.com/a/1015405/209039 + m = c.__get__(self, self.__class__) + return m + + def with_package(self, package): + """ Return a new Configurator instance with the same registry + as this configurator. ``package`` may be an actual Python package + object or a :term:`dotted Python name` representing a package.""" + configurator = self.__class__( + registry=self.registry, + package=package, + root_package=self.root_package, + autocommit=self.autocommit, + route_prefix=self.route_prefix, + introspection=self.introspection, + ) + configurator.basepath = self.basepath + configurator.includepath = self.includepath + configurator.info = self.info + return configurator + + def maybe_dotted(self, dotted): + """ Resolve the :term:`dotted Python name` ``dotted`` to a + global Python object. If ``dotted`` is not a string, return + it without attempting to do any name resolution. If + ``dotted`` is a relative dotted name (e.g. ``.foo.bar``, + consider it relative to the ``package`` argument supplied to + this Configurator's constructor.""" + return self.name_resolver.maybe_resolve(dotted) + + def absolute_asset_spec(self, relative_spec): + """ Resolve the potentially relative :term:`asset + specification` string passed as ``relative_spec`` into an + absolute asset specification string and return the string. + Use the ``package`` of this configurator as the package to + which the asset specification will be considered relative + when generating an absolute asset specification. If the + provided ``relative_spec`` argument is already absolute, or if + the ``relative_spec`` is not a string, it is simply returned.""" + if not isinstance(relative_spec, string_types): + return relative_spec + return self._make_spec(relative_spec) + + absolute_resource_spec = absolute_asset_spec # b/w compat forever + + def begin(self, request=_marker): + """ Indicate that application or test configuration has begun. + This pushes a dictionary containing the :term:`application + registry` implied by ``registry`` attribute of this + configurator and the :term:`request` implied by the + ``request`` argument onto the :term:`thread local` stack + consulted by various :mod:`pyramid.threadlocal` API + functions. + + If ``request`` is not specified and the registry owned by the + configurator is already pushed as the current threadlocal registry + then this method will keep the current threadlocal request unchanged. + + .. versionchanged:: 1.8 + The current threadlocal request is propagated if the current + threadlocal registry remains unchanged. + + """ + if request is _marker: + current = self.manager.get() + if current['registry'] == self.registry: + request = current['request'] + else: + request = None + self.manager.push({'registry':self.registry, 'request':request}) + + def end(self): + """ Indicate that application or test configuration has ended. + This pops the last value pushed onto the :term:`thread local` + stack (usually by the ``begin`` method) and returns that + value. + """ + return self.manager.pop() + + def __enter__(self): + self.begin() + return self + + def __exit__(self, exc_type, exc_value, exc_traceback): + self.end() + + if exc_value is None: + self.commit() + + # this is *not* an action method (uses caller_package) + def scan(self, package=None, categories=None, onerror=None, ignore=None, + **kw): + """Scan a Python package and any of its subpackages for objects + marked with :term:`configuration decoration` such as + :class:`pyramid.view.view_config`. Any decorated object found will + influence the current configuration state. + + The ``package`` argument should be a Python :term:`package` or module + object (or a :term:`dotted Python name` which refers to such a + package or module). If ``package`` is ``None``, the package of the + *caller* is used. + + The ``categories`` argument, if provided, should be the + :term:`Venusian` 'scan categories' to use during scanning. Providing + this argument is not often necessary; specifying scan categories is + an extremely advanced usage. By default, ``categories`` is ``None`` + which will execute *all* Venusian decorator callbacks including + :app:`Pyramid`-related decorators such as + :class:`pyramid.view.view_config`. See the :term:`Venusian` + documentation for more information about limiting a scan by using an + explicit set of categories. + + The ``onerror`` argument, if provided, should be a Venusian + ``onerror`` callback function. The onerror function is passed to + :meth:`venusian.Scanner.scan` to influence error behavior when an + exception is raised during the scanning process. See the + :term:`Venusian` documentation for more information about ``onerror`` + callbacks. + + The ``ignore`` argument, if provided, should be a Venusian ``ignore`` + value. Providing an ``ignore`` argument allows the scan to ignore + particular modules, packages, or global objects during a scan. + ``ignore`` can be a string or a callable, or a list containing + strings or callables. The simplest usage of ``ignore`` is to provide + a module or package by providing a full path to its dotted name. For + example: ``config.scan(ignore='my.module.subpackage')`` would ignore + the ``my.module.subpackage`` package during a scan, which would + prevent the subpackage and any of its submodules from being imported + and scanned. See the :term:`Venusian` documentation for more + information about the ``ignore`` argument. + + To perform a ``scan``, Pyramid creates a Venusian ``Scanner`` object. + The ``kw`` argument represents a set of keyword arguments to pass to + the Venusian ``Scanner`` object's constructor. See the + :term:`venusian` documentation (its ``Scanner`` class) for more + information about the constructor. By default, the only keyword + arguments passed to the Scanner constructor are ``{'config':self}`` + where ``self`` is this configurator object. This services the + requirement of all built-in Pyramid decorators, but extension systems + may require additional arguments. Providing this argument is not + often necessary; it's an advanced usage. + + .. versionadded:: 1.1 + The ``**kw`` argument. + + .. versionadded:: 1.3 + The ``ignore`` argument. + + """ + package = self.maybe_dotted(package) + if package is None: # pragma: no cover + package = caller_package() + + ctorkw = {'config': self} + ctorkw.update(kw) + + scanner = self.venusian.Scanner(**ctorkw) + + scanner.scan(package, categories=categories, onerror=onerror, + ignore=ignore) + + def make_wsgi_app(self): + """ Commits any pending configuration statements, sends a + :class:`pyramid.events.ApplicationCreated` event to all listeners, + adds this configuration's registry to + :attr:`pyramid.config.global_registries`, and returns a + :app:`Pyramid` WSGI application representing the committed + configuration state.""" + self.commit() + app = Router(self.registry) + + # Allow tools like "pshell development.ini" to find the 'last' + # registry configured. + global_registries.add(self.registry) + + # Push the registry onto the stack in case any code that depends on + # the registry threadlocal APIs used in listeners subscribed to the + # IApplicationCreated event. + self.begin() + try: + self.registry.notify(ApplicationCreated(app)) + finally: + self.end() + + return app + + +# this class is licensed under the ZPL (stolen from Zope) +class ActionState(object): + def __init__(self): + # NB "actions" is an API, dep'd upon by pyramid_zcml's load_zcml func + self.actions = [] + self._seen_files = set() + + def processSpec(self, spec): + """Check whether a callable needs to be processed. The ``spec`` + refers to a unique identifier for the callable. + + Return True if processing is needed and False otherwise. If + the callable needs to be processed, it will be marked as + processed, assuming that the caller will procces the callable if + it needs to be processed. + """ + if spec in self._seen_files: + return False + self._seen_files.add(spec) + return True + + def action(self, discriminator, callable=None, args=(), kw=None, order=0, + includepath=(), info=None, introspectables=(), **extra): + """Add an action with the given discriminator, callable and arguments + """ + if kw is None: + kw = {} + action = extra + action.update( + dict( + discriminator=discriminator, + callable=callable, + args=args, + kw=kw, + includepath=includepath, + info=info, + order=order, + introspectables=introspectables, + ) + ) + self.actions.append(action) + + def execute_actions(self, clear=True, introspector=None): + """Execute the configuration actions + + This calls the action callables after resolving conflicts + + For example: + + >>> output = [] + >>> def f(*a, **k): + ... output.append(('f', a, k)) + >>> context = ActionState() + >>> context.actions = [ + ... (1, f, (1,)), + ... (1, f, (11,), {}, ('x', )), + ... (2, f, (2,)), + ... ] + >>> context.execute_actions() + >>> output + [('f', (1,), {}), ('f', (2,), {})] + + If the action raises an error, we convert it to a + ConfigurationExecutionError. + + >>> output = [] + >>> def bad(): + ... bad.xxx + >>> context.actions = [ + ... (1, f, (1,)), + ... (1, f, (11,), {}, ('x', )), + ... (2, f, (2,)), + ... (3, bad, (), {}, (), 'oops') + ... ] + >>> try: + ... v = context.execute_actions() + ... except ConfigurationExecutionError, v: + ... pass + >>> print(v) + exceptions.AttributeError: 'function' object has no attribute 'xxx' + in: + oops + + Note that actions executed before the error still have an effect: + + >>> output + [('f', (1,), {}), ('f', (2,), {})] + + The execution is re-entrant such that actions may be added by other + actions with the one caveat that the order of any added actions must + be equal to or larger than the current action. + + >>> output = [] + >>> def f(*a, **k): + ... output.append(('f', a, k)) + ... context.actions.append((3, g, (8,), {})) + >>> def g(*a, **k): + ... output.append(('g', a, k)) + >>> context.actions = [ + ... (1, f, (1,)), + ... ] + >>> context.execute_actions() + >>> output + [('f', (1,), {}), ('g', (8,), {})] + + """ + try: + all_actions = [] + executed_actions = [] + action_iter = iter([]) + conflict_state = ConflictResolverState() + + while True: + # We clear the actions list prior to execution so if there + # are some new actions then we add them to the mix and resolve + # conflicts again. This orders the new actions as well as + # ensures that the previously executed actions have no new + # conflicts. + if self.actions: + all_actions.extend(self.actions) + action_iter = resolveConflicts( + self.actions, + state=conflict_state, + ) + self.actions = [] + + action = next(action_iter, None) + if action is None: + # we are done! + break + + callable = action['callable'] + args = action['args'] + kw = action['kw'] + info = action['info'] + # we use "get" below in case an action was added via a ZCML + # directive that did not know about introspectables + introspectables = action.get('introspectables', ()) + + try: + if callable is not None: + callable(*args, **kw) + except Exception: + t, v, tb = sys.exc_info() + try: + reraise(ConfigurationExecutionError, + ConfigurationExecutionError(t, v, info), + tb) + finally: + del t, v, tb + + if introspector is not None: + for introspectable in introspectables: + introspectable.register(introspector, info) + + executed_actions.append(action) + + self.actions = all_actions + return executed_actions + + finally: + if clear: + self.actions = [] + + +class ConflictResolverState(object): + def __init__(self): + # keep a set of resolved discriminators to test against to ensure + # that a new action does not conflict with something already executed + self.resolved_ainfos = {} + + # actions left over from a previous iteration + self.remaining_actions = [] + + # after executing an action we memoize its order to avoid any new + # actions sending us backward + self.min_order = None + + # unique tracks the index of the action so we need it to increase + # monotonically across invocations to resolveConflicts + self.start = 0 + + +# this function is licensed under the ZPL (stolen from Zope) +def resolveConflicts(actions, state=None): + """Resolve conflicting actions + + Given an actions list, identify and try to resolve conflicting actions. + Actions conflict if they have the same non-None discriminator. + + Conflicting actions can be resolved if the include path of one of + the actions is a prefix of the includepaths of the other + conflicting actions and is unequal to the include paths in the + other conflicting actions. + + Actions are resolved on a per-order basis because some discriminators + cannot be computed until earlier actions have executed. An action in an + earlier order may execute successfully only to find out later that it was + overridden by another action with a smaller include path. This will result + in a conflict as there is no way to revert the original action. + + ``state`` may be an instance of ``ConflictResolverState`` that + can be used to resume execution and resolve the new actions against the + list of executed actions from a previous call. + + """ + if state is None: + state = ConflictResolverState() + + # pick up where we left off last time, but track the new actions as well + state.remaining_actions.extend(normalize_actions(actions)) + actions = state.remaining_actions + + def orderandpos(v): + n, v = v + return (v['order'] or 0, n) + + def orderonly(v): + n, v = v + return v['order'] or 0 + + sactions = sorted(enumerate(actions, start=state.start), key=orderandpos) + for order, actiongroup in itertools.groupby(sactions, orderonly): + # "order" is an integer grouping. Actions in a lower order will be + # executed before actions in a higher order. All of the actions in + # one grouping will be executed (its callable, if any will be called) + # before any of the actions in the next. + output = [] + unique = {} + + # error out if we went backward in order + if state.min_order is not None and order < state.min_order: + r = ['Actions were added to order={0} after execution had moved ' + 'on to order={1}. Conflicting actions: ' + .format(order, state.min_order)] + for i, action in actiongroup: + for line in str(action['info']).rstrip().split('\n'): + r.append(" " + line) + raise ConfigurationError('\n'.join(r)) + + for i, action in actiongroup: + # Within an order, actions are executed sequentially based on + # original action ordering ("i"). + + # "ainfo" is a tuple of (i, action) where "i" is an integer + # expressing the relative position of this action in the action + # list being resolved, and "action" is an action dictionary. The + # purpose of an ainfo is to associate an "i" with a particular + # action; "i" exists for sorting after conflict resolution. + ainfo = (i, action) + + # wait to defer discriminators until we are on their order because + # the discriminator may depend on state from a previous order + discriminator = undefer(action['discriminator']) + action['discriminator'] = discriminator + + if discriminator is None: + # The discriminator is None, so this action can never conflict. + # We can add it directly to the result. + output.append(ainfo) + continue + + L = unique.setdefault(discriminator, []) + L.append(ainfo) + + # Check for conflicts + conflicts = {} + for discriminator, ainfos in unique.items(): + # We use (includepath, i) as a sort key because we need to + # sort the actions by the paths so that the shortest path with a + # given prefix comes first. The "first" action is the one with the + # shortest include path. We break sorting ties using "i". + def bypath(ainfo): + path, i = ainfo[1]['includepath'], ainfo[0] + return path, order, i + + ainfos.sort(key=bypath) + ainfo, rest = ainfos[0], ainfos[1:] + _, action = ainfo + + # ensure this new action does not conflict with a previously + # resolved action from an earlier order / invocation + prev_ainfo = state.resolved_ainfos.get(discriminator) + if prev_ainfo is not None: + _, paction = prev_ainfo + basepath, baseinfo = paction['includepath'], paction['info'] + includepath = action['includepath'] + # if the new action conflicts with the resolved action then + # note the conflict, otherwise drop the action as it's + # effectively overriden by the previous action + if (includepath[:len(basepath)] != basepath or + includepath == basepath): + L = conflicts.setdefault(discriminator, [baseinfo]) + L.append(action['info']) + + else: + output.append(ainfo) + + basepath, baseinfo = action['includepath'], action['info'] + for _, action in rest: + includepath = action['includepath'] + # Test whether path is a prefix of opath + if (includepath[:len(basepath)] != basepath or # not a prefix + includepath == basepath): + L = conflicts.setdefault(discriminator, [baseinfo]) + L.append(action['info']) + + if conflicts: + raise ConfigurationConflictError(conflicts) + + # sort resolved actions by "i" and yield them one by one + for i, action in sorted(output, key=operator.itemgetter(0)): + # do not memoize the order until we resolve an action inside it + state.min_order = action['order'] + state.start = i + 1 + state.remaining_actions.remove(action) + state.resolved_ainfos[action['discriminator']] = (i, action) + yield action + + +def normalize_actions(actions): + """Convert old-style tuple actions to new-style dicts.""" + result = [] + for v in actions: + if not isinstance(v, dict): + v = expand_action_tuple(*v) + result.append(v) + return result + + +def expand_action_tuple( + discriminator, callable=None, args=(), kw=None, includepath=(), + info=None, order=0, introspectables=(), +): + if kw is None: + kw = {} + return dict( + discriminator=discriminator, + callable=callable, + args=args, + kw=kw, + includepath=includepath, + info=info, + order=order, + introspectables=introspectables, + ) + + +global_registries = WeakOrderedSet() diff --git a/src/pyramid/config/adapters.py b/src/pyramid/config/adapters.py new file mode 100644 index 000000000..945faa3c6 --- /dev/null +++ b/src/pyramid/config/adapters.py @@ -0,0 +1,326 @@ +from webob import Response as WebobResponse + +from functools import update_wrapper + +from zope.interface import Interface + +from pyramid.interfaces import ( + IResponse, + ITraverser, + IResourceURL, + ) + +from pyramid.util import takes_one_arg + +from pyramid.config.util import action_method + + +class AdaptersConfiguratorMixin(object): + @action_method + def add_subscriber(self, subscriber, iface=None, **predicates): + """Add an event :term:`subscriber` for the event stream + implied by the supplied ``iface`` interface. + + The ``subscriber`` argument represents a callable object (or a + :term:`dotted Python name` which identifies a callable); it will be + called with a single object ``event`` whenever :app:`Pyramid` emits + an :term:`event` associated with the ``iface``, which may be an + :term:`interface` or a class or a :term:`dotted Python name` to a + global object representing an interface or a class. + + Using the default ``iface`` value, ``None`` will cause the subscriber + to be registered for all event types. See :ref:`events_chapter` for + more information about events and subscribers. + + Any number of predicate keyword arguments may be passed in + ``**predicates``. Each predicate named will narrow the set of + circumstances in which the subscriber will be invoked. Each named + predicate must have been registered via + :meth:`pyramid.config.Configurator.add_subscriber_predicate` before it + can be used. See :ref:`subscriber_predicates` for more information. + + .. versionadded:: 1.4 + The ``**predicates`` argument. + """ + dotted = self.maybe_dotted + subscriber, iface = dotted(subscriber), dotted(iface) + if iface is None: + iface = (Interface,) + if not isinstance(iface, (tuple, list)): + iface = (iface,) + + def register(): + predlist = self.get_predlist('subscriber') + order, preds, phash = predlist.make(self, **predicates) + + derived_predicates = [ self._derive_predicate(p) for p in preds ] + derived_subscriber = self._derive_subscriber( + subscriber, + derived_predicates, + ) + + intr.update( + {'phash':phash, + 'order':order, + 'predicates':preds, + 'derived_predicates':derived_predicates, + 'derived_subscriber':derived_subscriber, + } + ) + + self.registry.registerHandler(derived_subscriber, iface) + + intr = self.introspectable( + 'subscribers', + id(subscriber), + self.object_description(subscriber), + 'subscriber' + ) + + intr['subscriber'] = subscriber + intr['interfaces'] = iface + + self.action(None, register, introspectables=(intr,)) + return subscriber + + def _derive_predicate(self, predicate): + derived_predicate = predicate + + if eventonly(predicate): + def derived_predicate(*arg): + return predicate(arg[0]) + # seems pointless to try to fix __doc__, __module__, etc as + # predicate will invariably be an instance + + return derived_predicate + + def _derive_subscriber(self, subscriber, predicates): + derived_subscriber = subscriber + + if eventonly(subscriber): + def derived_subscriber(*arg): + return subscriber(arg[0]) + if hasattr(subscriber, '__name__'): + update_wrapper(derived_subscriber, subscriber) + + if not predicates: + return derived_subscriber + + def subscriber_wrapper(*arg): + # We need to accept *arg and pass it along because zope subscribers + # are designed awkwardly. Notification via + # registry.adapter.subscribers will always call an associated + # subscriber with all of the objects involved in the subscription + # lookup, despite the fact that the event sender always has the + # option to attach those objects to the event object itself, and + # almost always does. + # + # The "eventonly" jazz sprinkled in this function and related + # functions allows users to define subscribers and predicates which + # accept only an event argument without needing to accept the rest + # of the adaptation arguments. Had I been smart enough early on to + # use .subscriptions to find the subscriber functions in order to + # call them manually with a single "event" argument instead of + # relying on .subscribers to both find and call them implicitly + # with all args, the eventonly hack would not have been required. + # At this point, though, using .subscriptions and manual execution + # is not possible without badly breaking backwards compatibility. + if all((predicate(*arg) for predicate in predicates)): + return derived_subscriber(*arg) + + if hasattr(subscriber, '__name__'): + update_wrapper(subscriber_wrapper, subscriber) + + return subscriber_wrapper + + @action_method + def add_subscriber_predicate(self, name, factory, weighs_more_than=None, + weighs_less_than=None): + """ + .. versionadded:: 1.4 + + Adds a subscriber predicate factory. The associated subscriber + predicate can later be named as a keyword argument to + :meth:`pyramid.config.Configurator.add_subscriber` in the + ``**predicates`` anonymous keyword argument dictionary. + + ``name`` should be the name of the predicate. It must be a valid + Python identifier (it will be used as a ``**predicates`` keyword + argument to :meth:`~pyramid.config.Configurator.add_subscriber`). + + ``factory`` should be a :term:`predicate factory` or :term:`dotted + Python name` which refers to a predicate factory. + + See :ref:`subscriber_predicates` for more information. + + """ + self._add_predicate( + 'subscriber', + name, + factory, + weighs_more_than=weighs_more_than, + weighs_less_than=weighs_less_than + ) + + @action_method + def add_response_adapter(self, adapter, type_or_iface): + """ When an object of type (or interface) ``type_or_iface`` is + returned from a view callable, Pyramid will use the adapter + ``adapter`` to convert it into an object which implements the + :class:`pyramid.interfaces.IResponse` interface. If ``adapter`` is + None, an object returned of type (or interface) ``type_or_iface`` + will itself be used as a response object. + + ``adapter`` and ``type_or_interface`` may be Python objects or + strings representing dotted names to importable Python global + objects. + + See :ref:`using_iresponse` for more information.""" + adapter = self.maybe_dotted(adapter) + type_or_iface = self.maybe_dotted(type_or_iface) + def register(): + reg = self.registry + if adapter is None: + reg.registerSelfAdapter((type_or_iface,), IResponse) + else: + reg.registerAdapter(adapter, (type_or_iface,), IResponse) + discriminator = (IResponse, type_or_iface) + intr = self.introspectable( + 'response adapters', + discriminator, + self.object_description(adapter), + 'response adapter') + intr['adapter'] = adapter + intr['type'] = type_or_iface + self.action(discriminator, register, introspectables=(intr,)) + + def add_default_response_adapters(self): + # cope with WebOb response objects that aren't decorated with IResponse + self.add_response_adapter(None, WebobResponse) + + @action_method + def add_traverser(self, adapter, iface=None): + """ + The superdefault :term:`traversal` algorithm that :app:`Pyramid` uses + is explained in :ref:`traversal_algorithm`. Though it is rarely + necessary, this default algorithm can be swapped out selectively for + a different traversal pattern via configuration. The section + entitled :ref:`changing_the_traverser` details how to create a + traverser class. + + For example, to override the superdefault traverser used by Pyramid, + you might do something like this: + + .. code-block:: python + + from myapp.traversal import MyCustomTraverser + config.add_traverser(MyCustomTraverser) + + This would cause the Pyramid superdefault traverser to never be used; + instead all traversal would be done using your ``MyCustomTraverser`` + class, no matter which object was returned by the :term:`root + factory` of this application. Note that we passed no arguments to + the ``iface`` keyword parameter. The default value of ``iface``, + ``None`` represents that the registered traverser should be used when + no other more specific traverser is available for the object returned + by the root factory. + + However, more than one traversal algorithm can be active at the same + time. The traverser used can depend on the result of the :term:`root + factory`. For instance, if your root factory returns more than one + type of object conditionally, you could claim that an alternate + traverser adapter should be used against one particular class or + interface returned by that root factory. When the root factory + returned an object that implemented that class or interface, a custom + traverser would be used. Otherwise, the default traverser would be + used. The ``iface`` argument represents the class of the object that + the root factory might return or an :term:`interface` that the object + might implement. + + To use a particular traverser only when the root factory returns a + particular class: + + .. code-block:: python + + config.add_traverser(MyCustomTraverser, MyRootClass) + + When more than one traverser is active, the "most specific" traverser + will be used (the one that matches the class or interface of the + value returned by the root factory most closely). + + Note that either ``adapter`` or ``iface`` can be a :term:`dotted + Python name` or a Python object. + + See :ref:`changing_the_traverser` for more information. + """ + iface = self.maybe_dotted(iface) + adapter = self.maybe_dotted(adapter) + def register(iface=iface): + if iface is None: + iface = Interface + self.registry.registerAdapter(adapter, (iface,), ITraverser) + discriminator = ('traverser', iface) + intr = self.introspectable( + 'traversers', + discriminator, + 'traverser for %r' % iface, + 'traverser', + ) + intr['adapter'] = adapter + intr['iface'] = iface + self.action(discriminator, register, introspectables=(intr,)) + + @action_method + def add_resource_url_adapter(self, adapter, resource_iface=None): + """ + .. versionadded:: 1.3 + + When you add a traverser as described in + :ref:`changing_the_traverser`, it's convenient to continue to use the + :meth:`pyramid.request.Request.resource_url` API. However, since the + way traversal is done may have been modified, the URLs that + ``resource_url`` generates by default may be incorrect when resources + are returned by a custom traverser. + + If you've added a traverser, you can change how + :meth:`~pyramid.request.Request.resource_url` generates a URL for a + specific type of resource by calling this method. + + The ``adapter`` argument represents a class that implements the + :class:`~pyramid.interfaces.IResourceURL` interface. The class + constructor should accept two arguments in its constructor (the + resource and the request) and the resulting instance should provide + the attributes detailed in that interface (``virtual_path`` and + ``physical_path``, in particular). + + The ``resource_iface`` argument represents a class or interface that + the resource should possess for this url adapter to be used when + :meth:`pyramid.request.Request.resource_url` looks up a resource url + adapter. If ``resource_iface`` is not passed, or it is passed as + ``None``, the url adapter will be used for every type of resource. + + See :ref:`changing_resource_url` for more information. + """ + adapter = self.maybe_dotted(adapter) + resource_iface = self.maybe_dotted(resource_iface) + def register(resource_iface=resource_iface): + if resource_iface is None: + resource_iface = Interface + self.registry.registerAdapter( + adapter, + (resource_iface, Interface), + IResourceURL, + ) + discriminator = ('resource url adapter', resource_iface) + intr = self.introspectable( + 'resource url adapters', + discriminator, + 'resource url adapter for resource iface %r' % resource_iface, + 'resource url adapter', + ) + intr['adapter'] = adapter + intr['resource_iface'] = resource_iface + self.action(discriminator, register, introspectables=(intr,)) + +def eventonly(callee): + return takes_one_arg(callee, argname='event') diff --git a/src/pyramid/config/assets.py b/src/pyramid/config/assets.py new file mode 100644 index 000000000..b9536df42 --- /dev/null +++ b/src/pyramid/config/assets.py @@ -0,0 +1,396 @@ +import os +import pkg_resources +import sys + +from zope.interface import implementer + +from pyramid.interfaces import ( + IPackageOverrides, + PHASE1_CONFIG, +) + +from pyramid.exceptions import ConfigurationError +from pyramid.threadlocal import get_current_registry + +from pyramid.config.util import action_method + +class OverrideProvider(pkg_resources.DefaultProvider): + def __init__(self, module): + pkg_resources.DefaultProvider.__init__(self, module) + self.module_name = module.__name__ + + def _get_overrides(self): + reg = get_current_registry() + overrides = reg.queryUtility(IPackageOverrides, self.module_name) + return overrides + + def get_resource_filename(self, manager, resource_name): + """ Return a true filesystem path for resource_name, + co-ordinating the extraction with manager, if the resource + must be unpacked to the filesystem. + """ + overrides = self._get_overrides() + if overrides is not None: + filename = overrides.get_filename(resource_name) + if filename is not None: + return filename + return pkg_resources.DefaultProvider.get_resource_filename( + self, manager, resource_name) + + def get_resource_stream(self, manager, resource_name): + """ Return a readable file-like object for resource_name.""" + overrides = self._get_overrides() + if overrides is not None: + stream = overrides.get_stream(resource_name) + if stream is not None: + return stream + return pkg_resources.DefaultProvider.get_resource_stream( + self, manager, resource_name) + + def get_resource_string(self, manager, resource_name): + """ Return a string containing the contents of resource_name.""" + overrides = self._get_overrides() + if overrides is not None: + string = overrides.get_string(resource_name) + if string is not None: + return string + return pkg_resources.DefaultProvider.get_resource_string( + self, manager, resource_name) + + def has_resource(self, resource_name): + overrides = self._get_overrides() + if overrides is not None: + result = overrides.has_resource(resource_name) + if result is not None: + return result + return pkg_resources.DefaultProvider.has_resource( + self, resource_name) + + def resource_isdir(self, resource_name): + overrides = self._get_overrides() + if overrides is not None: + result = overrides.isdir(resource_name) + if result is not None: + return result + return pkg_resources.DefaultProvider.resource_isdir( + self, resource_name) + + def resource_listdir(self, resource_name): + overrides = self._get_overrides() + if overrides is not None: + result = overrides.listdir(resource_name) + if result is not None: + return result + return pkg_resources.DefaultProvider.resource_listdir( + self, resource_name) + + +@implementer(IPackageOverrides) +class PackageOverrides(object): + # pkg_resources arg in kw args below for testing + def __init__(self, package, pkg_resources=pkg_resources): + loader = self._real_loader = getattr(package, '__loader__', None) + if isinstance(loader, self.__class__): + self._real_loader = None + # We register ourselves as a __loader__ *only* to support the + # setuptools _find_adapter adapter lookup; this class doesn't + # actually support the PEP 302 loader "API". This is + # excusable due to the following statement in the spec: + # ... Loader objects are not + # required to offer any useful functionality (any such functionality, + # such as the zipimport get_data() method mentioned above, is + # optional)... + # A __loader__ attribute is basically metadata, and setuptools + # uses it as such. + package.__loader__ = self + # we call register_loader_type for every instantiation of this + # class; that's OK, it's idempotent to do it more than once. + pkg_resources.register_loader_type(self.__class__, OverrideProvider) + self.overrides = [] + self.overridden_package_name = package.__name__ + + def insert(self, path, source): + if not path or path.endswith('/'): + override = DirectoryOverride(path, source) + else: + override = FileOverride(path, source) + self.overrides.insert(0, override) + return override + + def filtered_sources(self, resource_name): + for override in self.overrides: + o = override(resource_name) + if o is not None: + yield o + + def get_filename(self, resource_name): + for source, path in self.filtered_sources(resource_name): + result = source.get_filename(path) + if result is not None: + return result + + def get_stream(self, resource_name): + for source, path in self.filtered_sources(resource_name): + result = source.get_stream(path) + if result is not None: + return result + + def get_string(self, resource_name): + for source, path in self.filtered_sources(resource_name): + result = source.get_string(path) + if result is not None: + return result + + def has_resource(self, resource_name): + for source, path in self.filtered_sources(resource_name): + if source.exists(path): + return True + + def isdir(self, resource_name): + for source, path in self.filtered_sources(resource_name): + result = source.isdir(path) + if result is not None: + return result + + def listdir(self, resource_name): + for source, path in self.filtered_sources(resource_name): + result = source.listdir(path) + if result is not None: + return result + + @property + def real_loader(self): + if self._real_loader is None: + raise NotImplementedError() + return self._real_loader + + def get_data(self, path): + """ See IPEP302Loader. + """ + return self.real_loader.get_data(path) + + def is_package(self, fullname): + """ See IPEP302Loader. + """ + return self.real_loader.is_package(fullname) + + def get_code(self, fullname): + """ See IPEP302Loader. + """ + return self.real_loader.get_code(fullname) + + def get_source(self, fullname): + """ See IPEP302Loader. + """ + return self.real_loader.get_source(fullname) + + +class DirectoryOverride: + def __init__(self, path, source): + self.path = path + self.pathlen = len(self.path) + self.source = source + + def __call__(self, resource_name): + if resource_name.startswith(self.path): + new_path = resource_name[self.pathlen:] + return self.source, new_path + +class FileOverride: + def __init__(self, path, source): + self.path = path + self.source = source + + def __call__(self, resource_name): + if resource_name == self.path: + return self.source, '' + + +class PackageAssetSource(object): + """ + An asset source relative to a package. + + If this asset source is a file, then we expect the ``prefix`` to point + to the new name of the file, and the incoming ``resource_name`` will be + the empty string, as returned by the ``FileOverride``. + + """ + def __init__(self, package, prefix): + self.package = package + if hasattr(package, '__name__'): + self.pkg_name = package.__name__ + else: + self.pkg_name = package + self.prefix = prefix + + def get_path(self, resource_name): + return '%s%s' % (self.prefix, resource_name) + + def get_filename(self, resource_name): + path = self.get_path(resource_name) + if pkg_resources.resource_exists(self.pkg_name, path): + return pkg_resources.resource_filename(self.pkg_name, path) + + def get_stream(self, resource_name): + path = self.get_path(resource_name) + if pkg_resources.resource_exists(self.pkg_name, path): + return pkg_resources.resource_stream(self.pkg_name, path) + + def get_string(self, resource_name): + path = self.get_path(resource_name) + if pkg_resources.resource_exists(self.pkg_name, path): + return pkg_resources.resource_string(self.pkg_name, path) + + def exists(self, resource_name): + path = self.get_path(resource_name) + if pkg_resources.resource_exists(self.pkg_name, path): + return True + + def isdir(self, resource_name): + path = self.get_path(resource_name) + if pkg_resources.resource_exists(self.pkg_name, path): + return pkg_resources.resource_isdir(self.pkg_name, path) + + def listdir(self, resource_name): + path = self.get_path(resource_name) + if pkg_resources.resource_exists(self.pkg_name, path): + return pkg_resources.resource_listdir(self.pkg_name, path) + + +class FSAssetSource(object): + """ + An asset source relative to a path in the filesystem. + + """ + def __init__(self, prefix): + self.prefix = prefix + + def get_path(self, resource_name): + if resource_name: + path = os.path.join(self.prefix, resource_name) + else: + path = self.prefix + return path + + def get_filename(self, resource_name): + path = self.get_path(resource_name) + if os.path.exists(path): + return path + + def get_stream(self, resource_name): + path = self.get_filename(resource_name) + if path is not None: + return open(path, 'rb') + + def get_string(self, resource_name): + stream = self.get_stream(resource_name) + if stream is not None: + with stream: + return stream.read() + + def exists(self, resource_name): + path = self.get_filename(resource_name) + if path is not None: + return True + + def isdir(self, resource_name): + path = self.get_filename(resource_name) + if path is not None: + return os.path.isdir(path) + + def listdir(self, resource_name): + path = self.get_filename(resource_name) + if path is not None: + return os.listdir(path) + + +class AssetsConfiguratorMixin(object): + def _override(self, package, path, override_source, + PackageOverrides=PackageOverrides): + pkg_name = package.__name__ + override = self.registry.queryUtility(IPackageOverrides, name=pkg_name) + if override is None: + override = PackageOverrides(package) + self.registry.registerUtility(override, IPackageOverrides, + name=pkg_name) + override.insert(path, override_source) + + @action_method + def override_asset(self, to_override, override_with, _override=None): + """ Add a :app:`Pyramid` asset override to the current + configuration state. + + ``to_override`` is an :term:`asset specification` to the + asset being overridden. + + ``override_with`` is an :term:`asset specification` to the + asset that is performing the override. This may also be an absolute + path. + + See :ref:`assets_chapter` for more + information about asset overrides.""" + if to_override == override_with: + raise ConfigurationError( + 'You cannot override an asset with itself') + + package = to_override + path = '' + if ':' in to_override: + package, path = to_override.split(':', 1) + + # *_isdir = override is package or directory + overridden_isdir = path == '' or path.endswith('/') + + if os.path.isabs(override_with): + override_source = FSAssetSource(override_with) + if not os.path.exists(override_with): + raise ConfigurationError( + 'Cannot override asset with an absolute path that does ' + 'not exist') + override_isdir = os.path.isdir(override_with) + override_package = None + override_prefix = override_with + else: + override_package = override_with + override_prefix = '' + if ':' in override_with: + override_package, override_prefix = override_with.split(':', 1) + + __import__(override_package) + to_package = sys.modules[override_package] + override_source = PackageAssetSource(to_package, override_prefix) + + override_isdir = ( + override_prefix == '' or + override_with.endswith('/') + ) + + if overridden_isdir and (not override_isdir): + raise ConfigurationError( + 'A directory cannot be overridden with a file (put a ' + 'slash at the end of override_with if necessary)') + + if (not overridden_isdir) and override_isdir: + raise ConfigurationError( + 'A file cannot be overridden with a directory (put a ' + 'slash at the end of to_override if necessary)') + + override = _override or self._override # test jig + + def register(): + __import__(package) + from_package = sys.modules[package] + override(from_package, path, override_source) + + intr = self.introspectable( + 'asset overrides', + (package, override_package, path, override_prefix), + '%s -> %s' % (to_override, override_with), + 'asset override', + ) + intr['to_override'] = to_override + intr['override_with'] = override_with + self.action(None, register, introspectables=(intr,), + order=PHASE1_CONFIG) + + override_resource = override_asset # bw compat diff --git a/src/pyramid/config/factories.py b/src/pyramid/config/factories.py new file mode 100644 index 000000000..52248269d --- /dev/null +++ b/src/pyramid/config/factories.py @@ -0,0 +1,245 @@ +from zope.interface import implementer + +from pyramid.interfaces import ( + IDefaultRootFactory, + IExecutionPolicy, + IRequestFactory, + IResponseFactory, + IRequestExtensions, + IRootFactory, + ISessionFactory, + ) + +from pyramid.router import default_execution_policy +from pyramid.traversal import DefaultRootFactory + +from pyramid.util import ( + get_callable_name, + InstancePropertyHelper, + ) + +from pyramid.config.util import action_method + +class FactoriesConfiguratorMixin(object): + @action_method + def set_root_factory(self, factory): + """ Add a :term:`root factory` to the current configuration + state. If the ``factory`` argument is ``None`` a default root + factory will be registered. + + .. note:: + + Using the ``root_factory`` argument to the + :class:`pyramid.config.Configurator` constructor can be used to + achieve the same purpose. + """ + factory = self.maybe_dotted(factory) + if factory is None: + factory = DefaultRootFactory + + def register(): + self.registry.registerUtility(factory, IRootFactory) + self.registry.registerUtility(factory, IDefaultRootFactory) # b/c + + intr = self.introspectable('root factories', + None, + self.object_description(factory), + 'root factory') + intr['factory'] = factory + self.action(IRootFactory, register, introspectables=(intr,)) + + _set_root_factory = set_root_factory # bw compat + + @action_method + def set_session_factory(self, factory): + """ + Configure the application with a :term:`session factory`. If this + method is called, the ``factory`` argument must be a session + factory callable or a :term:`dotted Python name` to that factory. + + .. note:: + + Using the ``session_factory`` argument to the + :class:`pyramid.config.Configurator` constructor can be used to + achieve the same purpose. + """ + factory = self.maybe_dotted(factory) + + def register(): + self.registry.registerUtility(factory, ISessionFactory) + intr = self.introspectable('session factory', None, + self.object_description(factory), + 'session factory') + intr['factory'] = factory + self.action(ISessionFactory, register, introspectables=(intr,)) + + @action_method + def set_request_factory(self, factory): + """ The object passed as ``factory`` should be an object (or a + :term:`dotted Python name` which refers to an object) which + will be used by the :app:`Pyramid` router to create all + request objects. This factory object must have the same + methods and attributes as the + :class:`pyramid.request.Request` class (particularly + ``__call__``, and ``blank``). + + See :meth:`pyramid.config.Configurator.add_request_method` + for a less intrusive way to extend the request objects with + custom methods and properties. + + .. note:: + + Using the ``request_factory`` argument to the + :class:`pyramid.config.Configurator` constructor + can be used to achieve the same purpose. + """ + factory = self.maybe_dotted(factory) + + def register(): + self.registry.registerUtility(factory, IRequestFactory) + intr = self.introspectable('request factory', None, + self.object_description(factory), + 'request factory') + intr['factory'] = factory + self.action(IRequestFactory, register, introspectables=(intr,)) + + @action_method + def set_response_factory(self, factory): + """ The object passed as ``factory`` should be an object (or a + :term:`dotted Python name` which refers to an object) which + will be used by the :app:`Pyramid` as the default response + objects. The factory should conform to the + :class:`pyramid.interfaces.IResponseFactory` interface. + + .. note:: + + Using the ``response_factory`` argument to the + :class:`pyramid.config.Configurator` constructor + can be used to achieve the same purpose. + """ + factory = self.maybe_dotted(factory) + + def register(): + self.registry.registerUtility(factory, IResponseFactory) + + intr = self.introspectable('response factory', None, + self.object_description(factory), + 'response factory') + intr['factory'] = factory + self.action(IResponseFactory, register, introspectables=(intr,)) + + @action_method + def add_request_method(self, + callable=None, + name=None, + property=False, + reify=False): + """ Add a property or method to the request object. + + When adding a method to the request, ``callable`` may be any + function that receives the request object as the first + parameter. If ``name`` is ``None`` then it will be computed + from the name of the ``callable``. + + When adding a property to the request, ``callable`` can either + be a callable that accepts the request as its single positional + parameter, or it can be a property descriptor. If ``callable`` is + a property descriptor, it has to be an instance of a class which is + a subclass of ``property``. If ``name`` is ``None``, the name of + the property will be computed from the name of the ``callable``. + + If the ``callable`` is a property descriptor a ``ValueError`` + will be raised if ``name`` is ``None`` or ``reify`` is ``True``. + + See :meth:`pyramid.request.Request.set_property` for more + details on ``property`` vs ``reify``. When ``reify`` is + ``True``, the value of ``property`` is assumed to also be + ``True``. + + In all cases, ``callable`` may also be a + :term:`dotted Python name` which refers to either a callable or + a property descriptor. + + If ``callable`` is ``None`` then the method is only used to + assist in conflict detection between different addons requesting + the same attribute on the request object. + + This is the recommended method for extending the request object + and should be used in favor of providing a custom request + factory via + :meth:`pyramid.config.Configurator.set_request_factory`. + + .. versionadded:: 1.4 + """ + if callable is not None: + callable = self.maybe_dotted(callable) + + property = property or reify + if property: + name, callable = InstancePropertyHelper.make_property( + callable, name=name, reify=reify) + elif name is None: + name = callable.__name__ + else: + name = get_callable_name(name) + + def register(): + exts = self.registry.queryUtility(IRequestExtensions) + + if exts is None: + exts = _RequestExtensions() + self.registry.registerUtility(exts, IRequestExtensions) + + plist = exts.descriptors if property else exts.methods + plist[name] = callable + + if callable is None: + self.action(('request extensions', name), None) + elif property: + intr = self.introspectable('request extensions', name, + self.object_description(callable), + 'request property') + intr['callable'] = callable + intr['property'] = True + intr['reify'] = reify + self.action(('request extensions', name), register, + introspectables=(intr,)) + else: + intr = self.introspectable('request extensions', name, + self.object_description(callable), + 'request method') + intr['callable'] = callable + intr['property'] = False + intr['reify'] = False + self.action(('request extensions', name), register, + introspectables=(intr,)) + + @action_method + def set_execution_policy(self, policy): + """ + Override the :app:`Pyramid` :term:`execution policy` in the + current configuration. The ``policy`` argument must be an instance + of an :class:`pyramid.interfaces.IExecutionPolicy` or a + :term:`dotted Python name` that points at an instance of an + execution policy. + + """ + policy = self.maybe_dotted(policy) + if policy is None: + policy = default_execution_policy + + def register(): + self.registry.registerUtility(policy, IExecutionPolicy) + + intr = self.introspectable('execution policy', None, + self.object_description(policy), + 'execution policy') + intr['policy'] = policy + self.action(IExecutionPolicy, register, introspectables=(intr,)) + + +@implementer(IRequestExtensions) +class _RequestExtensions(object): + def __init__(self): + self.descriptors = {} + self.methods = {} diff --git a/src/pyramid/config/i18n.py b/src/pyramid/config/i18n.py new file mode 100644 index 000000000..5dabe2845 --- /dev/null +++ b/src/pyramid/config/i18n.py @@ -0,0 +1,120 @@ +from pyramid.interfaces import ( + ILocaleNegotiator, + ITranslationDirectories, + ) + +from pyramid.exceptions import ConfigurationError +from pyramid.path import AssetResolver + +from pyramid.config.util import action_method + +class I18NConfiguratorMixin(object): + @action_method + def set_locale_negotiator(self, negotiator): + """ + Set the :term:`locale negotiator` for this application. The + :term:`locale negotiator` is a callable which accepts a + :term:`request` object and which returns a :term:`locale + name`. The ``negotiator`` argument should be the locale + negotiator implementation or a :term:`dotted Python name` + which refers to such an implementation. + + Later calls to this method override earlier calls; there can + be only one locale negotiator active at a time within an + application. See :ref:`activating_translation` for more + information. + + .. note:: + + Using the ``locale_negotiator`` argument to the + :class:`pyramid.config.Configurator` constructor can be used to + achieve the same purpose. + """ + def register(): + self._set_locale_negotiator(negotiator) + intr = self.introspectable('locale negotiator', None, + self.object_description(negotiator), + 'locale negotiator') + intr['negotiator'] = negotiator + self.action(ILocaleNegotiator, register, introspectables=(intr,)) + + def _set_locale_negotiator(self, negotiator): + locale_negotiator = self.maybe_dotted(negotiator) + self.registry.registerUtility(locale_negotiator, ILocaleNegotiator) + + @action_method + def add_translation_dirs(self, *specs, **kw): + """ Add one or more :term:`translation directory` paths to the + current configuration state. The ``specs`` argument is a + sequence that may contain absolute directory paths + (e.g. ``/usr/share/locale``) or :term:`asset specification` + names naming a directory path (e.g. ``some.package:locale``) + or a combination of the two. + + Example: + + .. code-block:: python + + config.add_translation_dirs('/usr/share/locale', + 'some.package:locale') + + The translation directories are defined as a list in which + translations defined later have precedence over translations defined + earlier. + + By default, consecutive calls to ``add_translation_dirs`` will add + directories to the start of the list. This means later calls to + ``add_translation_dirs`` will have their translations trumped by + earlier calls. If you explicitly need this call to trump an earlier + call then you may set ``override`` to ``True``. + + If multiple specs are provided in a single call to + ``add_translation_dirs``, the directories will be inserted in the + order they're provided (earlier items are trumped by later items). + + .. versionchanged:: 1.8 + + The ``override`` parameter was added to allow a later call + to ``add_translation_dirs`` to override an earlier call, inserting + folders at the beginning of the translation directory list. + + """ + introspectables = [] + override = kw.pop('override', False) + if kw: + raise TypeError('invalid keyword arguments: %s', sorted(kw.keys())) + + def register(): + directories = [] + resolver = AssetResolver(self.package_name) + + # defer spec resolution until register to allow for asset + # overrides to take place in an earlier config phase + for spec in specs: + # the trailing slash helps match asset overrides for folders + if not spec.endswith('/'): + spec += '/' + asset = resolver.resolve(spec) + directory = asset.abspath() + if not asset.isdir(): + raise ConfigurationError('"%s" is not a directory' % + directory) + intr = self.introspectable('translation directories', directory, + spec, 'translation directory') + intr['directory'] = directory + intr['spec'] = spec + introspectables.append(intr) + directories.append(directory) + + tdirs = self.registry.queryUtility(ITranslationDirectories) + if tdirs is None: + tdirs = [] + self.registry.registerUtility(tdirs, ITranslationDirectories) + if override: + tdirs.extend(directories) + else: + for directory in reversed(directories): + tdirs.insert(0, directory) + + self.action(None, register, introspectables=introspectables) + diff --git a/src/pyramid/config/predicates.py b/src/pyramid/config/predicates.py new file mode 100644 index 000000000..bda763161 --- /dev/null +++ b/src/pyramid/config/predicates.py @@ -0,0 +1,2 @@ +import zope.deprecation +zope.deprecation.moved('pyramid.predicates', 'Pyramid 1.10') diff --git a/src/pyramid/config/rendering.py b/src/pyramid/config/rendering.py new file mode 100644 index 000000000..0d55c41e8 --- /dev/null +++ b/src/pyramid/config/rendering.py @@ -0,0 +1,51 @@ +from pyramid.interfaces import ( + IRendererFactory, + PHASE1_CONFIG, + ) + +from pyramid import renderers +from pyramid.config.util import action_method + +DEFAULT_RENDERERS = ( + ('json', renderers.json_renderer_factory), + ('string', renderers.string_renderer_factory), + ) + +class RenderingConfiguratorMixin(object): + def add_default_renderers(self): + for name, renderer in DEFAULT_RENDERERS: + self.add_renderer(name, renderer) + + @action_method + def add_renderer(self, name, factory): + """ + Add a :app:`Pyramid` :term:`renderer` factory to the + current configuration state. + + The ``name`` argument is the renderer name. Use ``None`` to + represent the default renderer (a renderer which will be used for all + views unless they name another renderer specifically). + + The ``factory`` argument is Python reference to an + implementation of a :term:`renderer` factory or a + :term:`dotted Python name` to same. + """ + factory = self.maybe_dotted(factory) + # if name is None or the empty string, we're trying to register + # a default renderer, but registerUtility is too dumb to accept None + # as a name + if not name: + name = '' + def register(): + self.registry.registerUtility(factory, IRendererFactory, name=name) + intr = self.introspectable('renderer factories', + name, + self.object_description(factory), + 'renderer factory') + intr['factory'] = factory + intr['name'] = name + # we need to register renderers early (in phase 1) because they are + # used during view configuration (which happens in phase 3) + self.action((IRendererFactory, name), register, order=PHASE1_CONFIG, + introspectables=(intr,)) + diff --git a/src/pyramid/config/routes.py b/src/pyramid/config/routes.py new file mode 100644 index 000000000..5d05429a7 --- /dev/null +++ b/src/pyramid/config/routes.py @@ -0,0 +1,560 @@ +import contextlib +import warnings + +from pyramid.compat import urlparse +from pyramid.interfaces import ( + IRequest, + IRouteRequest, + IRoutesMapper, + PHASE2_CONFIG, + ) + +from pyramid.exceptions import ConfigurationError +from pyramid.request import route_request_iface +from pyramid.urldispatch import RoutesMapper + +from pyramid.util import ( + as_sorted_tuple, + is_nonstr_iter, +) + +import pyramid.predicates + +from pyramid.config.util import ( + action_method, + normalize_accept_offer, + predvalseq, +) + +class RoutesConfiguratorMixin(object): + @action_method + def add_route(self, + name, + pattern=None, + factory=None, + for_=None, + header=None, + xhr=None, + accept=None, + path_info=None, + request_method=None, + request_param=None, + traverse=None, + custom_predicates=(), + use_global_views=False, + path=None, + pregenerator=None, + static=False, + **predicates): + """ Add a :term:`route configuration` to the current + configuration state, as well as possibly a :term:`view + configuration` to be used to specify a :term:`view callable` + that will be invoked when this route matches. The arguments + to this method are divided into *predicate*, *non-predicate*, + and *view-related* types. :term:`Route predicate` arguments + narrow the circumstances in which a route will be match a + request; non-predicate arguments are informational. + + Non-Predicate Arguments + + name + + The name of the route, e.g. ``myroute``. This attribute is + required. It must be unique among all defined routes in a given + application. + + factory + + A Python object (often a function or a class) or a :term:`dotted + Python name` which refers to the same object that will generate a + :app:`Pyramid` root resource object when this route matches. For + example, ``mypackage.resources.MyFactory``. If this argument is + not specified, a default root factory will be used. See + :ref:`the_resource_tree` for more information about root factories. + + traverse + + If you would like to cause the :term:`context` to be + something other than the :term:`root` object when this route + matches, you can spell a traversal pattern as the + ``traverse`` argument. This traversal pattern will be used + as the traversal path: traversal will begin at the root + object implied by this route (either the global root, or the + object returned by the ``factory`` associated with this + route). + + The syntax of the ``traverse`` argument is the same as it is + for ``pattern``. For example, if the ``pattern`` provided to + ``add_route`` is ``articles/{article}/edit``, and the + ``traverse`` argument provided to ``add_route`` is + ``/{article}``, when a request comes in that causes the route + to match in such a way that the ``article`` match value is + ``'1'`` (when the request URI is ``/articles/1/edit``), the + traversal path will be generated as ``/1``. This means that + the root object's ``__getitem__`` will be called with the + name ``'1'`` during the traversal phase. If the ``'1'`` object + exists, it will become the :term:`context` of the request. + :ref:`traversal_chapter` has more information about + traversal. + + If the traversal path contains segment marker names which + are not present in the ``pattern`` argument, a runtime error + will occur. The ``traverse`` pattern should not contain + segment markers that do not exist in the ``pattern`` + argument. + + A similar combining of routing and traversal is available + when a route is matched which contains a ``*traverse`` + remainder marker in its pattern (see + :ref:`using_traverse_in_a_route_pattern`). The ``traverse`` + argument to add_route allows you to associate route patterns + with an arbitrary traversal path without using a + ``*traverse`` remainder marker; instead you can use other + match information. + + Note that the ``traverse`` argument to ``add_route`` is + ignored when attached to a route that has a ``*traverse`` + remainder marker in its pattern. + + pregenerator + + This option should be a callable object that implements the + :class:`pyramid.interfaces.IRoutePregenerator` interface. A + :term:`pregenerator` is a callable called by the + :meth:`pyramid.request.Request.route_url` function to augment or + replace the arguments it is passed when generating a URL for the + route. This is a feature not often used directly by applications, + it is meant to be hooked by frameworks that use :app:`Pyramid` as + a base. + + use_global_views + + When a request matches this route, and view lookup cannot + find a view which has a ``route_name`` predicate argument + that matches the route, try to fall back to using a view + that otherwise matches the context, request, and view name + (but which does not match the route_name predicate). + + static + + If ``static`` is ``True``, this route will never match an incoming + request; it will only be useful for URL generation. By default, + ``static`` is ``False``. See :ref:`static_route_narr`. + + .. versionadded:: 1.1 + + Predicate Arguments + + pattern + + The pattern of the route e.g. ``ideas/{idea}``. This + argument is required. See :ref:`route_pattern_syntax` + for information about the syntax of route patterns. If the + pattern doesn't match the current URL, route matching + continues. + + .. note:: + + For backwards compatibility purposes (as of :app:`Pyramid` 1.0), a + ``path`` keyword argument passed to this function will be used to + represent the pattern value if the ``pattern`` argument is + ``None``. If both ``path`` and ``pattern`` are passed, ``pattern`` + wins. + + xhr + + This value should be either ``True`` or ``False``. If this + value is specified and is ``True``, the :term:`request` must + possess an ``HTTP_X_REQUESTED_WITH`` (aka + ``X-Requested-With``) header for this route to match. This + is useful for detecting AJAX requests issued from jQuery, + Prototype and other Javascript libraries. If this predicate + returns ``False``, route matching continues. + + request_method + + A string representing an HTTP method name, e.g. ``GET``, ``POST``, + ``HEAD``, ``DELETE``, ``PUT`` or a tuple of elements containing + HTTP method names. If this argument is not specified, this route + will match if the request has *any* request method. If this + predicate returns ``False``, route matching continues. + + .. versionchanged:: 1.2 + The ability to pass a tuple of items as ``request_method``. + Previous versions allowed only a string. + + path_info + + This value represents a regular expression pattern that will + be tested against the ``PATH_INFO`` WSGI environment + variable. If the regex matches, this predicate will return + ``True``. If this predicate returns ``False``, route + matching continues. + + request_param + + This value can be any string or an iterable of strings. A view + declaration with this argument ensures that the associated route will + only match when the request has a key in the ``request.params`` + dictionary (an HTTP ``GET`` or ``POST`` variable) that has a + name which matches the supplied value. If the value + supplied as the argument has a ``=`` sign in it, + e.g. ``request_param="foo=123"``, then the key + (``foo``) must both exist in the ``request.params`` dictionary, and + the value must match the right hand side of the expression (``123``) + for the route to "match" the current request. If this predicate + returns ``False``, route matching continues. + + header + + This argument represents an HTTP header name or a header + name/value pair. If the argument contains a ``:`` (colon), + it will be considered a name/value pair + (e.g. ``User-Agent:Mozilla/.*`` or ``Host:localhost``). If + the value contains a colon, the value portion should be a + regular expression. If the value does not contain a colon, + the entire value will be considered to be the header name + (e.g. ``If-Modified-Since``). If the value evaluates to a + header name only without a value, the header specified by + the name must be present in the request for this predicate + to be true. If the value evaluates to a header name/value + pair, the header specified by the name must be present in + the request *and* the regular expression specified as the + value must match the header value. Whether or not the value + represents a header name or a header name/value pair, the + case of the header name is not significant. If this + predicate returns ``False``, route matching continues. + + accept + + A :term:`media type` that will be matched against the ``Accept`` + HTTP request header. If this value is specified, it may be a + specific media type such as ``text/html``, or a list of the same. + If the media type is acceptable by the ``Accept`` header of the + request, or if the ``Accept`` header isn't set at all in the request, + this predicate will match. If this does not match the ``Accept`` + header of the request, route matching continues. + + If ``accept`` is not specified, the ``HTTP_ACCEPT`` HTTP header is + not taken into consideration when deciding whether or not to select + the route. + + Unlike the ``accept`` argument to + :meth:`pyramid.config.Configurator.add_view`, this value is + strictly a predicate and supports :func:`pyramid.config.not_`. + + .. versionchanged:: 1.10 + + Specifying a media range is deprecated due to changes in WebOb + and ambiguities that occur when trying to match ranges against + ranges in the ``Accept`` header. Support will be removed in + :app:`Pyramid` 2.0. Use a list of specific media types to match + more than one type. + + effective_principals + + If specified, this value should be a :term:`principal` identifier or + a sequence of principal identifiers. If the + :attr:`pyramid.request.Request.effective_principals` property + indicates that every principal named in the argument list is present + in the current request, this predicate will return True; otherwise it + will return False. For example: + ``effective_principals=pyramid.security.Authenticated`` or + ``effective_principals=('fred', 'group:admins')``. + + .. versionadded:: 1.4a4 + + custom_predicates + + .. deprecated:: 1.5 + This value should be a sequence of references to custom + predicate callables. Use custom predicates when no set of + predefined predicates does what you need. Custom predicates + can be combined with predefined predicates as necessary. + Each custom predicate callable should accept two arguments: + ``info`` and ``request`` and should return either ``True`` + or ``False`` after doing arbitrary evaluation of the info + and/or the request. If all custom and non-custom predicate + callables return ``True`` the associated route will be + considered viable for a given request. If any predicate + callable returns ``False``, route matching continues. Note + that the value ``info`` passed to a custom route predicate + is a dictionary containing matching information; see + :ref:`custom_route_predicates` for more information about + ``info``. + + predicates + + Pass a key/value pair here to use a third-party predicate + registered via + :meth:`pyramid.config.Configurator.add_route_predicate`. More than + one key/value pair can be used at the same time. See + :ref:`view_and_route_predicates` for more information about + third-party predicates. + + .. versionadded:: 1.4 + + """ + if custom_predicates: + warnings.warn( + ('The "custom_predicates" argument to Configurator.add_route ' + 'is deprecated as of Pyramid 1.5. Use ' + '"config.add_route_predicate" and use the registered ' + 'route predicate as a predicate argument to add_route ' + 'instead. See "Adding A Third Party View, Route, or ' + 'Subscriber Predicate" in the "Hooks" chapter of the ' + 'documentation for more information.'), + DeprecationWarning, + stacklevel=3 + ) + + if accept is not None: + if not is_nonstr_iter(accept): + if '*' in accept: + warnings.warn( + ('Passing a media range to the "accept" argument of ' + 'Configurator.add_route is deprecated as of Pyramid ' + '1.10. Use a list of explicit media types.'), + DeprecationWarning, + stacklevel=3, + ) + # XXX switch this to False when range support is dropped + accept = [normalize_accept_offer(accept, allow_range=True)] + + else: + accept = [ + normalize_accept_offer(accept_option) + for accept_option in accept + ] + + # these are route predicates; if they do not match, the next route + # in the routelist will be tried + if request_method is not None: + request_method = as_sorted_tuple(request_method) + + factory = self.maybe_dotted(factory) + if pattern is None: + pattern = path + if pattern is None: + raise ConfigurationError('"pattern" argument may not be None') + + # check for an external route; an external route is one which is + # is a full url (e.g. 'http://example.com/{id}') + parsed = urlparse.urlparse(pattern) + external_url = pattern + + if parsed.hostname: + pattern = parsed.path + + original_pregenerator = pregenerator + def external_url_pregenerator(request, elements, kw): + if '_app_url' in kw: + raise ValueError( + 'You cannot generate a path to an external route ' + 'pattern via request.route_path nor pass an _app_url ' + 'to request.route_url when generating a URL for an ' + 'external route pattern (pattern was "%s") ' % + (pattern,) + ) + if '_scheme' in kw: + scheme = kw['_scheme'] + elif parsed.scheme: + scheme = parsed.scheme + else: + scheme = request.scheme + kw['_app_url'] = '{0}://{1}'.format(scheme, parsed.netloc) + + if original_pregenerator: + elements, kw = original_pregenerator( + request, elements, kw) + return elements, kw + + pregenerator = external_url_pregenerator + static = True + + elif self.route_prefix: + pattern = self.route_prefix.rstrip('/') + '/' + pattern.lstrip('/') + + mapper = self.get_routes_mapper() + + introspectables = [] + + intr = self.introspectable('routes', + name, + '%s (pattern: %r)' % (name, pattern), + 'route') + intr['name'] = name + intr['pattern'] = pattern + intr['factory'] = factory + intr['xhr'] = xhr + intr['request_methods'] = request_method + intr['path_info'] = path_info + intr['request_param'] = request_param + intr['header'] = header + intr['accept'] = accept + intr['traverse'] = traverse + intr['custom_predicates'] = custom_predicates + intr['pregenerator'] = pregenerator + intr['static'] = static + intr['use_global_views'] = use_global_views + + if static is True: + intr['external_url'] = external_url + + introspectables.append(intr) + + if factory: + factory_intr = self.introspectable('root factories', + name, + self.object_description(factory), + 'root factory') + factory_intr['factory'] = factory + factory_intr['route_name'] = name + factory_intr.relate('routes', name) + introspectables.append(factory_intr) + + def register_route_request_iface(): + request_iface = self.registry.queryUtility(IRouteRequest, name=name) + if request_iface is None: + if use_global_views: + bases = (IRequest,) + else: + bases = () + request_iface = route_request_iface(name, bases) + self.registry.registerUtility( + request_iface, IRouteRequest, name=name) + + def register_connect(): + pvals = predicates.copy() + pvals.update( + dict( + xhr=xhr, + request_method=request_method, + path_info=path_info, + request_param=request_param, + header=header, + accept=accept, + traverse=traverse, + custom=predvalseq(custom_predicates), + ) + ) + + predlist = self.get_predlist('route') + _, preds, _ = predlist.make(self, **pvals) + route = mapper.connect( + name, pattern, factory, predicates=preds, + pregenerator=pregenerator, static=static + ) + intr['object'] = route + return route + + # We have to connect routes in the order they were provided; + # we can't use a phase to do that, because when the actions are + # sorted, actions in the same phase lose relative ordering + self.action(('route-connect', name), register_connect) + + # But IRouteRequest interfaces must be registered before we begin to + # process view registrations (in phase 3) + self.action(('route', name), register_route_request_iface, + order=PHASE2_CONFIG, introspectables=introspectables) + + @action_method + def add_route_predicate(self, name, factory, weighs_more_than=None, + weighs_less_than=None): + """ Adds a route predicate factory. The view predicate can later be + named as a keyword argument to + :meth:`pyramid.config.Configurator.add_route`. + + ``name`` should be the name of the predicate. It must be a valid + Python identifier (it will be used as a keyword argument to + ``add_route``). + + ``factory`` should be a :term:`predicate factory` or :term:`dotted + Python name` which refers to a predicate factory. + + See :ref:`view_and_route_predicates` for more information. + + .. versionadded:: 1.4 + """ + self._add_predicate( + 'route', + name, + factory, + weighs_more_than=weighs_more_than, + weighs_less_than=weighs_less_than + ) + + def add_default_route_predicates(self): + p = pyramid.predicates + for (name, factory) in ( + ('xhr', p.XHRPredicate), + ('request_method', p.RequestMethodPredicate), + ('path_info', p.PathInfoPredicate), + ('request_param', p.RequestParamPredicate), + ('header', p.HeaderPredicate), + ('accept', p.AcceptPredicate), + ('effective_principals', p.EffectivePrincipalsPredicate), + ('custom', p.CustomPredicate), + ('traverse', p.TraversePredicate), + ): + self.add_route_predicate(name, factory) + + def get_routes_mapper(self): + """ Return the :term:`routes mapper` object associated with + this configurator's :term:`registry`.""" + mapper = self.registry.queryUtility(IRoutesMapper) + if mapper is None: + mapper = RoutesMapper() + self.registry.registerUtility(mapper, IRoutesMapper) + return mapper + + @contextlib.contextmanager + def route_prefix_context(self, route_prefix): + """ Return this configurator with the + :attr:`pyramid.config.Configurator.route_prefix` attribute mutated to + include the new ``route_prefix``. + + When the context exits, the ``route_prefix`` is reset to the original. + + Example Usage: + + >>> config = Configurator() + >>> with config.route_prefix_context('foo'): + ... config.add_route('bar', '/bar') + + Arguments + + route_prefix + + A string suitable to be used as a route prefix, or ``None``. + + .. versionadded:: 1.10 + """ + + original_route_prefix = self.route_prefix + + if route_prefix is None: + route_prefix = '' + + old_route_prefix = self.route_prefix + if old_route_prefix is None: + old_route_prefix = '' + + route_prefix = '{}/{}'.format( + old_route_prefix.rstrip('/'), + route_prefix.lstrip('/'), + ) + + route_prefix = route_prefix.strip('/') + + if not route_prefix: + route_prefix = None + + self.begin() + try: + self.route_prefix = route_prefix + yield + + finally: + self.route_prefix = original_route_prefix + self.end() diff --git a/src/pyramid/config/security.py b/src/pyramid/config/security.py new file mode 100644 index 000000000..c7afbcf4e --- /dev/null +++ b/src/pyramid/config/security.py @@ -0,0 +1,265 @@ +from zope.interface import implementer + +from pyramid.interfaces import ( + IAuthorizationPolicy, + IAuthenticationPolicy, + ICSRFStoragePolicy, + IDefaultCSRFOptions, + IDefaultPermission, + PHASE1_CONFIG, + PHASE2_CONFIG, + ) + +from pyramid.csrf import LegacySessionCSRFStoragePolicy +from pyramid.exceptions import ConfigurationError +from pyramid.util import as_sorted_tuple + +from pyramid.config.util import action_method + +class SecurityConfiguratorMixin(object): + + def add_default_security(self): + self.set_csrf_storage_policy(LegacySessionCSRFStoragePolicy()) + + @action_method + def set_authentication_policy(self, policy): + """ Override the :app:`Pyramid` :term:`authentication policy` in the + current configuration. The ``policy`` argument must be an instance + of an authentication policy or a :term:`dotted Python name` + that points at an instance of an authentication policy. + + .. note:: + + Using the ``authentication_policy`` argument to the + :class:`pyramid.config.Configurator` constructor can be used to + achieve the same purpose. + + """ + def register(): + self._set_authentication_policy(policy) + if self.registry.queryUtility(IAuthorizationPolicy) is None: + raise ConfigurationError( + 'Cannot configure an authentication policy without ' + 'also configuring an authorization policy ' + '(use the set_authorization_policy method)') + intr = self.introspectable('authentication policy', None, + self.object_description(policy), + 'authentication policy') + intr['policy'] = policy + # authentication policy used by view config (phase 3) + self.action(IAuthenticationPolicy, register, order=PHASE2_CONFIG, + introspectables=(intr,)) + + def _set_authentication_policy(self, policy): + policy = self.maybe_dotted(policy) + self.registry.registerUtility(policy, IAuthenticationPolicy) + + @action_method + def set_authorization_policy(self, policy): + """ Override the :app:`Pyramid` :term:`authorization policy` in the + current configuration. The ``policy`` argument must be an instance + of an authorization policy or a :term:`dotted Python name` that points + at an instance of an authorization policy. + + .. note:: + + Using the ``authorization_policy`` argument to the + :class:`pyramid.config.Configurator` constructor can be used to + achieve the same purpose. + """ + def register(): + self._set_authorization_policy(policy) + def ensure(): + if self.autocommit: + return + if self.registry.queryUtility(IAuthenticationPolicy) is None: + raise ConfigurationError( + 'Cannot configure an authorization policy without ' + 'also configuring an authentication policy ' + '(use the set_authorization_policy method)') + + intr = self.introspectable('authorization policy', None, + self.object_description(policy), + 'authorization policy') + intr['policy'] = policy + # authorization policy used by view config (phase 3) and + # authentication policy (phase 2) + self.action(IAuthorizationPolicy, register, order=PHASE1_CONFIG, + introspectables=(intr,)) + self.action(None, ensure) + + def _set_authorization_policy(self, policy): + policy = self.maybe_dotted(policy) + self.registry.registerUtility(policy, IAuthorizationPolicy) + + @action_method + def set_default_permission(self, permission): + """ + Set the default permission to be used by all subsequent + :term:`view configuration` registrations. ``permission`` + should be a :term:`permission` string to be used as the + default permission. An example of a permission + string:``'view'``. Adding a default permission makes it + unnecessary to protect each view configuration with an + explicit permission, unless your application policy requires + some exception for a particular view. + + If a default permission is *not* set, views represented by + view configuration registrations which do not explicitly + declare a permission will be executable by entirely anonymous + users (any authorization policy is ignored). + + Later calls to this method override will conflict with earlier calls; + there can be only one default permission active at a time within an + application. + + .. warning:: + + If a default permission is in effect, view configurations meant to + create a truly anonymously accessible view (even :term:`exception + view` views) *must* use the value of the permission importable as + :data:`pyramid.security.NO_PERMISSION_REQUIRED`. When this string + is used as the ``permission`` for a view configuration, the default + permission is ignored, and the view is registered, making it + available to all callers regardless of their credentials. + + .. seealso:: + + See also :ref:`setting_a_default_permission`. + + .. note:: + + Using the ``default_permission`` argument to the + :class:`pyramid.config.Configurator` constructor can be used to + achieve the same purpose. + """ + def register(): + self.registry.registerUtility(permission, IDefaultPermission) + intr = self.introspectable('default permission', + None, + permission, + 'default permission') + intr['value'] = permission + perm_intr = self.introspectable('permissions', + permission, + permission, + 'permission') + perm_intr['value'] = permission + # default permission used during view registration (phase 3) + self.action(IDefaultPermission, register, order=PHASE1_CONFIG, + introspectables=(intr, perm_intr,)) + + def add_permission(self, permission_name): + """ + A configurator directive which registers a free-standing + permission without associating it with a view callable. This can be + used so that the permission shows up in the introspectable data under + the ``permissions`` category (permissions mentioned via ``add_view`` + already end up in there). For example:: + + config = Configurator() + config.add_permission('view') + """ + intr = self.introspectable( + 'permissions', + permission_name, + permission_name, + 'permission' + ) + intr['value'] = permission_name + self.action(None, introspectables=(intr,)) + + @action_method + def set_default_csrf_options( + self, + require_csrf=True, + token='csrf_token', + header='X-CSRF-Token', + safe_methods=('GET', 'HEAD', 'OPTIONS', 'TRACE'), + callback=None, + ): + """ + Set the default CSRF options used by subsequent view registrations. + + ``require_csrf`` controls whether CSRF checks will be automatically + enabled on each view in the application. This value is used as the + fallback when ``require_csrf`` is left at the default of ``None`` on + :meth:`pyramid.config.Configurator.add_view`. + + ``token`` is the name of the CSRF token used in the body of the + request, accessed via ``request.POST[token]``. Default: ``csrf_token``. + + ``header`` is the name of the header containing the CSRF token, + accessed via ``request.headers[header]``. Default: ``X-CSRF-Token``. + + If ``token`` or ``header`` are set to ``None`` they will not be used + for checking CSRF tokens. + + ``safe_methods`` is an iterable of HTTP methods which are expected to + not contain side-effects as defined by RFC2616. Safe methods will + never be automatically checked for CSRF tokens. + Default: ``('GET', 'HEAD', 'OPTIONS', TRACE')``. + + If ``callback`` is set, it must be a callable accepting ``(request)`` + and returning ``True`` if the request should be checked for a valid + CSRF token. This callback allows an application to support + alternate authentication methods that do not rely on cookies which + are not subject to CSRF attacks. For example, if a request is + authenticated using the ``Authorization`` header instead of a cookie, + this may return ``False`` for that request so that clients do not + need to send the ``X-CSRF-Token`` header. The callback is only tested + for non-safe methods as defined by ``safe_methods``. + + .. versionadded:: 1.7 + + .. versionchanged:: 1.8 + Added the ``callback`` option. + + """ + options = DefaultCSRFOptions( + require_csrf, token, header, safe_methods, callback, + ) + def register(): + self.registry.registerUtility(options, IDefaultCSRFOptions) + intr = self.introspectable('default csrf view options', + None, + options, + 'default csrf view options') + intr['require_csrf'] = require_csrf + intr['token'] = token + intr['header'] = header + intr['safe_methods'] = as_sorted_tuple(safe_methods) + intr['callback'] = callback + + self.action(IDefaultCSRFOptions, register, order=PHASE1_CONFIG, + introspectables=(intr,)) + + @action_method + def set_csrf_storage_policy(self, policy): + """ + Set the :term:`CSRF storage policy` used by subsequent view + registrations. + + ``policy`` is a class that implements the + :meth:`pyramid.interfaces.ICSRFStoragePolicy` interface and defines + how to generate and persist CSRF tokens. + + """ + def register(): + self.registry.registerUtility(policy, ICSRFStoragePolicy) + intr = self.introspectable('csrf storage policy', + None, + policy, + 'csrf storage policy') + intr['policy'] = policy + self.action(ICSRFStoragePolicy, register, introspectables=(intr,)) + + +@implementer(IDefaultCSRFOptions) +class DefaultCSRFOptions(object): + def __init__(self, require_csrf, token, header, safe_methods, callback): + self.require_csrf = require_csrf + self.token = token + self.header = header + self.safe_methods = frozenset(safe_methods) + self.callback = callback diff --git a/src/pyramid/config/settings.py b/src/pyramid/config/settings.py new file mode 100644 index 000000000..11a1f7d8c --- /dev/null +++ b/src/pyramid/config/settings.py @@ -0,0 +1,107 @@ +import os + +from pyramid.settings import asbool, aslist + +class SettingsConfiguratorMixin(object): + def _set_settings(self, mapping): + if mapping is None: + mapping = {} + settings = Settings(mapping) + self.registry.settings = settings + return settings + + def add_settings(self, settings=None, **kw): + """Augment the :term:`deployment settings` with one or more + key/value pairs. + + You may pass a dictionary:: + + config.add_settings({'external_uri':'http://example.com'}) + + Or a set of key/value pairs:: + + config.add_settings(external_uri='http://example.com') + + This function is useful when you need to test code that accesses the + :attr:`pyramid.registry.Registry.settings` API (or the + :meth:`pyramid.config.Configurator.get_settings` API) and + which uses values from that API. + """ + if settings is None: + settings = {} + utility = self.registry.settings + if utility is None: + utility = self._set_settings(settings) + utility.update(settings) + utility.update(kw) + + def get_settings(self): + """ + Return a :term:`deployment settings` object for the current + application. A deployment settings object is a dictionary-like + object that contains key/value pairs based on the dictionary passed + as the ``settings`` argument to the + :class:`pyramid.config.Configurator` constructor. + + .. note:: the :attr:`pyramid.registry.Registry.settings` API + performs the same duty. + """ + return self.registry.settings + + +def Settings(d=None, _environ_=os.environ, **kw): + """ Deployment settings. Update application settings (usually + from PasteDeploy keywords) with framework-specific key/value pairs + (e.g. find ``PYRAMID_DEBUG_AUTHORIZATION`` in os.environ and jam into + keyword args).""" + if d is None: + d = {} + d = dict(d) + d.update(**kw) + + eget = _environ_.get + def expand_key(key): + keys = [key] + if not key.startswith('pyramid.'): + keys.append('pyramid.' + key) + return keys + def S(settings_key, env_key=None, type_=str, default=False): + value = default + keys = expand_key(settings_key) + for key in keys: + value = d.get(key, value) + if env_key: + value = eget(env_key, value) + value = type_(value) + d.update({k: value for k in keys}) + def O(settings_key, override_key): # noqa: E743 + for key in expand_key(settings_key): + d[key] = d[key] or d[override_key] + + S('debug_all', 'PYRAMID_DEBUG_ALL', asbool) + S('debug_authorization', 'PYRAMID_DEBUG_AUTHORIZATION', asbool) + O('debug_authorization', 'debug_all') + S('debug_notfound', 'PYRAMID_DEBUG_NOTFOUND', asbool) + O('debug_notfound', 'debug_all') + S('debug_routematch', 'PYRAMID_DEBUG_ROUTEMATCH', asbool) + O('debug_routematch', 'debug_all') + S('debug_templates', 'PYRAMID_DEBUG_TEMPLATES', asbool) + O('debug_templates', 'debug_all') + + S('reload_all', 'PYRAMID_RELOAD_ALL', asbool) + S('reload_templates', 'PYRAMID_RELOAD_TEMPLATES', asbool) + O('reload_templates', 'reload_all') + S('reload_assets', 'PYRAMID_RELOAD_ASSETS', asbool) + O('reload_assets', 'reload_all') + S('reload_resources', 'PYRAMID_RELOAD_RESOURCES', asbool) + O('reload_resources', 'reload_all') + # reload_resources is an older alias for reload_assets + for k in expand_key('reload_assets') + expand_key('reload_resources'): + d[k] = d['reload_assets'] or d['reload_resources'] + + S('default_locale_name', 'PYRAMID_DEFAULT_LOCALE_NAME', str, 'en') + S('prevent_http_cache', 'PYRAMID_PREVENT_HTTP_CACHE', asbool) + S('prevent_cachebust', 'PYRAMID_PREVENT_CACHEBUST', asbool) + S('csrf_trusted_origins', 'PYRAMID_CSRF_TRUSTED_ORIGINS', aslist, []) + + return d diff --git a/src/pyramid/config/testing.py b/src/pyramid/config/testing.py new file mode 100644 index 000000000..1daf5cdeb --- /dev/null +++ b/src/pyramid/config/testing.py @@ -0,0 +1,167 @@ +from zope.interface import Interface + +from pyramid.interfaces import ( + ITraverser, + IAuthorizationPolicy, + IAuthenticationPolicy, + IRendererFactory, + ) + +from pyramid.renderers import RendererHelper + +from pyramid.traversal import ( + decode_path_info, + split_path_info, + ) + +from pyramid.config.util import action_method + +class TestingConfiguratorMixin(object): + # testing API + def testing_securitypolicy(self, userid=None, groupids=(), + permissive=True, remember_result=None, + forget_result=None): + """Unit/integration testing helper: Registers a pair of faux + :app:`Pyramid` security policies: a :term:`authentication + policy` and a :term:`authorization policy`. + + The behavior of the registered :term:`authorization policy` + depends on the ``permissive`` argument. If ``permissive`` is + true, a permissive :term:`authorization policy` is registered; + this policy allows all access. If ``permissive`` is false, a + nonpermissive :term:`authorization policy` is registered; this + policy denies all access. + + ``remember_result``, if provided, should be the result returned by + the ``remember`` method of the faux authentication policy. If it is + not provided (or it is provided, and is ``None``), the default value + ``[]`` (the empty list) will be returned by ``remember``. + + ``forget_result``, if provided, should be the result returned by + the ``forget`` method of the faux authentication policy. If it is + not provided (or it is provided, and is ``None``), the default value + ``[]`` (the empty list) will be returned by ``forget``. + + The behavior of the registered :term:`authentication policy` + depends on the values provided for the ``userid`` and + ``groupids`` argument. The authentication policy will return + the userid identifier implied by the ``userid`` argument and + the group ids implied by the ``groupids`` argument when the + :attr:`pyramid.request.Request.authenticated_userid` or + :attr:`pyramid.request.Request.effective_principals` APIs are + used. + + This function is most useful when testing code that uses + the APIs named :meth:`pyramid.request.Request.has_permission`, + :attr:`pyramid.request.Request.authenticated_userid`, + :attr:`pyramid.request.Request.effective_principals`, and + :func:`pyramid.security.principals_allowed_by_permission`. + + .. versionadded:: 1.4 + The ``remember_result`` argument. + + .. versionadded:: 1.4 + The ``forget_result`` argument. + """ + from pyramid.testing import DummySecurityPolicy + policy = DummySecurityPolicy( + userid, groupids, permissive, remember_result, forget_result + ) + self.registry.registerUtility(policy, IAuthorizationPolicy) + self.registry.registerUtility(policy, IAuthenticationPolicy) + return policy + + def testing_resources(self, resources): + """Unit/integration testing helper: registers a dictionary of + :term:`resource` objects that can be resolved via the + :func:`pyramid.traversal.find_resource` API. + + The :func:`pyramid.traversal.find_resource` API is called with + a path as one of its arguments. If the dictionary you + register when calling this method contains that path as a + string key (e.g. ``/foo/bar`` or ``foo/bar``), the + corresponding value will be returned to ``find_resource`` (and + thus to your code) when + :func:`pyramid.traversal.find_resource` is called with an + equivalent path string or tuple. + """ + class DummyTraverserFactory: + def __init__(self, context): + self.context = context + + def __call__(self, request): + path = decode_path_info(request.environ['PATH_INFO']) + ob = resources[path] + traversed = split_path_info(path) + return {'context':ob, 'view_name':'','subpath':(), + 'traversed':traversed, 'virtual_root':ob, + 'virtual_root_path':(), 'root':ob} + self.registry.registerAdapter(DummyTraverserFactory, (Interface,), + ITraverser) + return resources + + testing_models = testing_resources # b/w compat + + @action_method + def testing_add_subscriber(self, event_iface=None): + """Unit/integration testing helper: Registers a + :term:`subscriber` which listens for events of the type + ``event_iface``. This method returns a list object which is + appended to by the subscriber whenever an event is captured. + + When an event is dispatched that matches the value implied by + the ``event_iface`` argument, that event will be appended to + the list. You can then compare the values in the list to + expected event notifications. This method is useful when + testing code that wants to call + :meth:`pyramid.registry.Registry.notify`, + or :func:`zope.component.event.dispatch`. + + The default value of ``event_iface`` (``None``) implies a + subscriber registered for *any* kind of event. + """ + event_iface = self.maybe_dotted(event_iface) + L = [] + def subscriber(*event): + L.extend(event) + self.add_subscriber(subscriber, event_iface) + return L + + def testing_add_renderer(self, path, renderer=None): + """Unit/integration testing helper: register a renderer at + ``path`` (usually a relative filename ala ``templates/foo.pt`` + or an asset specification) and return the renderer object. + If the ``renderer`` argument is None, a 'dummy' renderer will + be used. This function is useful when testing code that calls + the :func:`pyramid.renderers.render` function or + :func:`pyramid.renderers.render_to_response` function or + any other ``render_*`` or ``get_*`` API of the + :mod:`pyramid.renderers` module. + + Note that calling this method for with a ``path`` argument + representing a renderer factory type (e.g. for ``foo.pt`` + usually implies the ``chameleon_zpt`` renderer factory) + clobbers any existing renderer factory registered for that + type. + + .. note:: This method is also available under the alias + ``testing_add_template`` (an older name for it). + + """ + from pyramid.testing import DummyRendererFactory + helper = RendererHelper(name=path, registry=self.registry) + factory = self.registry.queryUtility(IRendererFactory, name=helper.type) + if not isinstance(factory, DummyRendererFactory): + factory = DummyRendererFactory(helper.type, factory) + self.registry.registerUtility(factory, IRendererFactory, + name=helper.type) + + from pyramid.testing import DummyTemplateRenderer + if renderer is None: + renderer = DummyTemplateRenderer() + factory.add(path, renderer) + return renderer + + testing_add_template = testing_add_renderer + + diff --git a/src/pyramid/config/tweens.py b/src/pyramid/config/tweens.py new file mode 100644 index 000000000..8bf21cf71 --- /dev/null +++ b/src/pyramid/config/tweens.py @@ -0,0 +1,196 @@ +from zope.interface import implementer + +from pyramid.interfaces import ITweens + +from pyramid.compat import ( + string_types, + is_nonstr_iter, + ) + +from pyramid.exceptions import ConfigurationError + +from pyramid.tweens import ( + MAIN, + INGRESS, + EXCVIEW, + ) + +from pyramid.util import ( + is_string_or_iterable, + TopologicalSorter, + ) + +from pyramid.config.util import action_method + +class TweensConfiguratorMixin(object): + def add_tween(self, tween_factory, under=None, over=None): + """ + .. versionadded:: 1.2 + + Add a 'tween factory'. A :term:`tween` (a contraction of 'between') + is a bit of code that sits between the Pyramid router's main request + handling function and the upstream WSGI component that uses + :app:`Pyramid` as its 'app'. Tweens are a feature that may be used + by Pyramid framework extensions, to provide, for example, + Pyramid-specific view timing support, bookkeeping code that examines + exceptions before they are returned to the upstream WSGI application, + or a variety of other features. Tweens behave a bit like + :term:`WSGI` 'middleware' but they have the benefit of running in a + context in which they have access to the Pyramid :term:`application + registry` as well as the Pyramid rendering machinery. + + .. note:: You can view the tween ordering configured into a given + Pyramid application by using the ``ptweens`` + command. See :ref:`displaying_tweens`. + + The ``tween_factory`` argument must be a :term:`dotted Python name` + to a global object representing the tween factory. + + The ``under`` and ``over`` arguments allow the caller of + ``add_tween`` to provide a hint about where in the tween chain this + tween factory should be placed when an implicit tween chain is used. + These hints are only used when an explicit tween chain is not used + (when the ``pyramid.tweens`` configuration value is not set). + Allowable values for ``under`` or ``over`` (or both) are: + + - ``None`` (the default). + + - A :term:`dotted Python name` to a tween factory: a string + representing the dotted name of a tween factory added in a call to + ``add_tween`` in the same configuration session. + + - One of the constants :attr:`pyramid.tweens.MAIN`, + :attr:`pyramid.tweens.INGRESS`, or :attr:`pyramid.tweens.EXCVIEW`. + + - An iterable of any combination of the above. This allows the user + to specify fallbacks if the desired tween is not included, as well + as compatibility with multiple other tweens. + + ``under`` means 'closer to the main Pyramid application than', + ``over`` means 'closer to the request ingress than'. + + For example, calling ``add_tween('myapp.tfactory', + over=pyramid.tweens.MAIN)`` will attempt to place the tween factory + represented by the dotted name ``myapp.tfactory`` directly 'above' + (in ``ptweens`` order) the main Pyramid request handler. + Likewise, calling ``add_tween('myapp.tfactory', + over=pyramid.tweens.MAIN, under='mypkg.someothertween')`` will + attempt to place this tween factory 'above' the main handler but + 'below' (a fictional) 'mypkg.someothertween' tween factory. + + If all options for ``under`` (or ``over``) cannot be found in the + current configuration, it is an error. If some options are specified + purely for compatibilty with other tweens, just add a fallback of + MAIN or INGRESS. For example, ``under=('mypkg.someothertween', + 'mypkg.someothertween2', INGRESS)``. This constraint will require + the tween to be located under both the 'mypkg.someothertween' tween, + the 'mypkg.someothertween2' tween, and INGRESS. If any of these is + not in the current configuration, this constraint will only organize + itself based on the tweens that are present. + + Specifying neither ``over`` nor ``under`` is equivalent to specifying + ``under=INGRESS``. + + Implicit tween ordering is obviously only best-effort. Pyramid will + attempt to present an implicit order of tweens as best it can, but + the only surefire way to get any particular ordering is to use an + explicit tween order. A user may always override the implicit tween + ordering by using an explicit ``pyramid.tweens`` configuration value + setting. + + ``under``, and ``over`` arguments are ignored when an explicit tween + chain is specified using the ``pyramid.tweens`` configuration value. + + For more information, see :ref:`registering_tweens`. + + """ + return self._add_tween(tween_factory, under=under, over=over, + explicit=False) + + def add_default_tweens(self): + self.add_tween(EXCVIEW) + + @action_method + def _add_tween(self, tween_factory, under=None, over=None, explicit=False): + + if not isinstance(tween_factory, string_types): + raise ConfigurationError( + 'The "tween_factory" argument to add_tween must be a ' + 'dotted name to a globally importable object, not %r' % + tween_factory) + + name = tween_factory + + if name in (MAIN, INGRESS): + raise ConfigurationError('%s is a reserved tween name' % name) + + tween_factory = self.maybe_dotted(tween_factory) + + for t, p in [('over', over), ('under', under)]: + if p is not None: + if not is_string_or_iterable(p): + raise ConfigurationError( + '"%s" must be a string or iterable, not %s' % (t, p)) + + if over is INGRESS or is_nonstr_iter(over) and INGRESS in over: + raise ConfigurationError('%s cannot be over INGRESS' % name) + + if under is MAIN or is_nonstr_iter(under) and MAIN in under: + raise ConfigurationError('%s cannot be under MAIN' % name) + + registry = self.registry + introspectables = [] + + tweens = registry.queryUtility(ITweens) + if tweens is None: + tweens = Tweens() + registry.registerUtility(tweens, ITweens) + + def register(): + if explicit: + tweens.add_explicit(name, tween_factory) + else: + tweens.add_implicit(name, tween_factory, under=under, over=over) + + discriminator = ('tween', name, explicit) + tween_type = explicit and 'explicit' or 'implicit' + + intr = self.introspectable('tweens', + discriminator, + name, + '%s tween' % tween_type) + intr['name'] = name + intr['factory'] = tween_factory + intr['type'] = tween_type + intr['under'] = under + intr['over'] = over + introspectables.append(intr) + self.action(discriminator, register, introspectables=introspectables) + +@implementer(ITweens) +class Tweens(object): + def __init__(self): + self.sorter = TopologicalSorter( + default_before=None, + default_after=INGRESS, + first=INGRESS, + last=MAIN) + self.explicit = [] + + def add_explicit(self, name, factory): + self.explicit.append((name, factory)) + + def add_implicit(self, name, factory, under=None, over=None): + self.sorter.add(name, factory, after=under, before=over) + + def implicit(self): + return self.sorter.sorted() + + def __call__(self, handler, registry): + if self.explicit: + use = self.explicit + else: + use = self.implicit() + for name, factory in use[::-1]: + handler = factory(handler, registry) + return handler diff --git a/src/pyramid/config/util.py b/src/pyramid/config/util.py new file mode 100644 index 000000000..05d810f6f --- /dev/null +++ b/src/pyramid/config/util.py @@ -0,0 +1,281 @@ +import functools +from hashlib import md5 +import traceback +from webob.acceptparse import Accept +from zope.interface import implementer + +from pyramid.compat import ( + bytes_, + is_nonstr_iter +) +from pyramid.interfaces import IActionInfo + +from pyramid.exceptions import ConfigurationError +from pyramid.predicates import Notted +from pyramid.registry import predvalseq +from pyramid.util import ( + TopologicalSorter, + takes_one_arg, +) + +TopologicalSorter = TopologicalSorter # support bw-compat imports +takes_one_arg = takes_one_arg # support bw-compat imports + +@implementer(IActionInfo) +class ActionInfo(object): + def __init__(self, file, line, function, src): + self.file = file + self.line = line + self.function = function + self.src = src + + def __str__(self): + srclines = self.src.split('\n') + src = '\n'.join(' %s' % x for x in srclines) + return 'Line %s of file %s:\n%s' % (self.line, self.file, src) + +def action_method(wrapped): + """ Wrapper to provide the right conflict info report data when a method + that calls Configurator.action calls another that does the same. Not a + documented API but used by some external systems.""" + def wrapper(self, *arg, **kw): + if self._ainfo is None: + self._ainfo = [] + info = kw.pop('_info', None) + # backframes for outer decorators to actionmethods + backframes = kw.pop('_backframes', 0) + 2 + if is_nonstr_iter(info) and len(info) == 4: + # _info permitted as extract_stack tuple + info = ActionInfo(*info) + if info is None: + try: + f = traceback.extract_stack(limit=4) + + # Work around a Python 3.5 issue whereby it would insert an + # extra stack frame. This should no longer be necessary in + # Python 3.5.1 + last_frame = ActionInfo(*f[-1]) + if last_frame.function == 'extract_stack': # pragma: no cover + f.pop() + info = ActionInfo(*f[-backframes]) + except Exception: # pragma: no cover + info = ActionInfo(None, 0, '', '') + self._ainfo.append(info) + try: + result = wrapped(self, *arg, **kw) + finally: + self._ainfo.pop() + return result + + if hasattr(wrapped, '__name__'): + functools.update_wrapper(wrapper, wrapped) + wrapper.__docobj__ = wrapped + return wrapper + + +MAX_ORDER = 1 << 30 +DEFAULT_PHASH = md5().hexdigest() + + +class not_(object): + """ + + You can invert the meaning of any predicate value by wrapping it in a call + to :class:`pyramid.config.not_`. + + .. code-block:: python + :linenos: + + from pyramid.config import not_ + + config.add_view( + 'mypackage.views.my_view', + route_name='ok', + request_method=not_('POST') + ) + + The above example will ensure that the view is called if the request method + is *not* ``POST``, at least if no other view is more specific. + + This technique of wrapping a predicate value in ``not_`` can be used + anywhere predicate values are accepted: + + - :meth:`pyramid.config.Configurator.add_view` + + - :meth:`pyramid.config.Configurator.add_route` + + - :meth:`pyramid.config.Configurator.add_subscriber` + + - :meth:`pyramid.view.view_config` + + - :meth:`pyramid.events.subscriber` + + .. versionadded:: 1.5 + """ + def __init__(self, value): + self.value = value + + +# under = after +# over = before + +class PredicateList(object): + + def __init__(self): + self.sorter = TopologicalSorter() + self.last_added = None + + def add(self, name, factory, weighs_more_than=None, weighs_less_than=None): + # Predicates should be added to a predicate list in (presumed) + # computation expense order. + ## if weighs_more_than is None and weighs_less_than is None: + ## weighs_more_than = self.last_added or FIRST + ## weighs_less_than = LAST + self.last_added = name + self.sorter.add( + name, + factory, + after=weighs_more_than, + before=weighs_less_than, + ) + + def names(self): + # Return the list of valid predicate names. + return self.sorter.names + + def make(self, config, **kw): + # Given a configurator and a list of keywords, a predicate list is + # computed. Elsewhere in the code, we evaluate predicates using a + # generator expression. All predicates associated with a view or + # route must evaluate true for the view or route to "match" during a + # request. The fastest predicate should be evaluated first, then the + # next fastest, and so on, as if one returns false, the remainder of + # the predicates won't need to be evaluated. + # + # While we compute predicates, we also compute a predicate hash (aka + # phash) that can be used by a caller to identify identical predicate + # lists. + ordered = self.sorter.sorted() + phash = md5() + weights = [] + preds = [] + for n, (name, predicate_factory) in enumerate(ordered): + vals = kw.pop(name, None) + if vals is None: # XXX should this be a sentinel other than None? + continue + if not isinstance(vals, predvalseq): + vals = (vals,) + for val in vals: + realval = val + notted = False + if isinstance(val, not_): + realval = val.value + notted = True + pred = predicate_factory(realval, config) + if notted: + pred = Notted(pred) + hashes = pred.phash() + if not is_nonstr_iter(hashes): + hashes = [hashes] + for h in hashes: + phash.update(bytes_(h)) + weights.append(1 << n + 1) + preds.append(pred) + if kw: + from difflib import get_close_matches + closest = [] + names = [ name for name, _ in ordered ] + for name in kw: + closest.extend(get_close_matches(name, names, 3)) + + raise ConfigurationError( + 'Unknown predicate values: %r (did you mean %s)' + % (kw, ','.join(closest)) + ) + # A "order" is computed for the predicate list. An order is + # a scoring. + # + # Each predicate is associated with a weight value. The weight of a + # predicate symbolizes the relative potential "importance" of the + # predicate to all other predicates. A larger weight indicates + # greater importance. + # + # All weights for a given predicate list are bitwise ORed together + # to create a "score"; this score is then subtracted from + # MAX_ORDER and divided by an integer representing the number of + # predicates+1 to determine the order. + # + # For views, the order represents the ordering in which a "multiview" + # ( a collection of views that share the same context/request/name + # triad but differ in other ways via predicates) will attempt to call + # its set of views. Views with lower orders will be tried first. + # The intent is to a) ensure that views with more predicates are + # always evaluated before views with fewer predicates and b) to + # ensure a stable call ordering of views that share the same number + # of predicates. Views which do not have any predicates get an order + # of MAX_ORDER, meaning that they will be tried very last. + score = 0 + for bit in weights: + score = score | bit + order = (MAX_ORDER - score) / (len(preds) + 1) + return order, preds, phash.hexdigest() + + +def normalize_accept_offer(offer, allow_range=False): + if allow_range and '*' in offer: + return offer.lower() + return str(Accept.parse_offer(offer)) + + +def sort_accept_offers(offers, order=None): + """ + Sort a list of offers by preference. + + For a given ``type/subtype`` category of offers, this algorithm will + always sort offers with params higher than the bare offer. + + :param offers: A list of offers to be sorted. + :param order: A weighted list of offers where items closer to the start of + the list will be a preferred over items closer to the end. + :return: A list of offers sorted first by specificity (higher to lower) + then by ``order``. + + """ + if order is None: + order = [] + + max_weight = len(offers) + + def find_order_index(value, default=None): + return next((i for i, x in enumerate(order) if x == value), default) + + def offer_sort_key(value): + """ + (type_weight, params_weight) + + type_weight: + - index of specific ``type/subtype`` in order list + - ``max_weight * 2`` if no match is found + + params_weight: + - index of specific ``type/subtype;params`` in order list + - ``max_weight`` if not found + - ``max_weight + 1`` if no params at all + + """ + parsed = Accept.parse_offer(value) + + type_w = find_order_index( + parsed.type + '/' + parsed.subtype, + max_weight, + ) + + if parsed.params: + param_w = find_order_index(value, max_weight) + + else: + param_w = max_weight + 1 + + return (type_w, param_w) + + return sorted(offers, key=offer_sort_key) diff --git a/src/pyramid/config/views.py b/src/pyramid/config/views.py new file mode 100644 index 000000000..e6baa7c17 --- /dev/null +++ b/src/pyramid/config/views.py @@ -0,0 +1,2327 @@ +import functools +import inspect +import posixpath +import operator +import os +import warnings + +from webob.acceptparse import Accept +from zope.interface import ( + Interface, + implementedBy, + implementer, + ) +from zope.interface.interfaces import IInterface + +from pyramid.interfaces import ( + IAcceptOrder, + IExceptionViewClassifier, + IException, + IMultiView, + IPackageOverrides, + IRendererFactory, + IRequest, + IResponse, + IRouteRequest, + ISecuredView, + IStaticURLInfo, + IView, + IViewClassifier, + IViewDerivers, + IViewDeriverInfo, + IViewMapperFactory, + PHASE1_CONFIG, + ) + +from pyramid import renderers + +from pyramid.asset import resolve_asset_spec +from pyramid.compat import ( + string_types, + urlparse, + url_quote, + WIN, + is_nonstr_iter, + ) + +from pyramid.decorator import reify + +from pyramid.exceptions import ( + ConfigurationError, + PredicateMismatch, + ) + +from pyramid.httpexceptions import ( + HTTPForbidden, + HTTPNotFound, + default_exceptionresponse_view, + ) + +from pyramid.registry import Deferred + +from pyramid.security import NO_PERMISSION_REQUIRED +from pyramid.static import static_view + +from pyramid.url import parse_url_overrides + +from pyramid.view import AppendSlashNotFoundViewFactory + +from pyramid.util import ( + as_sorted_tuple, + TopologicalSorter, + ) + +import pyramid.predicates +import pyramid.viewderivers + +from pyramid.viewderivers import ( + INGRESS, + VIEW, + preserve_view_attrs, + view_description, + requestonly, + DefaultViewMapper, + wraps_view, +) + +from pyramid.config.util import ( + action_method, + DEFAULT_PHASH, + MAX_ORDER, + normalize_accept_offer, + predvalseq, + sort_accept_offers, + ) + +urljoin = urlparse.urljoin +url_parse = urlparse.urlparse + +DefaultViewMapper = DefaultViewMapper # bw-compat +preserve_view_attrs = preserve_view_attrs # bw-compat +requestonly = requestonly # bw-compat +view_description = view_description # bw-compat + +@implementer(IMultiView) +class MultiView(object): + + def __init__(self, name): + self.name = name + self.media_views = {} + self.views = [] + self.accepts = [] + + def __discriminator__(self, context, request): + # used by introspection systems like so: + # view = adapters.lookup(....) + # view.__discriminator__(context, request) -> view's discriminator + # so that superdynamic systems can feed the discriminator to + # the introspection system to get info about it + view = self.match(context, request) + return view.__discriminator__(context, request) + + def add(self, view, order, phash=None, accept=None, accept_order=None): + if phash is not None: + for i, (s, v, h) in enumerate(list(self.views)): + if phash == h: + self.views[i] = (order, view, phash) + return + + if accept is None or '*' in accept: + self.views.append((order, view, phash)) + self.views.sort(key=operator.itemgetter(0)) + else: + subset = self.media_views.setdefault(accept, []) + for i, (s, v, h) in enumerate(list(subset)): + if phash == h: + subset[i] = (order, view, phash) + return + else: + subset.append((order, view, phash)) + subset.sort(key=operator.itemgetter(0)) + # dedupe accepts and sort appropriately + accepts = set(self.accepts) + accepts.add(accept) + if accept_order: + accept_order = [v for _, v in accept_order.sorted()] + self.accepts = sort_accept_offers(accepts, accept_order) + + def get_views(self, request): + if self.accepts and hasattr(request, 'accept'): + views = [] + for offer, _ in request.accept.acceptable_offers(self.accepts): + views.extend(self.media_views[offer]) + views.extend(self.views) + return views + return self.views + + def match(self, context, request): + for order, view, phash in self.get_views(request): + if not hasattr(view, '__predicated__'): + return view + if view.__predicated__(context, request): + return view + raise PredicateMismatch(self.name) + + def __permitted__(self, context, request): + view = self.match(context, request) + if hasattr(view, '__permitted__'): + return view.__permitted__(context, request) + return True + + def __call_permissive__(self, context, request): + view = self.match(context, request) + view = getattr(view, '__call_permissive__', view) + return view(context, request) + + def __call__(self, context, request): + for order, view, phash in self.get_views(request): + try: + return view(context, request) + except PredicateMismatch: + continue + raise PredicateMismatch(self.name) + +def attr_wrapped_view(view, info): + accept, order, phash = (info.options.get('accept', None), + getattr(info, 'order', MAX_ORDER), + getattr(info, 'phash', DEFAULT_PHASH)) + # this is a little silly but we don't want to decorate the original + # function with attributes that indicate accept, order, and phash, + # so we use a wrapper + if ( + (accept is None) and + (order == MAX_ORDER) and + (phash == DEFAULT_PHASH) + ): + return view # defaults + def attr_view(context, request): + return view(context, request) + attr_view.__accept__ = accept + attr_view.__order__ = order + attr_view.__phash__ = phash + attr_view.__view_attr__ = info.options.get('attr') + attr_view.__permission__ = info.options.get('permission') + return attr_view + +attr_wrapped_view.options = ('accept', 'attr', 'permission') + +def predicated_view(view, info): + preds = info.predicates + if not preds: + return view + def predicate_wrapper(context, request): + for predicate in preds: + if not predicate(context, request): + view_name = getattr(view, '__name__', view) + raise PredicateMismatch( + 'predicate mismatch for view %s (%s)' % ( + view_name, predicate.text())) + return view(context, request) + def checker(context, request): + return all((predicate(context, request) for predicate in + preds)) + predicate_wrapper.__predicated__ = checker + predicate_wrapper.__predicates__ = preds + return predicate_wrapper + +def viewdefaults(wrapped): + """ Decorator for add_view-like methods which takes into account + __view_defaults__ attached to view it is passed. Not a documented API but + used by some external systems.""" + def wrapper(self, *arg, **kw): + defaults = {} + if arg: + view = arg[0] + else: + view = kw.get('view') + view = self.maybe_dotted(view) + if inspect.isclass(view): + defaults = getattr(view, '__view_defaults__', {}).copy() + if '_backframes' not in kw: + kw['_backframes'] = 1 # for action_method + defaults.update(kw) + return wrapped(self, *arg, **defaults) + return functools.wraps(wrapped)(wrapper) + +def combine_decorators(*decorators): + def decorated(view_callable): + # reversed() allows a more natural ordering in the api + for decorator in reversed(decorators): + view_callable = decorator(view_callable) + return view_callable + return decorated + +class ViewsConfiguratorMixin(object): + @viewdefaults + @action_method + def add_view( + self, + view=None, + name="", + for_=None, + permission=None, + request_type=None, + route_name=None, + request_method=None, + request_param=None, + containment=None, + attr=None, + renderer=None, + wrapper=None, + xhr=None, + accept=None, + header=None, + path_info=None, + custom_predicates=(), + context=None, + decorator=None, + mapper=None, + http_cache=None, + match_param=None, + check_csrf=None, + require_csrf=None, + exception_only=False, + **view_options): + """ Add a :term:`view configuration` to the current + configuration state. Arguments to ``add_view`` are broken + down below into *predicate* arguments and *non-predicate* + arguments. Predicate arguments narrow the circumstances in + which the view callable will be invoked when a request is + presented to :app:`Pyramid`; non-predicate arguments are + informational. + + Non-Predicate Arguments + + view + + A :term:`view callable` or a :term:`dotted Python name` + which refers to a view callable. This argument is required + unless a ``renderer`` argument also exists. If a + ``renderer`` argument is passed, and a ``view`` argument is + not provided, the view callable defaults to a callable that + returns an empty dictionary (see + :ref:`views_which_use_a_renderer`). + + permission + + A :term:`permission` that the user must possess in order to invoke + the :term:`view callable`. See :ref:`view_security_section` for + more information about view security and permissions. This is + often a string like ``view`` or ``edit``. + + If ``permission`` is omitted, a *default* permission may be used + for this view registration if one was named as the + :class:`pyramid.config.Configurator` constructor's + ``default_permission`` argument, or if + :meth:`pyramid.config.Configurator.set_default_permission` was used + prior to this view registration. Pass the value + :data:`pyramid.security.NO_PERMISSION_REQUIRED` as the permission + argument to explicitly indicate that the view should always be + executable by entirely anonymous users, regardless of the default + permission, bypassing any :term:`authorization policy` that may be + in effect. + + attr + + This knob is most useful when the view definition is a class. + + The view machinery defaults to using the ``__call__`` method + of the :term:`view callable` (or the function itself, if the + view callable is a function) to obtain a response. The + ``attr`` value allows you to vary the method attribute used + to obtain the response. For example, if your view was a + class, and the class has a method named ``index`` and you + wanted to use this method instead of the class' ``__call__`` + method to return the response, you'd say ``attr="index"`` in the + view configuration for the view. + + renderer + + This is either a single string term (e.g. ``json``) or a + string implying a path or :term:`asset specification` + (e.g. ``templates/views.pt``) naming a :term:`renderer` + implementation. If the ``renderer`` value does not contain + a dot ``.``, the specified string will be used to look up a + renderer implementation, and that renderer implementation + will be used to construct a response from the view return + value. If the ``renderer`` value contains a dot (``.``), + the specified term will be treated as a path, and the + filename extension of the last element in the path will be + used to look up the renderer implementation, which will be + passed the full path. The renderer implementation will be + used to construct a :term:`response` from the view return + value. + + Note that if the view itself returns a :term:`response` (see + :ref:`the_response`), the specified renderer implementation + is never called. + + When the renderer is a path, although a path is usually just + a simple relative pathname (e.g. ``templates/foo.pt``, + implying that a template named "foo.pt" is in the + "templates" directory relative to the directory of the + current :term:`package` of the Configurator), a path can be + absolute, starting with a slash on UNIX or a drive letter + prefix on Windows. The path can alternately be a + :term:`asset specification` in the form + ``some.dotted.package_name:relative/path``, making it + possible to address template assets which live in a + separate package. + + The ``renderer`` attribute is optional. If it is not + defined, the "null" renderer is assumed (no rendering is + performed and the value is passed back to the upstream + :app:`Pyramid` machinery unmodified). + + http_cache + + .. versionadded:: 1.1 + + When you supply an ``http_cache`` value to a view configuration, + the ``Expires`` and ``Cache-Control`` headers of a response + generated by the associated view callable are modified. The value + for ``http_cache`` may be one of the following: + + - A nonzero integer. If it's a nonzero integer, it's treated as a + number of seconds. This number of seconds will be used to + compute the ``Expires`` header and the ``Cache-Control: + max-age`` parameter of responses to requests which call this view. + For example: ``http_cache=3600`` instructs the requesting browser + to 'cache this response for an hour, please'. + + - A ``datetime.timedelta`` instance. If it's a + ``datetime.timedelta`` instance, it will be converted into a + number of seconds, and that number of seconds will be used to + compute the ``Expires`` header and the ``Cache-Control: + max-age`` parameter of responses to requests which call this view. + For example: ``http_cache=datetime.timedelta(days=1)`` instructs + the requesting browser to 'cache this response for a day, please'. + + - Zero (``0``). If the value is zero, the ``Cache-Control`` and + ``Expires`` headers present in all responses from this view will + be composed such that client browser cache (and any intermediate + caches) are instructed to never cache the response. + + - A two-tuple. If it's a two tuple (e.g. ``http_cache=(1, + {'public':True})``), the first value in the tuple may be a + nonzero integer or a ``datetime.timedelta`` instance; in either + case this value will be used as the number of seconds to cache + the response. The second value in the tuple must be a + dictionary. The values present in the dictionary will be used as + input to the ``Cache-Control`` response header. For example: + ``http_cache=(3600, {'public':True})`` means 'cache for an hour, + and add ``public`` to the Cache-Control header of the response'. + All keys and values supported by the + ``webob.cachecontrol.CacheControl`` interface may be added to the + dictionary. Supplying ``{'public':True}`` is equivalent to + calling ``response.cache_control.public = True``. + + Providing a non-tuple value as ``http_cache`` is equivalent to + calling ``response.cache_expires(value)`` within your view's body. + + Providing a two-tuple value as ``http_cache`` is equivalent to + calling ``response.cache_expires(value[0], **value[1])`` within your + view's body. + + If you wish to avoid influencing, the ``Expires`` header, and + instead wish to only influence ``Cache-Control`` headers, pass a + tuple as ``http_cache`` with the first element of ``None``, e.g.: + ``(None, {'public':True})``. + + If you wish to prevent a view that uses ``http_cache`` in its + configuration from having its caching response headers changed by + this machinery, set ``response.cache_control.prevent_auto = True`` + before returning the response from the view. This effectively + disables any HTTP caching done by ``http_cache`` for that response. + + require_csrf + + .. versionadded:: 1.7 + + A boolean option or ``None``. Default: ``None``. + + If this option is set to ``True`` then CSRF checks will be enabled + for requests to this view. The required token or header default to + ``csrf_token`` and ``X-CSRF-Token``, respectively. + + CSRF checks only affect "unsafe" methods as defined by RFC2616. By + default, these methods are anything except + ``GET``, ``HEAD``, ``OPTIONS``, and ``TRACE``. + + The defaults here may be overridden by + :meth:`pyramid.config.Configurator.set_default_csrf_options`. + + This feature requires a configured :term:`session factory`. + + If this option is set to ``False`` then CSRF checks will be disabled + regardless of the default ``require_csrf`` setting passed + to ``set_default_csrf_options``. + + See :ref:`auto_csrf_checking` for more information. + + wrapper + + The :term:`view name` of a different :term:`view + configuration` which will receive the response body of this + view as the ``request.wrapped_body`` attribute of its own + :term:`request`, and the :term:`response` returned by this + view as the ``request.wrapped_response`` attribute of its + own request. Using a wrapper makes it possible to "chain" + views together to form a composite response. The response + of the outermost wrapper view will be returned to the user. + The wrapper view will be found as any view is found: see + :ref:`view_lookup`. The "best" wrapper view will be found + based on the lookup ordering: "under the hood" this wrapper + view is looked up via + ``pyramid.view.render_view_to_response(context, request, + 'wrapper_viewname')``. The context and request of a wrapper + view is the same context and request of the inner view. If + this attribute is unspecified, no view wrapping is done. + + decorator + + A :term:`dotted Python name` to function (or the function itself, + or an iterable of the aforementioned) which will be used to + decorate the registered :term:`view callable`. The decorator + function(s) will be called with the view callable as a single + argument. The view callable it is passed will accept + ``(context, request)``. The decorator(s) must return a + replacement view callable which also accepts ``(context, + request)``. + + If decorator is an iterable, the callables will be combined and + used in the order provided as a decorator. + For example:: + + @view_config(..., + decorator=(decorator2, + decorator1)) + def myview(request): + .... + + Is similar to doing:: + + @view_config(...) + @decorator2 + @decorator1 + def myview(request): + ... + + Except with the existing benefits of ``decorator=`` (having a common + decorator syntax for all view calling conventions and not having to + think about preserving function attributes such as ``__name__`` and + ``__module__`` within decorator logic). + + An important distinction is that each decorator will receive a + response object implementing :class:`pyramid.interfaces.IResponse` + instead of the raw value returned from the view callable. All + decorators in the chain must return a response object or raise an + exception: + + .. code-block:: python + + def log_timer(wrapped): + def wrapper(context, request): + start = time.time() + response = wrapped(context, request) + duration = time.time() - start + response.headers['X-View-Time'] = '%.3f' % (duration,) + log.info('view took %.3f seconds', duration) + return response + return wrapper + + .. versionchanged:: 1.4a4 + Passing an iterable. + + mapper + + A Python object or :term:`dotted Python name` which refers to a + :term:`view mapper`, or ``None``. By default it is ``None``, which + indicates that the view should use the default view mapper. This + plug-point is useful for Pyramid extension developers, but it's not + very useful for 'civilians' who are just developing stock Pyramid + applications. Pay no attention to the man behind the curtain. + + accept + + A :term:`media type` that will be matched against the ``Accept`` + HTTP request header. If this value is specified, it must be a + specific media type such as ``text/html`` or ``text/html;level=1``. + If the media type is acceptable by the ``Accept`` header of the + request, or if the ``Accept`` header isn't set at all in the request, + this predicate will match. If this does not match the ``Accept`` + header of the request, view matching continues. + + If ``accept`` is not specified, the ``HTTP_ACCEPT`` HTTP header is + not taken into consideration when deciding whether or not to invoke + the associated view callable. + + The ``accept`` argument is technically not a predicate and does + not support wrapping with :func:`pyramid.config.not_`. + + See :ref:`accept_content_negotiation` for more information. + + .. versionchanged:: 1.10 + + Specifying a media range is deprecated and will be removed in + :app:`Pyramid` 2.0. Use explicit media types to avoid any + ambiguities in content negotiation. + + exception_only + + .. versionadded:: 1.8 + + When this value is ``True``, the ``context`` argument must be + a subclass of ``Exception``. This flag indicates that only an + :term:`exception view` should be created, and that this view should + not match if the traversal :term:`context` matches the ``context`` + argument. If the ``context`` is a subclass of ``Exception`` and + this value is ``False`` (the default), then a view will be + registered to match the traversal :term:`context` as well. + + Predicate Arguments + + name + + The :term:`view name`. Read :ref:`traversal_chapter` to + understand the concept of a view name. + + context + + An object or a :term:`dotted Python name` referring to an + interface or class object that the :term:`context` must be + an instance of, *or* the :term:`interface` that the + :term:`context` must provide in order for this view to be + found and called. This predicate is true when the + :term:`context` is an instance of the represented class or + if the :term:`context` provides the represented interface; + it is otherwise false. This argument may also be provided + to ``add_view`` as ``for_`` (an older, still-supported + spelling). If the view should *only* match when handling + exceptions, then set the ``exception_only`` to ``True``. + + route_name + + This value must match the ``name`` of a :term:`route + configuration` declaration (see :ref:`urldispatch_chapter`) + that must match before this view will be called. + + request_type + + This value should be an :term:`interface` that the + :term:`request` must provide in order for this view to be + found and called. This value exists only for backwards + compatibility purposes. + + request_method + + This value can be either a string (such as ``"GET"``, ``"POST"``, + ``"PUT"``, ``"DELETE"``, ``"HEAD"`` or ``"OPTIONS"``) representing + an HTTP ``REQUEST_METHOD``, or a tuple containing one or more of + these strings. A view declaration with this argument ensures that + the view will only be called when the ``method`` attribute of the + request (aka the ``REQUEST_METHOD`` of the WSGI environment) matches + a supplied value. Note that use of ``GET`` also implies that the + view will respond to ``HEAD`` as of Pyramid 1.4. + + .. versionchanged:: 1.2 + The ability to pass a tuple of items as ``request_method``. + Previous versions allowed only a string. + + request_param + + This value can be any string or any sequence of strings. A view + declaration with this argument ensures that the view will only be + called when the :term:`request` has a key in the ``request.params`` + dictionary (an HTTP ``GET`` or ``POST`` variable) that has a + name which matches the supplied value (if the value is a string) + or values (if the value is a tuple). If any value + supplied has a ``=`` sign in it, + e.g. ``request_param="foo=123"``, then the key (``foo``) + must both exist in the ``request.params`` dictionary, *and* + the value must match the right hand side of the expression + (``123``) for the view to "match" the current request. + + match_param + + .. versionadded:: 1.2 + + This value can be a string of the format "key=value" or a tuple + containing one or more of these strings. + + A view declaration with this argument ensures that the view will + only be called when the :term:`request` has key/value pairs in its + :term:`matchdict` that equal those supplied in the predicate. + e.g. ``match_param="action=edit"`` would require the ``action`` + parameter in the :term:`matchdict` match the right hand side of + the expression (``edit``) for the view to "match" the current + request. + + If the ``match_param`` is a tuple, every key/value pair must match + for the predicate to pass. + + containment + + This value should be a Python class or :term:`interface` (or a + :term:`dotted Python name`) that an object in the + :term:`lineage` of the context must provide in order for this view + to be found and called. The nodes in your object graph must be + "location-aware" to use this feature. See + :ref:`location_aware` for more information about + location-awareness. + + xhr + + This value should be either ``True`` or ``False``. If this + value is specified and is ``True``, the :term:`request` + must possess an ``HTTP_X_REQUESTED_WITH`` (aka + ``X-Requested-With``) header that has the value + ``XMLHttpRequest`` for this view to be found and called. + This is useful for detecting AJAX requests issued from + jQuery, Prototype and other Javascript libraries. + + header + + This value represents an HTTP header name or a header + name/value pair. If the value contains a ``:`` (colon), it + will be considered a name/value pair + (e.g. ``User-Agent:Mozilla/.*`` or ``Host:localhost``). The + value portion should be a regular expression. If the value + does not contain a colon, the entire value will be + considered to be the header name + (e.g. ``If-Modified-Since``). If the value evaluates to a + header name only without a value, the header specified by + the name must be present in the request for this predicate + to be true. If the value evaluates to a header name/value + pair, the header specified by the name must be present in + the request *and* the regular expression specified as the + value must match the header value. Whether or not the value + represents a header name or a header name/value pair, the + case of the header name is not significant. + + path_info + + This value represents a regular expression pattern that will + be tested against the ``PATH_INFO`` WSGI environment + variable. If the regex matches, this predicate will be + ``True``. + + check_csrf + + .. deprecated:: 1.7 + Use the ``require_csrf`` option or see :ref:`auto_csrf_checking` + instead to have :class:`pyramid.exceptions.BadCSRFToken` + exceptions raised. + + If specified, this value should be one of ``None``, ``True``, + ``False``, or a string representing the 'check name'. If the value + is ``True`` or a string, CSRF checking will be performed. If the + value is ``False`` or ``None``, CSRF checking will not be performed. + + If the value provided is a string, that string will be used as the + 'check name'. If the value provided is ``True``, ``csrf_token`` will + be used as the check name. + + If CSRF checking is performed, the checked value will be the value of + ``request.params[check_name]``. This value will be compared against + the value of ``policy.get_csrf_token()`` (where ``policy`` is an + implementation of :meth:`pyramid.interfaces.ICSRFStoragePolicy`), and the + check will pass if these two values are the same. If the check + passes, the associated view will be permitted to execute. If the + check fails, the associated view will not be permitted to execute. + + .. versionadded:: 1.4a2 + + .. versionchanged:: 1.9 + This feature requires either a :term:`session factory` to have been + configured, or a :term:`CSRF storage policy` other than the default + to be in use. + + + physical_path + + If specified, this value should be a string or a tuple representing + the :term:`physical path` of the context found via traversal for this + predicate to match as true. For example: ``physical_path='/'`` or + ``physical_path='/a/b/c'`` or ``physical_path=('', 'a', 'b', 'c')``. + This is not a path prefix match or a regex, it's a whole-path match. + It's useful when you want to always potentially show a view when some + object is traversed to, but you can't be sure about what kind of + object it will be, so you can't use the ``context`` predicate. The + individual path elements inbetween slash characters or in tuple + elements should be the Unicode representation of the name of the + resource and should not be encoded in any way. + + .. versionadded:: 1.4a3 + + effective_principals + + If specified, this value should be a :term:`principal` identifier or + a sequence of principal identifiers. If the + :attr:`pyramid.request.Request.effective_principals` property + indicates that every principal named in the argument list is present + in the current request, this predicate will return True; otherwise it + will return False. For example: + ``effective_principals=pyramid.security.Authenticated`` or + ``effective_principals=('fred', 'group:admins')``. + + .. versionadded:: 1.4a4 + + custom_predicates + + .. deprecated:: 1.5 + This value should be a sequence of references to custom + predicate callables. Use custom predicates when no set of + predefined predicates do what you need. Custom predicates + can be combined with predefined predicates as necessary. + Each custom predicate callable should accept two arguments: + ``context`` and ``request`` and should return either + ``True`` or ``False`` after doing arbitrary evaluation of + the context and/or the request. The ``predicates`` argument + to this method and the ability to register third-party view + predicates via + :meth:`pyramid.config.Configurator.add_view_predicate` + obsoletes this argument, but it is kept around for backwards + compatibility. + + view_options + + Pass a key/value pair here to use a third-party predicate or set a + value for a view deriver. See + :meth:`pyramid.config.Configurator.add_view_predicate` and + :meth:`pyramid.config.Configurator.add_view_deriver`. See + :ref:`view_and_route_predicates` for more information about + third-party predicates and :ref:`view_derivers` for information + about view derivers. + + .. versionadded: 1.4a1 + + .. versionchanged: 1.7 + + Support setting view deriver options. Previously, only custom + view predicate values could be supplied. + + """ + if custom_predicates: + warnings.warn( + ('The "custom_predicates" argument to Configurator.add_view ' + 'is deprecated as of Pyramid 1.5. Use ' + '"config.add_view_predicate" and use the registered ' + 'view predicate as a predicate argument to add_view instead. ' + 'See "Adding A Third Party View, Route, or Subscriber ' + 'Predicate" in the "Hooks" chapter of the documentation ' + 'for more information.'), + DeprecationWarning, + stacklevel=4, + ) + + if check_csrf is not None: + warnings.warn( + ('The "check_csrf" argument to Configurator.add_view is ' + 'deprecated as of Pyramid 1.7. Use the "require_csrf" option ' + 'instead or see "Checking CSRF Tokens Automatically" in the ' + '"Sessions" chapter of the documentation for more ' + 'information.'), + DeprecationWarning, + stacklevel=4, + ) + + if accept is not None: + if is_nonstr_iter(accept): + raise ConfigurationError( + 'A list is not supported in the "accept" view predicate.', + ) + if '*' in accept: + warnings.warn( + ('Passing a media range to the "accept" argument of ' + 'Configurator.add_view is deprecated as of Pyramid 1.10. ' + 'Use explicit media types to avoid ambiguities in ' + 'content negotiation that may impact your users.'), + DeprecationWarning, + stacklevel=4, + ) + # XXX when media ranges are gone, switch allow_range=False + accept = normalize_accept_offer(accept, allow_range=True) + + view = self.maybe_dotted(view) + context = self.maybe_dotted(context) + for_ = self.maybe_dotted(for_) + containment = self.maybe_dotted(containment) + mapper = self.maybe_dotted(mapper) + + if is_nonstr_iter(decorator): + decorator = combine_decorators(*map(self.maybe_dotted, decorator)) + else: + decorator = self.maybe_dotted(decorator) + + if not view: + if renderer: + def view(context, request): + return {} + else: + raise ConfigurationError('"view" was not specified and ' + 'no "renderer" specified') + + if request_type is not None: + request_type = self.maybe_dotted(request_type) + if not IInterface.providedBy(request_type): + raise ConfigurationError( + 'request_type must be an interface, not %s' % request_type) + + if context is None: + context = for_ + + isexc = isexception(context) + if exception_only and not isexc: + raise ConfigurationError( + 'view "context" must be an exception type when ' + '"exception_only" is True') + + r_context = context + if r_context is None: + r_context = Interface + if not IInterface.providedBy(r_context): + r_context = implementedBy(r_context) + + if isinstance(renderer, string_types): + renderer = renderers.RendererHelper( + name=renderer, package=self.package, + registry=self.registry) + + introspectables = [] + ovals = view_options.copy() + ovals.update(dict( + xhr=xhr, + request_method=request_method, + path_info=path_info, + request_param=request_param, + header=header, + accept=accept, + containment=containment, + request_type=request_type, + match_param=match_param, + check_csrf=check_csrf, + custom=predvalseq(custom_predicates), + )) + + def discrim_func(): + # We need to defer the discriminator until we know what the phash + # is. It can't be computed any sooner because thirdparty + # predicates/view derivers may not yet exist when add_view is + # called. + predlist = self.get_predlist('view') + valid_predicates = predlist.names() + pvals = {} + dvals = {} + + for (k, v) in ovals.items(): + if k in valid_predicates: + pvals[k] = v + else: + dvals[k] = v + + self._check_view_options(**dvals) + + order, preds, phash = predlist.make(self, **pvals) + + view_intr.update({ + 'phash': phash, + 'order': order, + 'predicates': preds, + }) + return ('view', context, name, route_name, phash) + + discriminator = Deferred(discrim_func) + + if inspect.isclass(view) and attr: + view_desc = 'method %r of %s' % ( + attr, self.object_description(view)) + else: + view_desc = self.object_description(view) + + tmpl_intr = None + + view_intr = self.introspectable('views', + discriminator, + view_desc, + 'view') + view_intr.update(dict( + name=name, + context=context, + exception_only=exception_only, + containment=containment, + request_param=request_param, + request_methods=request_method, + route_name=route_name, + attr=attr, + xhr=xhr, + accept=accept, + header=header, + path_info=path_info, + match_param=match_param, + check_csrf=check_csrf, + http_cache=http_cache, + require_csrf=require_csrf, + callable=view, + mapper=mapper, + decorator=decorator, + )) + view_intr.update(view_options) + introspectables.append(view_intr) + + def register(permission=permission, renderer=renderer): + request_iface = IRequest + if route_name is not None: + request_iface = self.registry.queryUtility(IRouteRequest, + name=route_name) + if request_iface is None: + # route configuration should have already happened in + # phase 2 + raise ConfigurationError( + 'No route named %s found for view registration' % + route_name) + + if renderer is None: + # use default renderer if one exists (reg'd in phase 1) + if self.registry.queryUtility(IRendererFactory) is not None: + renderer = renderers.RendererHelper( + name=None, + package=self.package, + registry=self.registry + ) + + renderer_type = getattr(renderer, 'type', None) + intrspc = self.introspector + if ( + renderer_type is not None and + tmpl_intr is not None and + intrspc is not None and + intrspc.get('renderer factories', renderer_type) is not None + ): + # allow failure of registered template factories to be deferred + # until view execution, like other bad renderer factories; if + # we tried to relate this to an existing renderer factory + # without checking if the factory actually existed, we'd end + # up with a KeyError at startup time, which is inconsistent + # with how other bad renderer registrations behave (they throw + # a ValueError at view execution time) + tmpl_intr.relate('renderer factories', renderer.type) + + # make a new view separately for normal and exception paths + if not exception_only: + derived_view = derive_view(False, renderer) + register_view(IViewClassifier, request_iface, derived_view) + if isexc: + derived_exc_view = derive_view(True, renderer) + register_view(IExceptionViewClassifier, request_iface, + derived_exc_view) + + if exception_only: + derived_view = derived_exc_view + + # if there are two derived views, combine them into one for + # introspection purposes + if not exception_only and isexc: + derived_view = runtime_exc_view(derived_view, derived_exc_view) + + derived_view.__discriminator__ = lambda *arg: discriminator + # __discriminator__ is used by superdynamic systems + # that require it for introspection after manual view lookup; + # see also MultiView.__discriminator__ + view_intr['derived_callable'] = derived_view + + self.registry._clear_view_lookup_cache() + + def derive_view(isexc_only, renderer): + # added by discrim_func above during conflict resolving + preds = view_intr['predicates'] + order = view_intr['order'] + phash = view_intr['phash'] + + derived_view = self._derive_view( + view, + route_name=route_name, + permission=permission, + predicates=preds, + attr=attr, + context=context, + exception_only=isexc_only, + renderer=renderer, + wrapper_viewname=wrapper, + viewname=name, + accept=accept, + order=order, + phash=phash, + decorator=decorator, + mapper=mapper, + http_cache=http_cache, + require_csrf=require_csrf, + extra_options=ovals, + ) + return derived_view + + def register_view(classifier, request_iface, derived_view): + # A multiviews is a set of views which are registered for + # exactly the same context type/request type/name triad. Each + # constituent view in a multiview differs only by the + # predicates which it possesses. + + # To find a previously registered view for a context + # type/request type/name triad, we need to use the + # ``registered`` method of the adapter registry rather than + # ``lookup``. ``registered`` ignores interface inheritance + # for the required and provided arguments, returning only a + # view registered previously with the *exact* triad we pass + # in. + + # We need to do this three times, because we use three + # different interfaces as the ``provided`` interface while + # doing registrations, and ``registered`` performs exact + # matches on all the arguments it receives. + + old_view = None + order, phash = view_intr['order'], view_intr['phash'] + registered = self.registry.adapters.registered + + for view_type in (IView, ISecuredView, IMultiView): + old_view = registered( + (classifier, request_iface, r_context), + view_type, name) + if old_view is not None: + break + + old_phash = getattr(old_view, '__phash__', DEFAULT_PHASH) + is_multiview = IMultiView.providedBy(old_view) + want_multiview = ( + is_multiview + # no component was yet registered for exactly this triad + # or only one was registered but with the same phash, meaning + # that this view is an override + or (old_view is not None and old_phash != phash) + ) + + if not want_multiview: + if hasattr(derived_view, '__call_permissive__'): + view_iface = ISecuredView + else: + view_iface = IView + self.registry.registerAdapter( + derived_view, + (classifier, request_iface, context), + view_iface, + name + ) + + else: + # - A view or multiview was already registered for this + # triad, and the new view is not an override. + + # XXX we could try to be more efficient here and register + # a non-secured view for a multiview if none of the + # multiview's constituent views have a permission + # associated with them, but this code is getting pretty + # rough already + if is_multiview: + multiview = old_view + else: + multiview = MultiView(name) + old_accept = getattr(old_view, '__accept__', None) + old_order = getattr(old_view, '__order__', MAX_ORDER) + # don't bother passing accept_order here as we know we're + # adding another one right after which will re-sort + multiview.add(old_view, old_order, old_phash, old_accept) + accept_order = self.registry.queryUtility(IAcceptOrder) + multiview.add(derived_view, order, phash, accept, accept_order) + for view_type in (IView, ISecuredView): + # unregister any existing views + self.registry.adapters.unregister( + (classifier, request_iface, r_context), + view_type, name=name) + self.registry.registerAdapter( + multiview, + (classifier, request_iface, context), + IMultiView, name=name) + + if mapper: + mapper_intr = self.introspectable( + 'view mappers', + discriminator, + 'view mapper for %s' % view_desc, + 'view mapper' + ) + mapper_intr['mapper'] = mapper + mapper_intr.relate('views', discriminator) + introspectables.append(mapper_intr) + if route_name: + view_intr.relate('routes', route_name) # see add_route + if renderer is not None and renderer.name and '.' in renderer.name: + # the renderer is a template + tmpl_intr = self.introspectable( + 'templates', + discriminator, + renderer.name, + 'template' + ) + tmpl_intr.relate('views', discriminator) + tmpl_intr['name'] = renderer.name + tmpl_intr['type'] = renderer.type + tmpl_intr['renderer'] = renderer + introspectables.append(tmpl_intr) + if permission is not None: + # if a permission exists, register a permission introspectable + perm_intr = self.introspectable( + 'permissions', + permission, + permission, + 'permission' + ) + perm_intr['value'] = permission + perm_intr.relate('views', discriminator) + introspectables.append(perm_intr) + self.action(discriminator, register, introspectables=introspectables) + + def _check_view_options(self, **kw): + # we only need to validate deriver options because the predicates + # were checked by the predlist + derivers = self.registry.getUtility(IViewDerivers) + for deriver in derivers.values(): + for opt in getattr(deriver, 'options', []): + kw.pop(opt, None) + if kw: + raise ConfigurationError('Unknown view options: %s' % (kw,)) + + def _apply_view_derivers(self, info): + # These derivers are not really derivers and so have fixed order + outer_derivers = [('attr_wrapped_view', attr_wrapped_view), + ('predicated_view', predicated_view)] + + view = info.original_view + derivers = self.registry.getUtility(IViewDerivers) + for name, deriver in reversed(outer_derivers + derivers.sorted()): + view = wraps_view(deriver)(view, info) + return view + + @action_method + def add_view_predicate(self, name, factory, weighs_more_than=None, + weighs_less_than=None): + """ + .. versionadded:: 1.4 + + Adds a view predicate factory. The associated view predicate can + later be named as a keyword argument to + :meth:`pyramid.config.Configurator.add_view` in the + ``predicates`` anonyous keyword argument dictionary. + + ``name`` should be the name of the predicate. It must be a valid + Python identifier (it will be used as a keyword argument to + ``add_view`` by others). + + ``factory`` should be a :term:`predicate factory` or :term:`dotted + Python name` which refers to a predicate factory. + + See :ref:`view_and_route_predicates` for more information. + """ + self._add_predicate( + 'view', + name, + factory, + weighs_more_than=weighs_more_than, + weighs_less_than=weighs_less_than + ) + + def add_default_view_predicates(self): + p = pyramid.predicates + for (name, factory) in ( + ('xhr', p.XHRPredicate), + ('request_method', p.RequestMethodPredicate), + ('path_info', p.PathInfoPredicate), + ('request_param', p.RequestParamPredicate), + ('header', p.HeaderPredicate), + ('accept', p.AcceptPredicate), + ('containment', p.ContainmentPredicate), + ('request_type', p.RequestTypePredicate), + ('match_param', p.MatchParamPredicate), + ('check_csrf', p.CheckCSRFTokenPredicate), + ('physical_path', p.PhysicalPathPredicate), + ('effective_principals', p.EffectivePrincipalsPredicate), + ('custom', p.CustomPredicate), + ): + self.add_view_predicate(name, factory) + + def add_default_accept_view_order(self): + for accept in ( + 'text/html', + 'application/xhtml+xml', + 'application/xml', + 'text/xml', + 'text/plain', + 'application/json', + ): + self.add_accept_view_order(accept) + + @action_method + def add_accept_view_order( + self, + value, + weighs_more_than=None, + weighs_less_than=None, + ): + """ + Specify an ordering preference for the ``accept`` view option used + during :term:`view lookup`. + + By default, if two views have different ``accept`` options and a + request specifies ``Accept: */*`` or omits the header entirely then + it is random which view will be selected. This method provides a way + to specify a server-side, relative ordering between accept media types. + + ``value`` should be a :term:`media type` as specified by + :rfc:`7231#section-5.3.2`. For example, ``text/plain;charset=utf8``, + ``application/json`` or ``text/html``. + + ``weighs_more_than`` and ``weighs_less_than`` control the ordering + of media types. Each value may be a string or a list of strings. If + all options for ``weighs_more_than`` (or ``weighs_less_than``) cannot + be found, it is an error. + + Earlier calls to ``add_accept_view_order`` are given higher priority + over later calls, assuming similar constraints but standard conflict + resolution mechanisms can be used to override constraints. + + See :ref:`accept_content_negotiation` for more information. + + .. versionadded:: 1.10 + + """ + def check_type(than): + than_type, than_subtype, than_params = Accept.parse_offer(than) + # text/plain vs text/html;charset=utf8 + if bool(offer_params) ^ bool(than_params): + raise ConfigurationError( + 'cannot compare a media type with params to one without ' + 'params') + # text/plain;charset=utf8 vs text/html;charset=utf8 + if offer_params and ( + offer_subtype != than_subtype or offer_type != than_type + ): + raise ConfigurationError( + 'cannot compare params across different media types') + + def normalize_types(thans): + thans = [normalize_accept_offer(than) for than in thans] + for than in thans: + check_type(than) + return thans + + value = normalize_accept_offer(value) + offer_type, offer_subtype, offer_params = Accept.parse_offer(value) + + if weighs_more_than: + if not is_nonstr_iter(weighs_more_than): + weighs_more_than = [weighs_more_than] + weighs_more_than = normalize_types(weighs_more_than) + + if weighs_less_than: + if not is_nonstr_iter(weighs_less_than): + weighs_less_than = [weighs_less_than] + weighs_less_than = normalize_types(weighs_less_than) + + discriminator = ('accept view order', value) + intr = self.introspectable( + 'accept view order', + value, + value, + 'accept view order') + intr['value'] = value + intr['weighs_more_than'] = weighs_more_than + intr['weighs_less_than'] = weighs_less_than + def register(): + sorter = self.registry.queryUtility(IAcceptOrder) + if sorter is None: + sorter = TopologicalSorter() + self.registry.registerUtility(sorter, IAcceptOrder) + sorter.add( + value, value, + before=weighs_more_than, + after=weighs_less_than, + ) + self.action(discriminator, register, introspectables=(intr,), + order=PHASE1_CONFIG) # must be registered before add_view + + @action_method + def add_view_deriver(self, deriver, name=None, under=None, over=None): + """ + .. versionadded:: 1.7 + + Add a :term:`view deriver` to the view pipeline. View derivers are + a feature used by extension authors to wrap views in custom code + controllable by view-specific options. + + ``deriver`` should be a callable conforming to the + :class:`pyramid.interfaces.IViewDeriver` interface. + + ``name`` should be the name of the view deriver. There are no + restrictions on the name of a view deriver. If left unspecified, the + name will be constructed from the name of the ``deriver``. + + The ``under`` and ``over`` options can be used to control the ordering + of view derivers by providing hints about where in the view pipeline + the deriver is used. Each option may be a string or a list of strings. + At least one view deriver in each, the over and under directions, must + exist to fully satisfy the constraints. + + ``under`` means closer to the user-defined :term:`view callable`, + and ``over`` means closer to view pipeline ingress. + + The default value for ``over`` is ``rendered_view`` and ``under`` is + ``decorated_view``. This places the deriver somewhere between the two + in the view pipeline. If the deriver should be placed elsewhere in the + pipeline, such as above ``decorated_view``, then you MUST also specify + ``under`` to something earlier in the order, or a + ``CyclicDependencyError`` will be raised when trying to sort the + derivers. + + See :ref:`view_derivers` for more information. + + """ + deriver = self.maybe_dotted(deriver) + + if name is None: + name = deriver.__name__ + + if name in (INGRESS, VIEW): + raise ConfigurationError('%s is a reserved view deriver name' + % name) + + if under is None: + under = 'decorated_view' + + if over is None: + over = 'rendered_view' + + over = as_sorted_tuple(over) + under = as_sorted_tuple(under) + + if INGRESS in over: + raise ConfigurationError('%s cannot be over INGRESS' % name) + + # ensure everything is always over mapped_view + if VIEW in over and name != 'mapped_view': + over = as_sorted_tuple(over + ('mapped_view',)) + + if VIEW in under: + raise ConfigurationError('%s cannot be under VIEW' % name) + if 'mapped_view' in under: + raise ConfigurationError('%s cannot be under "mapped_view"' % name) + + discriminator = ('view deriver', name) + intr = self.introspectable( + 'view derivers', + name, + name, + 'view deriver') + intr['name'] = name + intr['deriver'] = deriver + intr['under'] = under + intr['over'] = over + def register(): + derivers = self.registry.queryUtility(IViewDerivers) + if derivers is None: + derivers = TopologicalSorter( + default_before=None, + default_after=INGRESS, + first=INGRESS, + last=VIEW, + ) + self.registry.registerUtility(derivers, IViewDerivers) + derivers.add(name, deriver, before=over, after=under) + self.action(discriminator, register, introspectables=(intr,), + order=PHASE1_CONFIG) # must be registered before add_view + + def add_default_view_derivers(self): + d = pyramid.viewderivers + derivers = [ + ('secured_view', d.secured_view), + ('owrapped_view', d.owrapped_view), + ('http_cached_view', d.http_cached_view), + ('decorated_view', d.decorated_view), + ('rendered_view', d.rendered_view), + ('mapped_view', d.mapped_view), + ] + last = INGRESS + for name, deriver in derivers: + self.add_view_deriver( + deriver, + name=name, + under=last, + over=VIEW, + ) + last = name + + # leave the csrf_view loosely coupled to the rest of the pipeline + # by ensuring nothing in the default pipeline depends on the order + # of the csrf_view + self.add_view_deriver( + d.csrf_view, + 'csrf_view', + under='secured_view', + over='owrapped_view', + ) + + def derive_view(self, view, attr=None, renderer=None): + """ + Create a :term:`view callable` using the function, instance, + or class (or :term:`dotted Python name` referring to the same) + provided as ``view`` object. + + .. warning:: + + This method is typically only used by :app:`Pyramid` framework + extension authors, not by :app:`Pyramid` application developers. + + This is API is useful to framework extenders who create + pluggable systems which need to register 'proxy' view + callables for functions, instances, or classes which meet the + requirements of being a :app:`Pyramid` view callable. For + example, a ``some_other_framework`` function in another + framework may want to allow a user to supply a view callable, + but he may want to wrap the view callable in his own before + registering the wrapper as a :app:`Pyramid` view callable. + Because a :app:`Pyramid` view callable can be any of a + number of valid objects, the framework extender will not know + how to call the user-supplied object. Running it through + ``derive_view`` normalizes it to a callable which accepts two + arguments: ``context`` and ``request``. + + For example: + + .. code-block:: python + + def some_other_framework(user_supplied_view): + config = Configurator(reg) + proxy_view = config.derive_view(user_supplied_view) + def my_wrapper(context, request): + do_something_that_mutates(request) + return proxy_view(context, request) + config.add_view(my_wrapper) + + The ``view`` object provided should be one of the following: + + - A function or another non-class callable object that accepts + a :term:`request` as a single positional argument and which + returns a :term:`response` object. + + - A function or other non-class callable object that accepts + two positional arguments, ``context, request`` and which + returns a :term:`response` object. + + - A class which accepts a single positional argument in its + constructor named ``request``, and which has a ``__call__`` + method that accepts no arguments that returns a + :term:`response` object. + + - A class which accepts two positional arguments named + ``context, request``, and which has a ``__call__`` method + that accepts no arguments that returns a :term:`response` + object. + + - A :term:`dotted Python name` which refers to any of the + kinds of objects above. + + This API returns a callable which accepts the arguments + ``context, request`` and which returns the result of calling + the provided ``view`` object. + + The ``attr`` keyword argument is most useful when the view + object is a class. It names the method that should be used as + the callable. If ``attr`` is not provided, the attribute + effectively defaults to ``__call__``. See + :ref:`class_as_view` for more information. + + The ``renderer`` keyword argument should be a renderer + name. If supplied, it will cause the returned callable to use + a :term:`renderer` to convert the user-supplied view result to + a :term:`response` object. If a ``renderer`` argument is not + supplied, the user-supplied view must itself return a + :term:`response` object. """ + return self._derive_view(view, attr=attr, renderer=renderer) + + # b/w compat + def _derive_view(self, view, permission=None, predicates=(), + attr=None, renderer=None, wrapper_viewname=None, + viewname=None, accept=None, order=MAX_ORDER, + phash=DEFAULT_PHASH, decorator=None, route_name=None, + mapper=None, http_cache=None, context=None, + require_csrf=None, exception_only=False, + extra_options=None): + view = self.maybe_dotted(view) + mapper = self.maybe_dotted(mapper) + if isinstance(renderer, string_types): + renderer = renderers.RendererHelper( + name=renderer, package=self.package, + registry=self.registry) + if renderer is None: + # use default renderer if one exists + if self.registry.queryUtility(IRendererFactory) is not None: + renderer = renderers.RendererHelper( + name=None, + package=self.package, + registry=self.registry) + + options = dict( + view=view, + context=context, + permission=permission, + attr=attr, + renderer=renderer, + wrapper=wrapper_viewname, + name=viewname, + accept=accept, + mapper=mapper, + decorator=decorator, + http_cache=http_cache, + require_csrf=require_csrf, + route_name=route_name + ) + if extra_options: + options.update(extra_options) + + info = ViewDeriverInfo( + view=view, + registry=self.registry, + package=self.package, + predicates=predicates, + exception_only=exception_only, + options=options, + ) + + # order and phash are only necessary for the predicated view and + # are not really view deriver options + info.order = order + info.phash = phash + + return self._apply_view_derivers(info) + + @viewdefaults + @action_method + def add_forbidden_view( + self, + view=None, + attr=None, + renderer=None, + wrapper=None, + route_name=None, + request_type=None, + request_method=None, + request_param=None, + containment=None, + xhr=None, + accept=None, + header=None, + path_info=None, + custom_predicates=(), + decorator=None, + mapper=None, + match_param=None, + **view_options + ): + """ Add a forbidden view to the current configuration state. The + view will be called when Pyramid or application code raises a + :exc:`pyramid.httpexceptions.HTTPForbidden` exception and the set of + circumstances implied by the predicates provided are matched. The + simplest example is: + + .. code-block:: python + + def forbidden(request): + return Response('Forbidden', status='403 Forbidden') + + config.add_forbidden_view(forbidden) + + If ``view`` argument is not provided, the view callable defaults to + :func:`~pyramid.httpexceptions.default_exceptionresponse_view`. + + All arguments have the same meaning as + :meth:`pyramid.config.Configurator.add_view` and each predicate + argument restricts the set of circumstances under which this forbidden + view will be invoked. Unlike + :meth:`pyramid.config.Configurator.add_view`, this method will raise + an exception if passed ``name``, ``permission``, ``require_csrf``, + ``context``, ``for_``, or ``exception_only`` keyword arguments. These + argument values make no sense in the context of a forbidden + :term:`exception view`. + + .. versionadded:: 1.3 + + .. versionchanged:: 1.8 + + The view is created using ``exception_only=True``. + """ + for arg in ( + 'name', 'permission', 'context', 'for_', 'require_csrf', + 'exception_only', + ): + if arg in view_options: + raise ConfigurationError( + '%s may not be used as an argument to add_forbidden_view' + % (arg,)) + + if view is None: + view = default_exceptionresponse_view + + settings = dict( + view=view, + context=HTTPForbidden, + exception_only=True, + wrapper=wrapper, + request_type=request_type, + request_method=request_method, + request_param=request_param, + containment=containment, + xhr=xhr, + accept=accept, + header=header, + path_info=path_info, + custom_predicates=custom_predicates, + decorator=decorator, + mapper=mapper, + match_param=match_param, + route_name=route_name, + permission=NO_PERMISSION_REQUIRED, + require_csrf=False, + attr=attr, + renderer=renderer, + ) + settings.update(view_options) + return self.add_view(**settings) + + set_forbidden_view = add_forbidden_view # deprecated sorta-bw-compat alias + + @viewdefaults + @action_method + def add_notfound_view( + self, + view=None, + attr=None, + renderer=None, + wrapper=None, + route_name=None, + request_type=None, + request_method=None, + request_param=None, + containment=None, + xhr=None, + accept=None, + header=None, + path_info=None, + custom_predicates=(), + decorator=None, + mapper=None, + match_param=None, + append_slash=False, + **view_options + ): + """ Add a default :term:`Not Found View` to the current configuration + state. The view will be called when Pyramid or application code raises + an :exc:`pyramid.httpexceptions.HTTPNotFound` exception (e.g., when a + view cannot be found for the request). The simplest example is: + + .. code-block:: python + + def notfound(request): + return Response('Not Found', status='404 Not Found') + + config.add_notfound_view(notfound) + + If ``view`` argument is not provided, the view callable defaults to + :func:`~pyramid.httpexceptions.default_exceptionresponse_view`. + + All arguments except ``append_slash`` have the same meaning as + :meth:`pyramid.config.Configurator.add_view` and each predicate + argument restricts the set of circumstances under which this notfound + view will be invoked. Unlike + :meth:`pyramid.config.Configurator.add_view`, this method will raise + an exception if passed ``name``, ``permission``, ``require_csrf``, + ``context``, ``for_``, or ``exception_only`` keyword arguments. These + argument values make no sense in the context of a Not Found View. + + If ``append_slash`` is ``True``, when this Not Found View is invoked, + and the current path info does not end in a slash, the notfound logic + will attempt to find a :term:`route` that matches the request's path + info suffixed with a slash. If such a route exists, Pyramid will + issue a redirect to the URL implied by the route; if it does not, + Pyramid will return the result of the view callable provided as + ``view``, as normal. + + If the argument provided as ``append_slash`` is not a boolean but + instead implements :class:`~pyramid.interfaces.IResponse`, the + append_slash logic will behave as if ``append_slash=True`` was passed, + but the provided class will be used as the response class instead of + the default :class:`~pyramid.httpexceptions.HTTPTemporaryRedirect` + response class when a redirect is performed. For example: + + .. code-block:: python + + from pyramid.httpexceptions import HTTPMovedPermanently + config.add_notfound_view(append_slash=HTTPMovedPermanently) + + The above means that a redirect to a slash-appended route will be + attempted, but instead of :class:`~pyramid.httpexceptions.HTTPTemporaryRedirect` + being used, :class:`~pyramid.httpexceptions.HTTPMovedPermanently will + be used` for the redirect response if a slash-appended route is found. + + :class:`~pyramid.httpexceptions.HTTPTemporaryRedirect` class is used + as default response, which is equivalent to + :class:`~pyramid.httpexceptions.HTTPFound` with addition of redirecting + with the same HTTP method (useful when doing POST requests). + + .. versionadded:: 1.3 + + .. versionchanged:: 1.6 + + The ``append_slash`` argument was modified to allow any object that + implements the ``IResponse`` interface to specify the response class + used when a redirect is performed. + + .. versionchanged:: 1.8 + + The view is created using ``exception_only=True``. + + .. versionchanged: 1.10 + + Default response was changed from :class:`~pyramid.httpexceptions.HTTPFound` + to :class:`~pyramid.httpexceptions.HTTPTemporaryRedirect`. + + """ + for arg in ( + 'name', 'permission', 'context', 'for_', 'require_csrf', + 'exception_only', + ): + if arg in view_options: + raise ConfigurationError( + '%s may not be used as an argument to add_notfound_view' + % (arg,)) + + if view is None: + view = default_exceptionresponse_view + + settings = dict( + view=view, + context=HTTPNotFound, + exception_only=True, + wrapper=wrapper, + request_type=request_type, + request_method=request_method, + request_param=request_param, + containment=containment, + xhr=xhr, + accept=accept, + header=header, + path_info=path_info, + custom_predicates=custom_predicates, + decorator=decorator, + mapper=mapper, + match_param=match_param, + route_name=route_name, + permission=NO_PERMISSION_REQUIRED, + require_csrf=False, + ) + settings.update(view_options) + if append_slash: + view = self._derive_view(view, attr=attr, renderer=renderer) + if IResponse.implementedBy(append_slash): + view = AppendSlashNotFoundViewFactory( + view, redirect_class=append_slash, + ) + else: + view = AppendSlashNotFoundViewFactory(view) + settings['view'] = view + else: + settings['attr'] = attr + settings['renderer'] = renderer + return self.add_view(**settings) + + set_notfound_view = add_notfound_view # deprecated sorta-bw-compat alias + + @viewdefaults + @action_method + def add_exception_view( + self, + view=None, + context=None, + # force all other arguments to be specified as key=value + **view_options + ): + """ Add an :term:`exception view` for the specified ``exception`` to + the current configuration state. The view will be called when Pyramid + or application code raises the given exception. + + This method accepts almost all of the same arguments as + :meth:`pyramid.config.Configurator.add_view` except for ``name``, + ``permission``, ``for_``, ``require_csrf``, and ``exception_only``. + + By default, this method will set ``context=Exception``, thus + registering for most default Python exceptions. Any subclass of + ``Exception`` may be specified. + + .. versionadded:: 1.8 + """ + for arg in ( + 'name', 'for_', 'exception_only', 'require_csrf', 'permission', + ): + if arg in view_options: + raise ConfigurationError( + '%s may not be used as an argument to add_exception_view' + % (arg,)) + if context is None: + context = Exception + view_options.update(dict( + view=view, + context=context, + exception_only=True, + permission=NO_PERMISSION_REQUIRED, + require_csrf=False, + )) + return self.add_view(**view_options) + + @action_method + def set_view_mapper(self, mapper): + """ + Setting a :term:`view mapper` makes it possible to make use of + :term:`view callable` objects which implement different call + signatures than the ones supported by :app:`Pyramid` as described in + its narrative documentation. + + The ``mapper`` argument should be an object implementing + :class:`pyramid.interfaces.IViewMapperFactory` or a :term:`dotted + Python name` to such an object. The provided ``mapper`` will become + the default view mapper to be used by all subsequent :term:`view + configuration` registrations. + + .. seealso:: + + See also :ref:`using_a_view_mapper`. + + .. note:: + + Using the ``default_view_mapper`` argument to the + :class:`pyramid.config.Configurator` constructor + can be used to achieve the same purpose. + """ + mapper = self.maybe_dotted(mapper) + def register(): + self.registry.registerUtility(mapper, IViewMapperFactory) + # IViewMapperFactory is looked up as the result of view config + # in phase 3 + intr = self.introspectable('view mappers', + IViewMapperFactory, + self.object_description(mapper), + 'default view mapper') + intr['mapper'] = mapper + self.action(IViewMapperFactory, register, order=PHASE1_CONFIG, + introspectables=(intr,)) + + @action_method + def add_static_view(self, name, path, **kw): + """ Add a view used to render static assets such as images + and CSS files. + + The ``name`` argument is a string representing an + application-relative local URL prefix. It may alternately be a full + URL. + + The ``path`` argument is the path on disk where the static files + reside. This can be an absolute path, a package-relative path, or a + :term:`asset specification`. + + The ``cache_max_age`` keyword argument is input to set the + ``Expires`` and ``Cache-Control`` headers for static assets served. + Note that this argument has no effect when the ``name`` is a *url + prefix*. By default, this argument is ``None``, meaning that no + particular Expires or Cache-Control headers are set in the response. + + The ``permission`` keyword argument is used to specify the + :term:`permission` required by a user to execute the static view. By + default, it is the string + :data:`pyramid.security.NO_PERMISSION_REQUIRED`, a special sentinel + which indicates that, even if a :term:`default permission` exists for + the current application, the static view should be renderered to + completely anonymous users. This default value is permissive + because, in most web apps, static assets seldom need protection from + viewing. If ``permission`` is specified, the security checking will + be performed against the default root factory ACL. + + Any other keyword arguments sent to ``add_static_view`` are passed on + to :meth:`pyramid.config.Configurator.add_route` (e.g. ``factory``, + perhaps to define a custom factory with a custom ACL for this static + view). + + *Usage* + + The ``add_static_view`` function is typically used in conjunction + with the :meth:`pyramid.request.Request.static_url` method. + ``add_static_view`` adds a view which renders a static asset when + some URL is visited; :meth:`pyramid.request.Request.static_url` + generates a URL to that asset. + + The ``name`` argument to ``add_static_view`` is usually a simple URL + prefix (e.g. ``'images'``). When this is the case, the + :meth:`pyramid.request.Request.static_url` API will generate a URL + which points to a Pyramid view, which will serve up a set of assets + that live in the package itself. For example: + + .. code-block:: python + + add_static_view('images', 'mypackage:images/') + + Code that registers such a view can generate URLs to the view via + :meth:`pyramid.request.Request.static_url`: + + .. code-block:: python + + request.static_url('mypackage:images/logo.png') + + When ``add_static_view`` is called with a ``name`` argument that + represents a URL prefix, as it is above, subsequent calls to + :meth:`pyramid.request.Request.static_url` with paths that start with + the ``path`` argument passed to ``add_static_view`` will generate a + URL something like ``http:///images/logo.png``, + which will cause the ``logo.png`` file in the ``images`` subdirectory + of the ``mypackage`` package to be served. + + ``add_static_view`` can alternately be used with a ``name`` argument + which is a *URL*, causing static assets to be served from an external + webserver. This happens when the ``name`` argument is a fully + qualified URL (e.g. starts with ``http://`` or similar). In this + mode, the ``name`` is used as the prefix of the full URL when + generating a URL using :meth:`pyramid.request.Request.static_url`. + Furthermore, if a protocol-relative URL (e.g. ``//example.com/images``) + is used as the ``name`` argument, the generated URL will use the + protocol of the request (http or https, respectively). + + For example, if ``add_static_view`` is called like so: + + .. code-block:: python + + add_static_view('http://example.com/images', 'mypackage:images/') + + Subsequently, the URLs generated by + :meth:`pyramid.request.Request.static_url` for that static view will + be prefixed with ``http://example.com/images`` (the external webserver + listening on ``example.com`` must be itself configured to respond + properly to such a request.): + + .. code-block:: python + + static_url('mypackage:images/logo.png', request) + + See :ref:`static_assets_section` for more information. + """ + spec = self._make_spec(path) + info = self._get_static_info() + info.add(self, name, spec, **kw) + + def add_cache_buster(self, path, cachebust, explicit=False): + """ + Add a cache buster to a set of files on disk. + + The ``path`` should be the path on disk where the static files + reside. This can be an absolute path, a package-relative path, or a + :term:`asset specification`. + + The ``cachebust`` argument may be set to cause + :meth:`~pyramid.request.Request.static_url` to use cache busting when + generating URLs. See :ref:`cache_busting` for general information + about cache busting. The value of the ``cachebust`` argument must + be an object which implements + :class:`~pyramid.interfaces.ICacheBuster`. + + If ``explicit`` is set to ``True`` then the ``path`` for the cache + buster will be matched based on the ``rawspec`` instead of the + ``pathspec`` as defined in the + :class:`~pyramid.interfaces.ICacheBuster` interface. + Default: ``False``. + + """ + spec = self._make_spec(path) + info = self._get_static_info() + info.add_cache_buster(self, spec, cachebust, explicit=explicit) + + def _get_static_info(self): + info = self.registry.queryUtility(IStaticURLInfo) + if info is None: + info = StaticURLInfo() + self.registry.registerUtility(info, IStaticURLInfo) + return info + +def isexception(o): + if IInterface.providedBy(o): + if IException.isEqualOrExtendedBy(o): + return True + return ( + isinstance(o, Exception) or + (inspect.isclass(o) and (issubclass(o, Exception))) + ) + +def runtime_exc_view(view, excview): + # create a view callable which can pretend to be both a normal view + # and an exception view, dispatching to the appropriate one based + # on the state of request.exception + def wrapper_view(context, request): + if getattr(request, 'exception', None): + return excview(context, request) + return view(context, request) + + # these constants are the same between the two views + wrapper_view.__wraps__ = wrapper_view + wrapper_view.__original_view__ = getattr(view, '__original_view__', view) + wrapper_view.__module__ = view.__module__ + wrapper_view.__doc__ = view.__doc__ + wrapper_view.__name__ = view.__name__ + + wrapper_view.__accept__ = getattr(view, '__accept__', None) + wrapper_view.__order__ = getattr(view, '__order__', MAX_ORDER) + wrapper_view.__phash__ = getattr(view, '__phash__', DEFAULT_PHASH) + wrapper_view.__view_attr__ = getattr(view, '__view_attr__', None) + wrapper_view.__permission__ = getattr(view, '__permission__', None) + + def wrap_fn(attr): + def wrapper(context, request): + if getattr(request, 'exception', None): + selected_view = excview + else: + selected_view = view + fn = getattr(selected_view, attr, None) + if fn is not None: + return fn(context, request) + return wrapper + + # these methods are dynamic per-request and should dispatch to their + # respective views based on whether it's an exception or not + wrapper_view.__call_permissive__ = wrap_fn('__call_permissive__') + wrapper_view.__permitted__ = wrap_fn('__permitted__') + wrapper_view.__predicated__ = wrap_fn('__predicated__') + wrapper_view.__predicates__ = wrap_fn('__predicates__') + return wrapper_view + +@implementer(IViewDeriverInfo) +class ViewDeriverInfo(object): + def __init__(self, + view, + registry, + package, + predicates, + exception_only, + options, + ): + self.original_view = view + self.registry = registry + self.package = package + self.predicates = predicates or [] + self.options = options or {} + self.exception_only = exception_only + + @reify + def settings(self): + return self.registry.settings + +@implementer(IStaticURLInfo) +class StaticURLInfo(object): + def __init__(self): + self.registrations = [] + self.cache_busters = [] + + def generate(self, path, request, **kw): + for (url, spec, route_name) in self.registrations: + if path.startswith(spec): + subpath = path[len(spec):] + if WIN: # pragma: no cover + subpath = subpath.replace('\\', '/') # windows + if self.cache_busters: + subpath, kw = self._bust_asset_path( + request, spec, subpath, kw) + if url is None: + kw['subpath'] = subpath + return request.route_url(route_name, **kw) + else: + app_url, qs, anchor = parse_url_overrides(request, kw) + parsed = url_parse(url) + if not parsed.scheme: + url = urlparse.urlunparse(parsed._replace( + scheme=request.environ['wsgi.url_scheme'])) + subpath = url_quote(subpath) + result = urljoin(url, subpath) + return result + qs + anchor + + raise ValueError('No static URL definition matching %s' % path) + + def add(self, config, name, spec, **extra): + # This feature only allows for the serving of a directory and + # the files contained within, not of a single asset; + # appending a slash here if the spec doesn't have one is + # required for proper prefix matching done in ``generate`` + # (``subpath = path[len(spec):]``). + if os.path.isabs(spec): # FBO windows + sep = os.sep + else: + sep = '/' + if not spec.endswith(sep) and not spec.endswith(':'): + spec = spec + sep + + # we also make sure the name ends with a slash, purely as a + # convenience: a name that is a url is required to end in a + # slash, so that ``urljoin(name, subpath))`` will work above + # when the name is a URL, and it doesn't hurt things for it to + # have a name that ends in a slash if it's used as a route + # name instead of a URL. + if not name.endswith('/'): + # make sure it ends with a slash + name = name + '/' + + if url_parse(name).netloc: + # it's a URL + # url, spec, route_name + url = name + route_name = None + else: + # it's a view name + url = None + cache_max_age = extra.pop('cache_max_age', None) + + # create a view + view = static_view(spec, cache_max_age=cache_max_age, + use_subpath=True) + + # Mutate extra to allow factory, etc to be passed through here. + # Treat permission specially because we'd like to default to + # permissiveness (see docs of config.add_static_view). + permission = extra.pop('permission', None) + if permission is None: + permission = NO_PERMISSION_REQUIRED + + context = extra.pop('context', None) + if context is None: + context = extra.pop('for_', None) + + renderer = extra.pop('renderer', None) + + # register a route using the computed view, permission, and + # pattern, plus any extras passed to us via add_static_view + pattern = "%s*subpath" % name # name already ends with slash + if config.route_prefix: + route_name = '__%s/%s' % (config.route_prefix, name) + else: + route_name = '__%s' % name + config.add_route(route_name, pattern, **extra) + config.add_view( + route_name=route_name, + view=view, + permission=permission, + context=context, + renderer=renderer, + ) + + def register(): + registrations = self.registrations + + names = [t[0] for t in registrations] + + if name in names: + idx = names.index(name) + registrations.pop(idx) + + # url, spec, route_name + registrations.append((url, spec, route_name)) + + intr = config.introspectable('static views', + name, + 'static view for %r' % name, + 'static view') + intr['name'] = name + intr['spec'] = spec + + config.action(None, callable=register, introspectables=(intr,)) + + def add_cache_buster(self, config, spec, cachebust, explicit=False): + # ensure the spec always has a trailing slash as we only support + # adding cache busters to folders, not files + if os.path.isabs(spec): # FBO windows + sep = os.sep + else: + sep = '/' + if not spec.endswith(sep) and not spec.endswith(':'): + spec = spec + sep + + def register(): + if config.registry.settings.get('pyramid.prevent_cachebust'): + return + + cache_busters = self.cache_busters + + # find duplicate cache buster (old_idx) + # and insertion location (new_idx) + new_idx, old_idx = len(cache_busters), None + for idx, (spec_, cb_, explicit_) in enumerate(cache_busters): + # if we find an identical (spec, explicit) then use it + if spec == spec_ and explicit == explicit_: + old_idx = new_idx = idx + break + + # past all explicit==False specs then add to the end + elif not explicit and explicit_: + new_idx = idx + break + + # explicit matches and spec is shorter + elif explicit == explicit_ and len(spec) < len(spec_): + new_idx = idx + break + + if old_idx is not None: + cache_busters.pop(old_idx) + + cache_busters.insert(new_idx, (spec, cachebust, explicit)) + + intr = config.introspectable('cache busters', + spec, + 'cache buster for %r' % spec, + 'cache buster') + intr['cachebust'] = cachebust + intr['path'] = spec + intr['explicit'] = explicit + + config.action(None, callable=register, introspectables=(intr,)) + + def _bust_asset_path(self, request, spec, subpath, kw): + registry = request.registry + pkg_name, pkg_subpath = resolve_asset_spec(spec) + rawspec = None + + if pkg_name is not None: + pathspec = '{0}:{1}{2}'.format(pkg_name, pkg_subpath, subpath) + overrides = registry.queryUtility(IPackageOverrides, name=pkg_name) + if overrides is not None: + resource_name = posixpath.join(pkg_subpath, subpath) + sources = overrides.filtered_sources(resource_name) + for source, filtered_path in sources: + rawspec = source.get_path(filtered_path) + if hasattr(source, 'pkg_name'): + rawspec = '{0}:{1}'.format(source.pkg_name, rawspec) + break + + else: + pathspec = pkg_subpath + subpath + + if rawspec is None: + rawspec = pathspec + + kw['pathspec'] = pathspec + kw['rawspec'] = rawspec + for spec_, cachebust, explicit in reversed(self.cache_busters): + if ( + (explicit and rawspec.startswith(spec_)) or + (not explicit and pathspec.startswith(spec_)) + ): + subpath, kw = cachebust(request, subpath, kw) + break + return subpath, kw diff --git a/src/pyramid/config/zca.py b/src/pyramid/config/zca.py new file mode 100644 index 000000000..bcd5c31e3 --- /dev/null +++ b/src/pyramid/config/zca.py @@ -0,0 +1,20 @@ +from pyramid.threadlocal import get_current_registry + +class ZCAConfiguratorMixin(object): + def hook_zca(self): + """ Call :func:`zope.component.getSiteManager.sethook` with the + argument :data:`pyramid.threadlocal.get_current_registry`, causing + the :term:`Zope Component Architecture` 'global' APIs such as + :func:`zope.component.getSiteManager`, + :func:`zope.component.getAdapter` and others to use the + :app:`Pyramid` :term:`application registry` rather than the Zope + 'global' registry.""" + from zope.component import getSiteManager + getSiteManager.sethook(get_current_registry) + + def unhook_zca(self): + """ Call :func:`zope.component.getSiteManager.reset` to undo the + action of :meth:`pyramid.config.Configurator.hook_zca`.""" + from zope.component import getSiteManager + getSiteManager.reset() + diff --git a/src/pyramid/csrf.py b/src/pyramid/csrf.py new file mode 100644 index 000000000..da171d9af --- /dev/null +++ b/src/pyramid/csrf.py @@ -0,0 +1,336 @@ +import uuid + +from webob.cookies import CookieProfile +from zope.interface import implementer + + +from pyramid.compat import ( + bytes_, + urlparse, + text_, +) +from pyramid.exceptions import ( + BadCSRFOrigin, + BadCSRFToken, +) +from pyramid.interfaces import ICSRFStoragePolicy +from pyramid.settings import aslist +from pyramid.util import ( + SimpleSerializer, + is_same_domain, + strings_differ +) + + +@implementer(ICSRFStoragePolicy) +class LegacySessionCSRFStoragePolicy(object): + """ A CSRF storage policy that defers control of CSRF storage to the + session. + + This policy maintains compatibility with legacy ISession implementations + that know how to manage CSRF tokens themselves via + ``ISession.new_csrf_token`` and ``ISession.get_csrf_token``. + + Note that using this CSRF implementation requires that + a :term:`session factory` is configured. + + .. versionadded:: 1.9 + + """ + def new_csrf_token(self, request): + """ Sets a new CSRF token into the session and returns it. """ + return request.session.new_csrf_token() + + def get_csrf_token(self, request): + """ Returns the currently active CSRF token from the session, + generating a new one if needed.""" + return request.session.get_csrf_token() + + def check_csrf_token(self, request, supplied_token): + """ Returns ``True`` if the ``supplied_token`` is valid.""" + expected_token = self.get_csrf_token(request) + return not strings_differ( + bytes_(expected_token), bytes_(supplied_token)) + + +@implementer(ICSRFStoragePolicy) +class SessionCSRFStoragePolicy(object): + """ A CSRF storage policy that persists the CSRF token in the session. + + Note that using this CSRF implementation requires that + a :term:`session factory` is configured. + + ``key`` + + The session key where the CSRF token will be stored. + Default: `_csrft_`. + + .. versionadded:: 1.9 + + """ + _token_factory = staticmethod(lambda: text_(uuid.uuid4().hex)) + + def __init__(self, key='_csrft_'): + self.key = key + + def new_csrf_token(self, request): + """ Sets a new CSRF token into the session and returns it. """ + token = self._token_factory() + request.session[self.key] = token + return token + + def get_csrf_token(self, request): + """ Returns the currently active CSRF token from the session, + generating a new one if needed.""" + token = request.session.get(self.key, None) + if not token: + token = self.new_csrf_token(request) + return token + + def check_csrf_token(self, request, supplied_token): + """ Returns ``True`` if the ``supplied_token`` is valid.""" + expected_token = self.get_csrf_token(request) + return not strings_differ( + bytes_(expected_token), bytes_(supplied_token)) + + +@implementer(ICSRFStoragePolicy) +class CookieCSRFStoragePolicy(object): + """ An alternative CSRF implementation that stores its information in + unauthenticated cookies, known as the 'Double Submit Cookie' method in the + `OWASP CSRF guidelines `_. This gives some additional flexibility with + regards to scaling as the tokens can be generated and verified by a + front-end server. + + .. versionadded:: 1.9 + + .. versionchanged: 1.10 + + Added the ``samesite`` option and made the default ``'Lax'``. + + """ + _token_factory = staticmethod(lambda: text_(uuid.uuid4().hex)) + + def __init__(self, cookie_name='csrf_token', secure=False, httponly=False, + domain=None, max_age=None, path='/', samesite='Lax'): + serializer = SimpleSerializer() + self.cookie_profile = CookieProfile( + cookie_name=cookie_name, + secure=secure, + max_age=max_age, + httponly=httponly, + path=path, + domains=[domain], + serializer=serializer, + samesite=samesite, + ) + self.cookie_name = cookie_name + + def new_csrf_token(self, request): + """ Sets a new CSRF token into the request and returns it. """ + token = self._token_factory() + request.cookies[self.cookie_name] = token + def set_cookie(request, response): + self.cookie_profile.set_cookies( + response, + token, + ) + request.add_response_callback(set_cookie) + return token + + def get_csrf_token(self, request): + """ Returns the currently active CSRF token by checking the cookies + sent with the current request.""" + bound_cookies = self.cookie_profile.bind(request) + token = bound_cookies.get_value() + if not token: + token = self.new_csrf_token(request) + return token + + def check_csrf_token(self, request, supplied_token): + """ Returns ``True`` if the ``supplied_token`` is valid.""" + expected_token = self.get_csrf_token(request) + return not strings_differ( + bytes_(expected_token), bytes_(supplied_token)) + + +def get_csrf_token(request): + """ Get the currently active CSRF token for the request passed, generating + a new one using ``new_csrf_token(request)`` if one does not exist. This + calls the equivalent method in the chosen CSRF protection implementation. + + .. versionadded :: 1.9 + + """ + registry = request.registry + csrf = registry.getUtility(ICSRFStoragePolicy) + return csrf.get_csrf_token(request) + + +def new_csrf_token(request): + """ Generate a new CSRF token for the request passed and persist it in an + implementation defined manner. This calls the equivalent method in the + chosen CSRF protection implementation. + + .. versionadded :: 1.9 + + """ + registry = request.registry + csrf = registry.getUtility(ICSRFStoragePolicy) + return csrf.new_csrf_token(request) + + +def check_csrf_token(request, + token='csrf_token', + header='X-CSRF-Token', + raises=True): + """ Check the CSRF token returned by the + :class:`pyramid.interfaces.ICSRFStoragePolicy` implementation against the + value in ``request.POST.get(token)`` (if a POST request) or + ``request.headers.get(header)``. If a ``token`` keyword is not supplied to + this function, the string ``csrf_token`` will be used to look up the token + in ``request.POST``. If a ``header`` keyword is not supplied to this + function, the string ``X-CSRF-Token`` will be used to look up the token in + ``request.headers``. + + If the value supplied by post or by header cannot be verified by the + :class:`pyramid.interfaces.ICSRFStoragePolicy`, and ``raises`` is + ``True``, this function will raise an + :exc:`pyramid.exceptions.BadCSRFToken` exception. If the values differ + and ``raises`` is ``False``, this function will return ``False``. If the + CSRF check is successful, this function will return ``True`` + unconditionally. + + See :ref:`auto_csrf_checking` for information about how to secure your + application automatically against CSRF attacks. + + .. versionadded:: 1.4a2 + + .. versionchanged:: 1.7a1 + A CSRF token passed in the query string of the request is no longer + considered valid. It must be passed in either the request body or + a header. + + .. versionchanged:: 1.9 + Moved from :mod:`pyramid.session` to :mod:`pyramid.csrf` and updated + to use the configured :class:`pyramid.interfaces.ICSRFStoragePolicy` to + verify the CSRF token. + + """ + supplied_token = "" + # We first check the headers for a csrf token, as that is significantly + # cheaper than checking the POST body + if header is not None: + supplied_token = request.headers.get(header, "") + + # If this is a POST/PUT/etc request, then we'll check the body to see if it + # has a token. We explicitly use request.POST here because CSRF tokens + # should never appear in an URL as doing so is a security issue. We also + # explicitly check for request.POST here as we do not support sending form + # encoded data over anything but a request.POST. + if supplied_token == "" and token is not None: + supplied_token = request.POST.get(token, "") + + policy = request.registry.getUtility(ICSRFStoragePolicy) + if not policy.check_csrf_token(request, text_(supplied_token)): + if raises: + raise BadCSRFToken('check_csrf_token(): Invalid token') + return False + return True + + +def check_csrf_origin(request, trusted_origins=None, raises=True): + """ + Check the ``Origin`` of the request to see if it is a cross site request or + not. + + If the value supplied by the ``Origin`` or ``Referer`` header isn't one of the + trusted origins and ``raises`` is ``True``, this function will raise a + :exc:`pyramid.exceptions.BadCSRFOrigin` exception, but if ``raises`` is + ``False``, this function will return ``False`` instead. If the CSRF origin + checks are successful this function will return ``True`` unconditionally. + + Additional trusted origins may be added by passing a list of domain (and + ports if non-standard like ``['example.com', 'dev.example.com:8080']``) in + with the ``trusted_origins`` parameter. If ``trusted_origins`` is ``None`` + (the default) this list of additional domains will be pulled from the + ``pyramid.csrf_trusted_origins`` setting. + + Note that this function will do nothing if ``request.scheme`` is not + ``https``. + + .. versionadded:: 1.7 + + .. versionchanged:: 1.9 + Moved from :mod:`pyramid.session` to :mod:`pyramid.csrf` + + """ + def _fail(reason): + if raises: + raise BadCSRFOrigin(reason) + else: + return False + + if request.scheme == "https": + # Suppose user visits http://example.com/ + # An active network attacker (man-in-the-middle, MITM) sends a + # POST form that targets https://example.com/detonate-bomb/ and + # submits it via JavaScript. + # + # The attacker will need to provide a CSRF cookie and token, but + # that's no problem for a MITM when we cannot make any assumptions + # about what kind of session storage is being used. So the MITM can + # circumvent the CSRF protection. This is true for any HTTP connection, + # but anyone using HTTPS expects better! For this reason, for + # https://example.com/ we need additional protection that treats + # http://example.com/ as completely untrusted. Under HTTPS, + # Barth et al. found that the Referer header is missing for + # same-domain requests in only about 0.2% of cases or less, so + # we can use strict Referer checking. + + # Determine the origin of this request + origin = request.headers.get("Origin") + if origin is None: + origin = request.referrer + + # Fail if we were not able to locate an origin at all + if not origin: + return _fail("Origin checking failed - no Origin or Referer.") + + # Parse our origin so we we can extract the required information from + # it. + originp = urlparse.urlparse(origin) + + # Ensure that our Referer is also secure. + if originp.scheme != "https": + return _fail( + "Referer checking failed - Referer is insecure while host is " + "secure." + ) + + # Determine which origins we trust, which by default will include the + # current origin. + if trusted_origins is None: + trusted_origins = aslist( + request.registry.settings.get( + "pyramid.csrf_trusted_origins", []) + ) + + if request.host_port not in set(["80", "443"]): + trusted_origins.append("{0.domain}:{0.host_port}".format(request)) + else: + trusted_origins.append(request.domain) + + # Actually check to see if the request's origin matches any of our + # trusted origins. + if not any(is_same_domain(originp.netloc, host) + for host in trusted_origins): + reason = ( + "Referer checking failed - {0} does not match any trusted " + "origins." + ) + return _fail(reason.format(origin)) + + return True diff --git a/src/pyramid/decorator.py b/src/pyramid/decorator.py new file mode 100644 index 000000000..065a3feed --- /dev/null +++ b/src/pyramid/decorator.py @@ -0,0 +1,45 @@ +from functools import update_wrapper + + +class reify(object): + """ Use as a class method decorator. It operates almost exactly like the + Python ``@property`` decorator, but it puts the result of the method it + decorates into the instance dict after the first call, effectively + replacing the function it decorates with an instance variable. It is, in + Python parlance, a non-data descriptor. The following is an example and + its usage: + + .. doctest:: + + >>> from pyramid.decorator import reify + + >>> class Foo(object): + ... @reify + ... def jammy(self): + ... print('jammy called') + ... return 1 + + >>> f = Foo() + >>> v = f.jammy + jammy called + >>> print(v) + 1 + >>> f.jammy + 1 + >>> # jammy func not called the second time; it replaced itself with 1 + >>> # Note: reassignment is possible + >>> f.jammy = 2 + >>> f.jammy + 2 + """ + def __init__(self, wrapped): + self.wrapped = wrapped + update_wrapper(self, wrapped) + + def __get__(self, inst, objtype=None): + if inst is None: + return self + val = self.wrapped(inst) + setattr(inst, self.wrapped.__name__, val) + return val + diff --git a/src/pyramid/encode.py b/src/pyramid/encode.py new file mode 100644 index 000000000..73ff14e62 --- /dev/null +++ b/src/pyramid/encode.py @@ -0,0 +1,84 @@ +from pyramid.compat import ( + text_type, + binary_type, + is_nonstr_iter, + url_quote as _url_quote, + url_quote_plus as _quote_plus, + ) + +def url_quote(val, safe=''): # bw compat api + cls = val.__class__ + if cls is text_type: + val = val.encode('utf-8') + elif cls is not binary_type: + val = str(val).encode('utf-8') + return _url_quote(val, safe=safe) + +# bw compat api (dnr) +def quote_plus(val, safe=''): + cls = val.__class__ + if cls is text_type: + val = val.encode('utf-8') + elif cls is not binary_type: + val = str(val).encode('utf-8') + return _quote_plus(val, safe=safe) + +def urlencode(query, doseq=True, quote_via=quote_plus): + """ + An alternate implementation of Python's stdlib + :func:`urllib.parse.urlencode` function which accepts unicode keys and + values within the ``query`` dict/sequence; all Unicode keys and values are + first converted to UTF-8 before being used to compose the query string. + + The value of ``query`` must be a sequence of two-tuples + representing key/value pairs *or* an object (often a dictionary) + with an ``.items()`` method that returns a sequence of two-tuples + representing key/value pairs. + + For minimal calling convention backwards compatibility, this + version of urlencode accepts *but ignores* a second argument + conventionally named ``doseq``. The Python stdlib version behaves + differently when ``doseq`` is False and when a sequence is + presented as one of the values. This version always behaves in + the ``doseq=True`` mode, no matter what the value of the second + argument. + + Both the key and value are encoded using the ``quote_via`` function which + by default is using a similar algorithm to :func:`urllib.parse.quote_plus` + which converts spaces into '+' characters and '/' into '%2F'. + + .. versionchanged:: 1.5 + In a key/value pair, if the value is ``None`` then it will be + dropped from the resulting output. + + .. versionchanged:: 1.9 + Added the ``quote_via`` argument to allow alternate quoting algorithms + to be used. + + """ + try: + # presumed to be a dictionary + query = query.items() + except AttributeError: + pass + + result = '' + prefix = '' + + for (k, v) in query: + k = quote_via(k) + + if is_nonstr_iter(v): + for x in v: + x = quote_via(x) + result += '%s%s=%s' % (prefix, k, x) + prefix = '&' + elif v is None: + result += '%s%s=' % (prefix, k) + else: + v = quote_via(v) + result += '%s%s=%s' % (prefix, k, v) + + prefix = '&' + + return result diff --git a/src/pyramid/events.py b/src/pyramid/events.py new file mode 100644 index 000000000..93fc127a1 --- /dev/null +++ b/src/pyramid/events.py @@ -0,0 +1,289 @@ +import venusian + +from zope.interface import ( + implementer, + Interface + ) + +from pyramid.interfaces import ( + IContextFound, + INewRequest, + INewResponse, + IApplicationCreated, + IBeforeRender, + IBeforeTraversal, + ) + +class subscriber(object): + """ Decorator activated via a :term:`scan` which treats the function + being decorated as an event subscriber for the set of interfaces passed + as ``*ifaces`` and the set of predicate terms passed as ``**predicates`` + to the decorator constructor. + + For example: + + .. code-block:: python + + from pyramid.events import NewRequest + from pyramid.events import subscriber + + @subscriber(NewRequest) + def mysubscriber(event): + event.request.foo = 1 + + More than one event type can be passed as a constructor argument. The + decorated subscriber will be called for each event type. + + .. code-block:: python + + from pyramid.events import NewRequest, NewResponse + from pyramid.events import subscriber + + @subscriber(NewRequest, NewResponse) + def mysubscriber(event): + print(event) + + When the ``subscriber`` decorator is used without passing an arguments, + the function it decorates is called for every event sent: + + .. code-block:: python + + from pyramid.events import subscriber + + @subscriber() + def mysubscriber(event): + print(event) + + This method will have no effect until a :term:`scan` is performed + against the package or module which contains it, ala: + + .. code-block:: python + + from pyramid.config import Configurator + config = Configurator() + config.scan('somepackage_containing_subscribers') + + Any ``**predicate`` arguments will be passed along to + :meth:`pyramid.config.Configurator.add_subscriber`. See + :ref:`subscriber_predicates` for a description of how predicates can + narrow the set of circumstances in which a subscriber will be called. + + Two additional keyword arguments which will be passed to the + :term:`venusian` ``attach`` function are ``_depth`` and ``_category``. + + ``_depth`` is provided for people who wish to reuse this class from another + decorator. The default value is ``0`` and should be specified relative to + the ``subscriber`` invocation. It will be passed in to the + :term:`venusian` ``attach`` function as the depth of the callstack when + Venusian checks if the decorator is being used in a class or module + context. It's not often used, but it can be useful in this circumstance. + + ``_category`` sets the decorator category name. It can be useful in + combination with the ``category`` argument of ``scan`` to control which + views should be processed. + + See the :py:func:`venusian.attach` function in Venusian for more + information about the ``_depth`` and ``_category`` arguments. + + .. versionchanged:: 1.9.1 + Added the ``_depth`` and ``_category`` arguments. + + """ + venusian = venusian # for unit testing + + def __init__(self, *ifaces, **predicates): + self.ifaces = ifaces + self.predicates = predicates + self.depth = predicates.pop('_depth', 0) + self.category = predicates.pop('_category', 'pyramid') + + def register(self, scanner, name, wrapped): + config = scanner.config + for iface in self.ifaces or (Interface,): + config.add_subscriber(wrapped, iface, **self.predicates) + + def __call__(self, wrapped): + self.venusian.attach(wrapped, self.register, category=self.category, + depth=self.depth + 1) + return wrapped + +@implementer(INewRequest) +class NewRequest(object): + """ An instance of this class is emitted as an :term:`event` + whenever :app:`Pyramid` begins to process a new request. The + event instance has an attribute, ``request``, which is a + :term:`request` object. This event class implements the + :class:`pyramid.interfaces.INewRequest` interface.""" + def __init__(self, request): + self.request = request + +@implementer(INewResponse) +class NewResponse(object): + """ An instance of this class is emitted as an :term:`event` + whenever any :app:`Pyramid` :term:`view` or :term:`exception + view` returns a :term:`response`. + + The instance has two attributes:``request``, which is the request + which caused the response, and ``response``, which is the response + object returned by a view or renderer. + + If the ``response`` was generated by an :term:`exception view`, the + request will have an attribute named ``exception``, which is the + exception object which caused the exception view to be executed. If the + response was generated by a 'normal' view, this attribute of the request + will be ``None``. + + This event will not be generated if a response cannot be created due to + an exception that is not caught by an exception view (no response is + created under this circumstace). + + This class implements the + :class:`pyramid.interfaces.INewResponse` interface. + + .. note:: + + Postprocessing a response is usually better handled in a WSGI + :term:`middleware` component than in subscriber code that is + called by a :class:`pyramid.interfaces.INewResponse` event. + The :class:`pyramid.interfaces.INewResponse` event exists + almost purely for symmetry with the + :class:`pyramid.interfaces.INewRequest` event. + """ + def __init__(self, request, response): + self.request = request + self.response = response + +@implementer(IBeforeTraversal) +class BeforeTraversal(object): + """ + An instance of this class is emitted as an :term:`event` after the + :app:`Pyramid` :term:`router` has attempted to find a :term:`route` object + but before any traversal or view code is executed. The instance has an + attribute, ``request``, which is the request object generated by + :app:`Pyramid`. + + Notably, the request object **may** have an attribute named + ``matched_route``, which is the matched route if found. If no route + matched, this attribute is not available. + + This class implements the :class:`pyramid.interfaces.IBeforeTraversal` + interface. + """ + + def __init__(self, request): + self.request = request + +@implementer(IContextFound) +class ContextFound(object): + """ An instance of this class is emitted as an :term:`event` after + the :app:`Pyramid` :term:`router` finds a :term:`context` + object (after it performs traversal) but before any view code is + executed. The instance has an attribute, ``request``, which is + the request object generated by :app:`Pyramid`. + + Notably, the request object will have an attribute named + ``context``, which is the context that will be provided to the + view which will eventually be called, as well as other attributes + attached by context-finding code. + + This class implements the + :class:`pyramid.interfaces.IContextFound` interface. + + .. note:: + + As of :app:`Pyramid` 1.0, for backwards compatibility purposes, this + event may also be imported as :class:`pyramid.events.AfterTraversal`. + """ + def __init__(self, request): + self.request = request + +AfterTraversal = ContextFound # b/c as of 1.0 + +@implementer(IApplicationCreated) +class ApplicationCreated(object): + """ An instance of this class is emitted as an :term:`event` when + the :meth:`pyramid.config.Configurator.make_wsgi_app` is + called. The instance has an attribute, ``app``, which is an + instance of the :term:`router` that will handle WSGI requests. + This class implements the + :class:`pyramid.interfaces.IApplicationCreated` interface. + + .. note:: + + For backwards compatibility purposes, this class can also be imported as + :class:`pyramid.events.WSGIApplicationCreatedEvent`. This was the name + of the event class before :app:`Pyramid` 1.0. + """ + def __init__(self, app): + self.app = app + self.object = app + +WSGIApplicationCreatedEvent = ApplicationCreated # b/c (as of 1.0) + +@implementer(IBeforeRender) +class BeforeRender(dict): + """ + Subscribers to this event may introspect and modify the set of + :term:`renderer globals` before they are passed to a :term:`renderer`. + This event object itself has a dictionary-like interface that can be used + for this purpose. For example:: + + from pyramid.events import subscriber + from pyramid.events import BeforeRender + + @subscriber(BeforeRender) + def add_global(event): + event['mykey'] = 'foo' + + An object of this type is sent as an event just before a :term:`renderer` + is invoked. + + If a subscriber adds a key via ``__setitem__`` that already exists in + the renderer globals dictionary, it will overwrite the older value there. + This can be problematic because event subscribers to the BeforeRender + event do not possess any relative ordering. For maximum interoperability + with other third-party subscribers, if you write an event subscriber meant + to be used as a BeforeRender subscriber, your subscriber code will need to + ensure no value already exists in the renderer globals dictionary before + setting an overriding value (which can be done using ``.get`` or + ``__contains__`` of the event object). + + The dictionary returned from the view is accessible through the + :attr:`rendering_val` attribute of a :class:`~pyramid.events.BeforeRender` + event. + + Suppose you return ``{'mykey': 'somevalue', 'mykey2': 'somevalue2'}`` from + your view callable, like so:: + + from pyramid.view import view_config + + @view_config(renderer='some_renderer') + def myview(request): + return {'mykey': 'somevalue', 'mykey2': 'somevalue2'} + + :attr:`rendering_val` can be used to access these values from the + :class:`~pyramid.events.BeforeRender` object:: + + from pyramid.events import subscriber + from pyramid.events import BeforeRender + + @subscriber(BeforeRender) + def read_return(event): + # {'mykey': 'somevalue'} is returned from the view + print(event.rendering_val['mykey']) + + In other words, :attr:`rendering_val` is the (non-system) value returned + by a view or passed to ``render*`` as ``value``. This feature is new in + Pyramid 1.2. + + For a description of the values present in the renderer globals dictionary, + see :ref:`renderer_system_values`. + + .. seealso:: + + See also :class:`pyramid.interfaces.IBeforeRender`. + """ + def __init__(self, system, rendering_val=None): + dict.__init__(self, system) + self.rendering_val = rendering_val + diff --git a/src/pyramid/exceptions.py b/src/pyramid/exceptions.py new file mode 100644 index 000000000..c95922eb0 --- /dev/null +++ b/src/pyramid/exceptions.py @@ -0,0 +1,127 @@ +from pyramid.httpexceptions import ( + HTTPBadRequest, + HTTPNotFound, + HTTPForbidden, + ) + +NotFound = HTTPNotFound # bw compat +Forbidden = HTTPForbidden # bw compat + +CR = '\n' + + +class BadCSRFOrigin(HTTPBadRequest): + """ + This exception indicates the request has failed cross-site request forgery + origin validation. + """ + title = "Bad CSRF Origin" + explanation = ( + "Access is denied. This server can not verify that the origin or " + "referrer of your request matches the current site. Either your " + "browser supplied the wrong Origin or Referrer or it did not supply " + "one at all." + ) + + +class BadCSRFToken(HTTPBadRequest): + """ + This exception indicates the request has failed cross-site request + forgery token validation. + """ + title = 'Bad CSRF Token' + explanation = ( + 'Access is denied. This server can not verify that your cross-site ' + 'request forgery token belongs to your login session. Either you ' + 'supplied the wrong cross-site request forgery token or your session ' + 'no longer exists. This may be due to session timeout or because ' + 'browser is not supplying the credentials required, as can happen ' + 'when the browser has cookies turned off.') + +class PredicateMismatch(HTTPNotFound): + """ + This exception is raised by multiviews when no view matches + all given predicates. + + This exception subclasses the :class:`HTTPNotFound` exception for a + specific reason: if it reaches the main exception handler, it should + be treated as :class:`HTTPNotFound`` by any exception view + registrations. Thus, typically, this exception will not be seen + publicly. + + However, this exception will be raised if the predicates of all + views configured to handle another exception context cannot be + successfully matched. For instance, if a view is configured to + handle a context of ``HTTPForbidden`` and the configured with + additional predicates, then :class:`PredicateMismatch` will be + raised if: + + * An original view callable has raised :class:`HTTPForbidden` (thus + invoking an exception view); and + * The given request fails to match all predicates for said + exception view associated with :class:`HTTPForbidden`. + + The same applies to any type of exception being handled by an + exception view. + """ + +class URLDecodeError(UnicodeDecodeError): + """ + This exception is raised when :app:`Pyramid` cannot + successfully decode a URL or a URL path segment. This exception + behaves just like the Python builtin + :exc:`UnicodeDecodeError`. It is a subclass of the builtin + :exc:`UnicodeDecodeError` exception only for identity purposes, + mostly so an exception view can be registered when a URL cannot be + decoded. + """ + +class ConfigurationError(Exception): + """ Raised when inappropriate input values are supplied to an API + method of a :term:`Configurator`""" + +class ConfigurationConflictError(ConfigurationError): + """ Raised when a configuration conflict is detected during action + processing""" + + def __init__(self, conflicts): + self._conflicts = conflicts + + def __str__(self): + r = ["Conflicting configuration actions"] + items = sorted(self._conflicts.items()) + for discriminator, infos in items: + r.append(" For: %s" % (discriminator, )) + for info in infos: + for line in str(info).rstrip().split(CR): + r.append(" " + line) + + return CR.join(r) + + +class ConfigurationExecutionError(ConfigurationError): + """An error occurred during execution of a configuration action + """ + + def __init__(self, etype, evalue, info): + self.etype, self.evalue, self.info = etype, evalue, info + + def __str__(self): + return "%s: %s\n in:\n %s" % (self.etype, self.evalue, self.info) + + +class CyclicDependencyError(Exception): + """ The exception raised when the Pyramid topological sorter detects a + cyclic dependency.""" + def __init__(self, cycles): + self.cycles = cycles + + def __str__(self): + L = [] + cycles = self.cycles + for cycle in cycles: + dependent = cycle + dependees = cycles[cycle] + L.append('%r sorts before %r' % (dependent, dependees)) + msg = 'Implicit ordering cycle:' + '; '.join(L) + return msg diff --git a/src/pyramid/httpexceptions.py b/src/pyramid/httpexceptions.py new file mode 100644 index 000000000..bef8420b1 --- /dev/null +++ b/src/pyramid/httpexceptions.py @@ -0,0 +1,1182 @@ +""" +HTTP Exceptions +--------------- + +This module contains Pyramid HTTP exception classes. Each class relates to a +single HTTP status code. Each class is a subclass of the +:class:`~HTTPException`. Each exception class is also a :term:`response` +object. + +Each exception class has a status code according to :rfc:`2068` or :rfc:`7538`: +codes with 100-300 are not really errors; 400s are client errors, +and 500s are server errors. + +Exception + HTTPException + HTTPSuccessful + * 200 - HTTPOk + * 201 - HTTPCreated + * 202 - HTTPAccepted + * 203 - HTTPNonAuthoritativeInformation + * 204 - HTTPNoContent + * 205 - HTTPResetContent + * 206 - HTTPPartialContent + HTTPRedirection + * 300 - HTTPMultipleChoices + * 301 - HTTPMovedPermanently + * 302 - HTTPFound + * 303 - HTTPSeeOther + * 304 - HTTPNotModified + * 305 - HTTPUseProxy + * 307 - HTTPTemporaryRedirect + * 308 - HTTPPermanentRedirect + HTTPError + HTTPClientError + * 400 - HTTPBadRequest + * 401 - HTTPUnauthorized + * 402 - HTTPPaymentRequired + * 403 - HTTPForbidden + * 404 - HTTPNotFound + * 405 - HTTPMethodNotAllowed + * 406 - HTTPNotAcceptable + * 407 - HTTPProxyAuthenticationRequired + * 408 - HTTPRequestTimeout + * 409 - HTTPConflict + * 410 - HTTPGone + * 411 - HTTPLengthRequired + * 412 - HTTPPreconditionFailed + * 413 - HTTPRequestEntityTooLarge + * 414 - HTTPRequestURITooLong + * 415 - HTTPUnsupportedMediaType + * 416 - HTTPRequestRangeNotSatisfiable + * 417 - HTTPExpectationFailed + * 422 - HTTPUnprocessableEntity + * 423 - HTTPLocked + * 424 - HTTPFailedDependency + * 428 - HTTPPreconditionRequired + * 429 - HTTPTooManyRequests + * 431 - HTTPRequestHeaderFieldsTooLarge + HTTPServerError + * 500 - HTTPInternalServerError + * 501 - HTTPNotImplemented + * 502 - HTTPBadGateway + * 503 - HTTPServiceUnavailable + * 504 - HTTPGatewayTimeout + * 505 - HTTPVersionNotSupported + * 507 - HTTPInsufficientStorage + +HTTP exceptions are also :term:`response` objects, thus they accept most of +the same parameters that can be passed to a regular +:class:`~pyramid.response.Response`. Each HTTP exception also has the +following attributes: + + ``code`` + the HTTP status code for the exception + + ``title`` + remainder of the status line (stuff after the code) + + ``explanation`` + a plain-text explanation of the error message that is + not subject to environment or header substitutions; + it is accessible in the template via ${explanation} + + ``detail`` + a plain-text message customization that is not subject + to environment or header substitutions; accessible in + the template via ${detail} + + ``body_template`` + a ``String.template``-format content fragment used for environment + and header substitution; the default template includes both + the explanation and further detail provided in the + message. + +Each HTTP exception accepts the following parameters, any others will +be forwarded to its :class:`~pyramid.response.Response` superclass: + + ``detail`` + a plain-text override of the default ``detail`` + + ``headers`` + a list of (k,v) header pairs, or a dict, to be added to the + response; use the content_type='application/json' kwarg and other + similar kwargs to to change properties of the response supported by the + :class:`pyramid.response.Response` superclass + + ``comment`` + a plain-text additional information which is + usually stripped/hidden for end-users + + ``body_template`` + a ``string.Template`` object containing a content fragment in HTML + that frames the explanation and further detail + + ``body`` + a string that will override the ``body_template`` and be used as the + body of the response. + +Substitution of response headers into template values is always performed. +Substitution of WSGI environment values is performed if a ``request`` is +passed to the exception's constructor. + +The subclasses of :class:`~_HTTPMove` +(:class:`~HTTPMultipleChoices`, :class:`~HTTPMovedPermanently`, +:class:`~HTTPFound`, :class:`~HTTPSeeOther`, :class:`~HTTPUseProxy`, +:class:`~HTTPTemporaryRedirect`, and :class: `~HTTPPermanentRedirect) are +redirections that require a ``Location`` field. Reflecting this, these +subclasses have one additional keyword argument: ``location``, +which indicates the location to which to redirect. +""" +import json + +from string import Template + +from zope.interface import implementer + +from webob import html_escape as _html_escape +from webob.acceptparse import create_accept_header + +from pyramid.compat import ( + class_types, + text_type, + binary_type, + text_, + ) + +from pyramid.interfaces import IExceptionResponse +from pyramid.response import Response + +def _no_escape(value): + if value is None: + return '' + if not isinstance(value, text_type): + if hasattr(value, '__unicode__'): + value = value.__unicode__() + if isinstance(value, binary_type): + value = text_(value, 'utf-8') + else: + value = text_type(value) + return value + +@implementer(IExceptionResponse) +class HTTPException(Response, Exception): + + ## You should set in subclasses: + # code = 200 + # title = 'OK' + # explanation = 'why this happens' + # body_template_obj = Template('response template') + # + # This class itself uses the error code "520" with the error message/title + # of "Unknown Error". This is not an RFC standard, however it is + # implemented in practice. Sub-classes should be overriding the default + # values and 520 should not be seen in the wild from Pyramid applications. + # Due to changes in WebOb, a code of "None" is not valid, and WebOb due to + # more strict error checking rejects it now. + + # differences from webob.exc.WSGIHTTPException: + # + # - doesn't use "strip_tags" (${br} placeholder for
, no other html + # in default body template) + # + # - __call__ never generates a new Response, it always mutates self + # + # - explicitly sets self.message = detail to prevent whining by Python + # 2.6.5+ access of Exception.message + # + # - its base class of HTTPException is no longer a Python 2.4 compatibility + # shim; it's purely a base class that inherits from Exception. This + # implies that this class' ``exception`` property always returns + # ``self`` (it exists only for bw compat at this point). + # + # - documentation improvements (Pyramid-specific docstrings where necessary) + # + code = 520 + title = 'Unknown Error' + explanation = '' + body_template_obj = Template('''\ +${explanation}${br}${br} +${detail} +${html_comment} +''') + + plain_template_obj = Template('''\ +${status} + +${body}''') + + html_template_obj = Template('''\ + + + ${status} + + +

${status}

+ ${body} + +''') + + ## Set this to True for responses that should have no request body + empty_body = False + + def __init__(self, detail=None, headers=None, comment=None, + body_template=None, json_formatter=None, **kw): + status = '%s %s' % (self.code, self.title) + Response.__init__(self, status=status, **kw) + Exception.__init__(self, detail) + self.detail = self.message = detail + if headers: + self.headers.extend(headers) + self.comment = comment + if body_template is not None: + self.body_template = body_template + self.body_template_obj = Template(body_template) + if json_formatter is not None: + self._json_formatter = json_formatter + + if self.empty_body: + del self.content_type + del self.content_length + + def __str__(self): + return str(self.detail) if self.detail else self.explanation + + def _json_formatter(self, status, body, title, environ): + return {'message': body, + 'code': status, + 'title': self.title} + + def prepare(self, environ): + if not self.has_body and not self.empty_body: + html_comment = '' + comment = self.comment or '' + accept_value = environ.get('HTTP_ACCEPT', '') + accept = create_accept_header(accept_value) + # Attempt to match text/html or application/json, if those don't + # match, we will fall through to defaulting to text/plain + acceptable = accept.acceptable_offers(['text/html', 'application/json']) + acceptable = [offer[0] for offer in acceptable] + ['text/plain'] + match = acceptable[0] + + if match == 'text/html': + self.content_type = 'text/html' + escape = _html_escape + page_template = self.html_template_obj + br = '
' + if comment: + html_comment = '' % escape(comment) + elif match == 'application/json': + self.content_type = 'application/json' + self.charset = None + escape = _no_escape + br = '\n' + if comment: + html_comment = escape(comment) + + class JsonPageTemplate(object): + def __init__(self, excobj): + self.excobj = excobj + + def substitute(self, status, body): + jsonbody = self.excobj._json_formatter( + status=status, + body=body, title=self.excobj.title, + environ=environ) + return json.dumps(jsonbody) + + page_template = JsonPageTemplate(self) + else: + self.content_type = 'text/plain' + escape = _no_escape + page_template = self.plain_template_obj + br = '\n' + if comment: + html_comment = escape(comment) + args = { + 'br': br, + 'explanation': escape(self.explanation), + 'detail': escape(self.detail or ''), + 'comment': escape(comment), + 'html_comment': html_comment, + } + body_tmpl = self.body_template_obj + if HTTPException.body_template_obj is not body_tmpl: + # Custom template; add headers to args + for k, v in environ.items(): + if (not k.startswith('wsgi.')) and ('.' in k): + # omit custom environ variables, stringifying them may + # trigger code that should not be executed here; see + # https://github.com/Pylons/pyramid/issues/239 + continue + args[k] = escape(v) + for k, v in self.headers.items(): + args[k.lower()] = escape(v) + body = body_tmpl.substitute(args) + page = page_template.substitute(status=self.status, body=body) + if isinstance(page, text_type): + page = page.encode(self.charset if self.charset else 'UTF-8') + self.app_iter = [page] + self.body = page + + @property + def wsgi_response(self): + # bw compat only + return self + + exception = wsgi_response # bw compat only + + def __call__(self, environ, start_response): + # differences from webob.exc.WSGIHTTPException + # + # - does not try to deal with HEAD requests + # + # - does not manufacture a new response object when generating + # the default response + # + self.prepare(environ) + return Response.__call__(self, environ, start_response) + +WSGIHTTPException = HTTPException # b/c post 1.5 + +class HTTPError(HTTPException): + """ + base class for exceptions with status codes in the 400s and 500s + + This is an exception which indicates that an error has occurred, + and that any work in progress should not be committed. + """ + +class HTTPRedirection(HTTPException): + """ + base class for exceptions with status codes in the 300s (redirections) + + This is an abstract base class for 3xx redirection. It indicates + that further action needs to be taken by the user agent in order + to fulfill the request. It does not necessarly signal an error + condition. + """ + +class HTTPSuccessful(HTTPException): + """ + Base class for exceptions with status codes in the 200s (successful + responses) + """ + +############################################################ +## 2xx success +############################################################ + +class HTTPOk(HTTPSuccessful): + """ + subclass of :class:`~HTTPSuccessful` + + Indicates that the request has succeeded. + + code: 200, title: OK + """ + code = 200 + title = 'OK' + +class HTTPCreated(HTTPSuccessful): + """ + subclass of :class:`~HTTPSuccessful` + + This indicates that request has been fulfilled and resulted in a new + resource being created. + + code: 201, title: Created + """ + code = 201 + title = 'Created' + +class HTTPAccepted(HTTPSuccessful): + """ + subclass of :class:`~HTTPSuccessful` + + This indicates that the request has been accepted for processing, but the + processing has not been completed. + + code: 202, title: Accepted + """ + code = 202 + title = 'Accepted' + explanation = 'The request is accepted for processing.' + +class HTTPNonAuthoritativeInformation(HTTPSuccessful): + """ + subclass of :class:`~HTTPSuccessful` + + This indicates that the returned metainformation in the entity-header is + not the definitive set as available from the origin server, but is + gathered from a local or a third-party copy. + + code: 203, title: Non-Authoritative Information + """ + code = 203 + title = 'Non-Authoritative Information' + +class HTTPNoContent(HTTPSuccessful): + """ + subclass of :class:`~HTTPSuccessful` + + This indicates that the server has fulfilled the request but does + not need to return an entity-body, and might want to return updated + metainformation. + + code: 204, title: No Content + """ + code = 204 + title = 'No Content' + empty_body = True + +class HTTPResetContent(HTTPSuccessful): + """ + subclass of :class:`~HTTPSuccessful` + + This indicates that the server has fulfilled the request and + the user agent SHOULD reset the document view which caused the + request to be sent. + + code: 205, title: Reset Content + """ + code = 205 + title = 'Reset Content' + empty_body = True + +class HTTPPartialContent(HTTPSuccessful): + """ + subclass of :class:`~HTTPSuccessful` + + This indicates that the server has fulfilled the partial GET + request for the resource. + + code: 206, title: Partial Content + """ + code = 206 + title = 'Partial Content' + +## FIXME: add 207 Multi-Status (but it's complicated) + +############################################################ +## 3xx redirection +############################################################ + +class _HTTPMove(HTTPRedirection): + """ + redirections which require a Location field + + Since a 'Location' header is a required attribute of 301, 302, 303, + 305 and 307 (but not 304), this base class provides the mechanics to + make this easy. + + You must provide a ``location`` keyword argument. + """ + # differences from webob.exc._HTTPMove: + # + # - ${location} isn't wrapped in an tag in body + # + # - location keyword arg defaults to '' + # + # - location isn't prepended with req.path_url when adding it as + # a header + # + # - ``location`` is first keyword (and positional) argument + # + # - ``add_slash`` argument is no longer accepted: code that passes + # add_slash argument to the constructor will receive an exception. + explanation = 'The resource has been moved to' + body_template_obj = Template('''\ +${explanation} ${location}; you should be redirected automatically. +${detail} +${html_comment}''') + + def __init__(self, location='', detail=None, headers=None, comment=None, + body_template=None, **kw): + if location is None: + raise ValueError("HTTP redirects need a location to redirect to.") + super(_HTTPMove, self).__init__( + detail=detail, headers=headers, comment=comment, + body_template=body_template, location=location, **kw) + +class HTTPMultipleChoices(_HTTPMove): + """ + subclass of :class:`~_HTTPMove` + + This indicates that the requested resource corresponds to any one + of a set of representations, each with its own specific location, + and agent-driven negotiation information is being provided so that + the user can select a preferred representation and redirect its + request to that location. + + code: 300, title: Multiple Choices + """ + code = 300 + title = 'Multiple Choices' + +class HTTPMovedPermanently(_HTTPMove): + """ + subclass of :class:`~_HTTPMove` + + This indicates that the requested resource has been assigned a new + permanent URI and any future references to this resource SHOULD use + one of the returned URIs. + + code: 301, title: Moved Permanently + """ + code = 301 + title = 'Moved Permanently' + +class HTTPFound(_HTTPMove): + """ + subclass of :class:`~_HTTPMove` + + This indicates that the requested resource resides temporarily under + a different URI. + + code: 302, title: Found + """ + code = 302 + title = 'Found' + explanation = 'The resource was found at' + +# This one is safe after a POST (the redirected location will be +# retrieved with GET): +class HTTPSeeOther(_HTTPMove): + """ + subclass of :class:`~_HTTPMove` + + This indicates that the response to the request can be found under + a different URI and SHOULD be retrieved using a GET method on that + resource. + + code: 303, title: See Other + """ + code = 303 + title = 'See Other' + +class HTTPNotModified(HTTPRedirection): + """ + subclass of :class:`~HTTPRedirection` + + This indicates that if the client has performed a conditional GET + request and access is allowed, but the document has not been + modified, the server SHOULD respond with this status code. + + code: 304, title: Not Modified + """ + # FIXME: this should include a date or etag header + code = 304 + title = 'Not Modified' + empty_body = True + +class HTTPUseProxy(_HTTPMove): + """ + subclass of :class:`~_HTTPMove` + + This indicates that the requested resource MUST be accessed through + the proxy given by the Location field. + + code: 305, title: Use Proxy + """ + # Not a move, but looks a little like one + code = 305 + title = 'Use Proxy' + explanation = ( + 'The resource must be accessed through a proxy located at') + +class HTTPTemporaryRedirect(_HTTPMove): + """ + subclass of :class:`~_HTTPMove` + + This indicates that the requested resource resides temporarily + under a different URI. + + code: 307, title: Temporary Redirect + """ + code = 307 + title = 'Temporary Redirect' + +class HTTPPermanentRedirect(_HTTPMove): + """ + subclass of :class:`~_HTTPMove` + + This indicates that the requested resource resides permanently + under a different URI and that the request method must not be + changed. + + code: 308, title: Permanent Redirect + """ + code = 308 + title = 'Permanent Redirect' + +############################################################ +## 4xx client error +############################################################ + +class HTTPClientError(HTTPError): + """ + base class for the 400s, where the client is in error + + This is an error condition in which the client is presumed to be + in-error. This is an expected problem, and thus is not considered + a bug. A server-side traceback is not warranted. Unless specialized, + this is a '400 Bad Request' + """ + code = 400 + title = 'Bad Request' + +class HTTPBadRequest(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the body or headers failed validity checks, + preventing the server from being able to continue processing. + + code: 400, title: Bad Request + """ + explanation = ('The server could not comply with the request since ' + 'it is either malformed or otherwise incorrect.') + +class HTTPUnauthorized(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the request requires user authentication. + + code: 401, title: Unauthorized + """ + code = 401 + title = 'Unauthorized' + explanation = ( + 'This server could not verify that you are authorized to ' + 'access the document you requested. Either you supplied the ' + 'wrong credentials (e.g., bad password), or your browser ' + 'does not understand how to supply the credentials required.') + +class HTTPPaymentRequired(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + code: 402, title: Payment Required + """ + code = 402 + title = 'Payment Required' + explanation = ('Access was denied for financial reasons.') + +class HTTPForbidden(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the server understood the request, but is + refusing to fulfill it. + + code: 403, title: Forbidden + + Raise this exception within :term:`view` code to immediately return the + :term:`forbidden view` to the invoking user. Usually this is a basic + ``403`` page, but the forbidden view can be customized as necessary. See + :ref:`changing_the_forbidden_view`. A ``Forbidden`` exception will be + the ``context`` of a :term:`Forbidden View`. + + This exception's constructor treats two arguments specially. The first + argument, ``detail``, should be a string. The value of this string will + be used as the ``message`` attribute of the exception object. The second + special keyword argument, ``result`` is usually an instance of + :class:`pyramid.security.Denied` or :class:`pyramid.security.ACLDenied` + each of which indicates a reason for the forbidden error. However, + ``result`` is also permitted to be just a plain boolean ``False`` object + or ``None``. The ``result`` value will be used as the ``result`` + attribute of the exception object. It defaults to ``None``. + + The :term:`Forbidden View` can use the attributes of a Forbidden + exception as necessary to provide extended information in an error + report shown to a user. + """ + # differences from webob.exc.HTTPForbidden: + # + # - accepts a ``result`` keyword argument + # + # - overrides constructor to set ``self.result`` + # + # differences from older ``pyramid.exceptions.Forbidden``: + # + # - ``result`` must be passed as a keyword argument. + # + code = 403 + title = 'Forbidden' + explanation = ('Access was denied to this resource.') + def __init__(self, detail=None, headers=None, comment=None, + body_template=None, result=None, **kw): + HTTPClientError.__init__(self, detail=detail, headers=headers, + comment=comment, body_template=body_template, + **kw) + self.result = result + +class HTTPNotFound(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the server did not find anything matching the + Request-URI. + + code: 404, title: Not Found + + Raise this exception within :term:`view` code to immediately + return the :term:`Not Found View` to the invoking user. Usually + this is a basic ``404`` page, but the Not Found View can be + customized as necessary. See :ref:`changing_the_notfound_view`. + + This exception's constructor accepts a ``detail`` argument + (the first argument), which should be a string. The value of this + string will be available as the ``message`` attribute of this exception, + for availability to the :term:`Not Found View`. + """ + code = 404 + title = 'Not Found' + explanation = ('The resource could not be found.') + +class HTTPMethodNotAllowed(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the method specified in the Request-Line is + not allowed for the resource identified by the Request-URI. + + code: 405, title: Method Not Allowed + """ + # differences from webob.exc.HTTPMethodNotAllowed: + # + # - body_template_obj uses ${br} instead of
+ code = 405 + title = 'Method Not Allowed' + body_template_obj = Template('''\ +The method ${REQUEST_METHOD} is not allowed for this resource. ${br}${br} +${detail}''') + +class HTTPNotAcceptable(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates the resource identified by the request is only + capable of generating response entities which have content + characteristics not acceptable according to the accept headers + sent in the request. + + code: 406, title: Not Acceptable + """ + # differences from webob.exc.HTTPNotAcceptable: + # + # - "template" attribute left off (useless, bug in webob?) + code = 406 + title = 'Not Acceptable' + +class HTTPProxyAuthenticationRequired(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This is similar to 401, but indicates that the client must first + authenticate itself with the proxy. + + code: 407, title: Proxy Authentication Required + """ + code = 407 + title = 'Proxy Authentication Required' + explanation = ('Authentication with a local proxy is needed.') + +class HTTPRequestTimeout(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the client did not produce a request within + the time that the server was prepared to wait. + + code: 408, title: Request Timeout + """ + code = 408 + title = 'Request Timeout' + explanation = ('The server has waited too long for the request to ' + 'be sent by the client.') + +class HTTPConflict(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the request could not be completed due to a + conflict with the current state of the resource. + + code: 409, title: Conflict + """ + code = 409 + title = 'Conflict' + explanation = ('There was a conflict when trying to complete ' + 'your request.') + +class HTTPGone(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the requested resource is no longer available + at the server and no forwarding address is known. + + code: 410, title: Gone + """ + code = 410 + title = 'Gone' + explanation = ('This resource is no longer available. No forwarding ' + 'address is given.') + +class HTTPLengthRequired(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the server refuses to accept the request + without a defined Content-Length. + + code: 411, title: Length Required + """ + code = 411 + title = 'Length Required' + explanation = ('Content-Length header required.') + +class HTTPPreconditionFailed(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the precondition given in one or more of the + request-header fields evaluated to false when it was tested on the + server. + + code: 412, title: Precondition Failed + """ + code = 412 + title = 'Precondition Failed' + explanation = ('Request precondition failed.') + +class HTTPRequestEntityTooLarge(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the server is refusing to process a request + because the request entity is larger than the server is willing or + able to process. + + code: 413, title: Request Entity Too Large + """ + code = 413 + title = 'Request Entity Too Large' + explanation = ('The body of your request was too large for this server.') + +class HTTPRequestURITooLong(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the server is refusing to service the request + because the Request-URI is longer than the server is willing to + interpret. + + code: 414, title: Request-URI Too Long + """ + code = 414 + title = 'Request-URI Too Long' + explanation = ('The request URI was too long for this server.') + +class HTTPUnsupportedMediaType(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the server is refusing to service the request + because the entity of the request is in a format not supported by + the requested resource for the requested method. + + code: 415, title: Unsupported Media Type + """ + # differences from webob.exc.HTTPUnsupportedMediaType: + # + # - "template_obj" attribute left off (useless, bug in webob?) + code = 415 + title = 'Unsupported Media Type' + +class HTTPRequestRangeNotSatisfiable(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + The server SHOULD return a response with this status code if a + request included a Range request-header field, and none of the + range-specifier values in this field overlap the current extent + of the selected resource, and the request did not include an + If-Range request-header field. + + code: 416, title: Request Range Not Satisfiable + """ + code = 416 + title = 'Request Range Not Satisfiable' + explanation = ('The Range requested is not available.') + +class HTTPExpectationFailed(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indidcates that the expectation given in an Expect + request-header field could not be met by this server. + + code: 417, title: Expectation Failed + """ + code = 417 + title = 'Expectation Failed' + explanation = ('Expectation failed.') + +class HTTPUnprocessableEntity(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the server is unable to process the contained + instructions. + + May be used to notify the client that their JSON/XML is well formed, but + not correct for the current request. + + See RFC4918 section 11 for more information. + + code: 422, title: Unprocessable Entity + """ + ## Note: from WebDAV + code = 422 + title = 'Unprocessable Entity' + explanation = 'Unable to process the contained instructions' + +class HTTPLocked(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the resource is locked. + + code: 423, title: Locked + """ + ## Note: from WebDAV + code = 423 + title = 'Locked' + explanation = ('The resource is locked') + +class HTTPFailedDependency(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the method could not be performed because the + requested action depended on another action and that action failed. + + code: 424, title: Failed Dependency + """ + ## Note: from WebDAV + code = 424 + title = 'Failed Dependency' + explanation = ( + 'The method could not be performed because the requested ' + 'action dependended on another action and that action failed') + +class HTTPPreconditionRequired(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the origin server requires the + request to be conditional. + + Its typical use is to avoid the "lost update" problem, where a client + GETs a resource's state, modifies it, and PUTs it back to the server, + when meanwhile a third party has modified the state on the server, + leading to a conflict. By requiring requests to be conditional, the + server can assure that clients are working with the correct copies. + + RFC 6585.3 + + code: 428, title: Precondition Required + """ + code = 428 + title = 'Precondition Required' + explanation = ( + 'The origin server requires the request to be conditional.') + +class HTTPTooManyRequests(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the user has sent too many + requests in a given amount of time ("rate limiting"). + + RFC 6585.4 + + code: 429, title: Too Many Requests + """ + code = 429 + title = 'Too Many Requests' + explanation = ( + 'The action could not be performed because there were too ' + 'many requests by the client.') + +class HTTPRequestHeaderFieldsTooLarge(HTTPClientError): + """ + subclass of :class:`~HTTPClientError` + + This indicates that the server is unwilling to process + the request because its header fields are too large. The request MAY + be resubmitted after reducing the size of the request header fields. + + RFC 6585.5 + + code: 431, title: Request Header Fields Too Large + """ + code = 431 + title = 'Request Header Fields Too Large' + explanation = ( + 'The requests header fields were too large.') + +############################################################ +## 5xx Server Error +############################################################ +# Response status codes beginning with the digit "5" indicate cases in +# which the server is aware that it has erred or is incapable of +# performing the request. Except when responding to a HEAD request, the +# server SHOULD include an entity containing an explanation of the error +# situation, and whether it is a temporary or permanent condition. User +# agents SHOULD display any included entity to the user. These response +# codes are applicable to any request method. + +class HTTPServerError(HTTPError): + """ + base class for the 500s, where the server is in-error + + This is an error condition in which the server is presumed to be + in-error. Unless specialized, this is a '500 Internal Server Error'. + """ + code = 500 + title = 'Internal Server Error' + +class HTTPInternalServerError(HTTPServerError): + """ + subclass of :class:`~HTTPServerError` + + This indicates that the server encountered an unexpected condition + which prevented it from fulfilling the request. + + code: 500, title: Internal Server Error + """ + explanation = ( + 'The server has either erred or is incapable of performing ' + 'the requested operation.') + +class HTTPNotImplemented(HTTPServerError): + """ + subclass of :class:`~HTTPServerError` + + This indicates that the server does not support the functionality + required to fulfill the request. + + code: 501, title: Not Implemented + """ + # differences from webob.exc.HTTPNotAcceptable: + # + # - "template" attr left off (useless, bug in webob?) + code = 501 + title = 'Not Implemented' + +class HTTPBadGateway(HTTPServerError): + """ + subclass of :class:`~HTTPServerError` + + This indicates that the server, while acting as a gateway or proxy, + received an invalid response from the upstream server it accessed + in attempting to fulfill the request. + + code: 502, title: Bad Gateway + """ + code = 502 + title = 'Bad Gateway' + explanation = ('Bad gateway.') + +class HTTPServiceUnavailable(HTTPServerError): + """ + subclass of :class:`~HTTPServerError` + + This indicates that the server is currently unable to handle the + request due to a temporary overloading or maintenance of the server. + + code: 503, title: Service Unavailable + """ + code = 503 + title = 'Service Unavailable' + explanation = ('The server is currently unavailable. ' + 'Please try again at a later time.') + +class HTTPGatewayTimeout(HTTPServerError): + """ + subclass of :class:`~HTTPServerError` + + This indicates that the server, while acting as a gateway or proxy, + did not receive a timely response from the upstream server specified + by the URI (e.g. HTTP, FTP, LDAP) or some other auxiliary server + (e.g. DNS) it needed to access in attempting to complete the request. + + code: 504, title: Gateway Timeout + """ + code = 504 + title = 'Gateway Timeout' + explanation = ('The gateway has timed out.') + +class HTTPVersionNotSupported(HTTPServerError): + """ + subclass of :class:`~HTTPServerError` + + This indicates that the server does not support, or refuses to + support, the HTTP protocol version that was used in the request + message. + + code: 505, title: HTTP Version Not Supported + """ + code = 505 + title = 'HTTP Version Not Supported' + explanation = ('The HTTP version is not supported.') + +class HTTPInsufficientStorage(HTTPServerError): + """ + subclass of :class:`~HTTPServerError` + + This indicates that the server does not have enough space to save + the resource. + + code: 507, title: Insufficient Storage + """ + code = 507 + title = 'Insufficient Storage' + explanation = ('There was not enough space to save the resource') + +def exception_response(status_code, **kw): + """Creates an HTTP exception based on a status code. Example:: + + raise exception_response(404) # raises an HTTPNotFound exception. + + The values passed as ``kw`` are provided to the exception's constructor. + """ + exc = status_map[status_code](**kw) + return exc + +def default_exceptionresponse_view(context, request): + if not isinstance(context, Exception): + # backwards compat for an exception response view registered via + # config.set_notfound_view or config.set_forbidden_view + # instead of as a proper exception view + context = request.exception or context + return context # assumed to be an IResponse + +status_map = {} +code = None +for name, value in list(globals().items()): + if ( + isinstance(value, class_types) and + issubclass(value, HTTPException) and + value not in {HTTPClientError, HTTPServerError} and + not name.startswith('_') + ): + code = getattr(value, 'code', None) + if code: + status_map[code] = value +del name, value, code diff --git a/src/pyramid/i18n.py b/src/pyramid/i18n.py new file mode 100644 index 000000000..1d11adfe3 --- /dev/null +++ b/src/pyramid/i18n.py @@ -0,0 +1,397 @@ +import gettext +import os + +from translationstring import ( + Translator, + Pluralizer, + TranslationString, # API + TranslationStringFactory, # API + ) + +from pyramid.compat import PY2 +from pyramid.decorator import reify + +from pyramid.interfaces import ( + ILocalizer, + ITranslationDirectories, + ILocaleNegotiator, + ) + +from pyramid.threadlocal import get_current_registry + +TranslationString = TranslationString # PyFlakes +TranslationStringFactory = TranslationStringFactory # PyFlakes + +DEFAULT_PLURAL = lambda n: int(n != 1) + +class Localizer(object): + """ + An object providing translation and pluralizations related to + the current request's locale name. A + :class:`pyramid.i18n.Localizer` object is created using the + :func:`pyramid.i18n.get_localizer` function. + """ + def __init__(self, locale_name, translations): + self.locale_name = locale_name + self.translations = translations + self.pluralizer = None + self.translator = None + + def translate(self, tstring, domain=None, mapping=None): + """ + Translate a :term:`translation string` to the current language + and interpolate any *replacement markers* in the result. The + ``translate`` method accepts three arguments: ``tstring`` + (required), ``domain`` (optional) and ``mapping`` (optional). + When called, it will translate the ``tstring`` translation + string to a ``unicode`` object using the current locale. If + the current locale could not be determined, the result of + interpolation of the default value is returned. The optional + ``domain`` argument can be used to specify or override the + domain of the ``tstring`` (useful when ``tstring`` is a normal + string rather than a translation string). The optional + ``mapping`` argument can specify or override the ``tstring`` + interpolation mapping, useful when the ``tstring`` argument is + a simple string instead of a translation string. + + Example:: + + from pyramid.18n import TranslationString + ts = TranslationString('Add ${item}', domain='mypackage', + mapping={'item':'Item'}) + translated = localizer.translate(ts) + + Example:: + + translated = localizer.translate('Add ${item}', domain='mypackage', + mapping={'item':'Item'}) + + """ + if self.translator is None: + self.translator = Translator(self.translations) + return self.translator(tstring, domain=domain, mapping=mapping) + + def pluralize(self, singular, plural, n, domain=None, mapping=None): + """ + Return a Unicode string translation by using two + :term:`message identifier` objects as a singular/plural pair + and an ``n`` value representing the number that appears in the + message using gettext plural forms support. The ``singular`` + and ``plural`` objects should be unicode strings. There is no + reason to use translation string objects as arguments as all + metadata is ignored. + + ``n`` represents the number of elements. ``domain`` is the + translation domain to use to do the pluralization, and ``mapping`` + is the interpolation mapping that should be used on the result. If + the ``domain`` is not supplied, a default domain is used (usually + ``messages``). + + Example:: + + num = 1 + translated = localizer.pluralize('Add ${num} item', + 'Add ${num} items', + num, + mapping={'num':num}) + + If using the gettext plural support, which is required for + languages that have pluralisation rules other than n != 1, the + ``singular`` argument must be the message_id defined in the + translation file. The plural argument is not used in this case. + + Example:: + + num = 1 + translated = localizer.pluralize('item_plural', + '', + num, + mapping={'num':num}) + + + """ + if self.pluralizer is None: + self.pluralizer = Pluralizer(self.translations) + return self.pluralizer(singular, plural, n, domain=domain, + mapping=mapping) + + +def default_locale_negotiator(request): + """ The default :term:`locale negotiator`. Returns a locale name + or ``None``. + + - First, the negotiator looks for the ``_LOCALE_`` attribute of + the request object (possibly set by a view or a listener for an + :term:`event`). If the attribute exists and it is not ``None``, + its value will be used. + + - Then it looks for the ``request.params['_LOCALE_']`` value. + + - Then it looks for the ``request.cookies['_LOCALE_']`` value. + + - Finally, the negotiator returns ``None`` if the locale could not + be determined via any of the previous checks (when a locale + negotiator returns ``None``, it signifies that the + :term:`default locale name` should be used.) + """ + name = '_LOCALE_' + locale_name = getattr(request, name, None) + if locale_name is None: + locale_name = request.params.get(name) + if locale_name is None: + locale_name = request.cookies.get(name) + return locale_name + +def negotiate_locale_name(request): + """ Negotiate and return the :term:`locale name` associated with + the current request.""" + try: + registry = request.registry + except AttributeError: + registry = get_current_registry() + negotiator = registry.queryUtility(ILocaleNegotiator, + default=default_locale_negotiator) + locale_name = negotiator(request) + + if locale_name is None: + settings = registry.settings or {} + locale_name = settings.get('default_locale_name', 'en') + + return locale_name + +def get_locale_name(request): + """ + .. deprecated:: 1.5 + Use :attr:`pyramid.request.Request.locale_name` directly instead. + Return the :term:`locale name` associated with the current request. + """ + return request.locale_name + +def make_localizer(current_locale_name, translation_directories): + """ Create a :class:`pyramid.i18n.Localizer` object + corresponding to the provided locale name from the + translations found in the list of translation directories.""" + translations = Translations() + translations._catalog = {} + + locales_to_try = [] + if '_' in current_locale_name: + locales_to_try = [current_locale_name.split('_')[0]] + locales_to_try.append(current_locale_name) + + # intent: order locales left to right in least specific to most specific, + # e.g. ['de', 'de_DE']. This services the intent of creating a + # translations object that returns a "more specific" translation for a + # region, but will fall back to a "less specific" translation for the + # locale if necessary. Ordering from least specific to most specific + # allows us to call translations.add in the below loop to get this + # behavior. + + for tdir in translation_directories: + locale_dirs = [] + for lname in locales_to_try: + ldir = os.path.realpath(os.path.join(tdir, lname)) + if os.path.isdir(ldir): + locale_dirs.append(ldir) + + for locale_dir in locale_dirs: + messages_dir = os.path.join(locale_dir, 'LC_MESSAGES') + if not os.path.isdir(os.path.realpath(messages_dir)): + continue + for mofile in os.listdir(messages_dir): + mopath = os.path.realpath(os.path.join(messages_dir, + mofile)) + if mofile.endswith('.mo') and os.path.isfile(mopath): + with open(mopath, 'rb') as mofp: + domain = mofile[:-3] + dtrans = Translations(mofp, domain) + translations.add(dtrans) + + return Localizer(locale_name=current_locale_name, + translations=translations) + +def get_localizer(request): + """ + .. deprecated:: 1.5 + Use the :attr:`pyramid.request.Request.localizer` attribute directly + instead. Retrieve a :class:`pyramid.i18n.Localizer` object + corresponding to the current request's locale name. + """ + return request.localizer + +class Translations(gettext.GNUTranslations, object): + """An extended translation catalog class (ripped off from Babel) """ + + DEFAULT_DOMAIN = 'messages' + + def __init__(self, fileobj=None, domain=DEFAULT_DOMAIN): + """Initialize the translations catalog. + + :param fileobj: the file-like object the translation should be read + from + """ + # germanic plural by default; self.plural will be overwritten by + # 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 + # 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 + self._domains = {} + + @classmethod + def load(cls, dirname=None, locales=None, domain=DEFAULT_DOMAIN): + """Load translations from the given directory. + + :param dirname: the directory containing the ``MO`` files + :param locales: the list of locales in order of preference (items in + this list can be either `Locale` objects or locale + strings) + :param domain: the message domain + :return: the loaded catalog, or a ``NullTranslations`` instance if no + matching translations were found + :rtype: `Translations` + """ + if locales is not None: + if not isinstance(locales, (list, tuple)): + locales = [locales] + locales = [str(l) for l in locales] + if not domain: + domain = cls.DEFAULT_DOMAIN + filename = gettext.find(domain, dirname, locales) + if not filename: + return gettext.NullTranslations() + with open(filename, 'rb') as fp: + return cls(fileobj=fp, domain=domain) + + def __repr__(self): + return '<%s: "%s">' % (type(self).__name__, + self._info.get('project-id-version')) + + def add(self, translations, merge=True): + """Add the given translations to the catalog. + + If the domain of the translations is different than that of the + current catalog, they are added as a catalog that is only accessible + by the various ``d*gettext`` functions. + + :param translations: the `Translations` instance with the messages to + add + :param merge: whether translations for message domains that have + already been added should be merged with the existing + translations + :return: the `Translations` instance (``self``) so that `merge` calls + can be easily chained + :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) + + existing = self._domains.get(domain) + if merge and existing is not None: + existing.merge(translations) + else: + translations.add_fallback(self) + self._domains[domain] = translations + + return self + + def merge(self, translations): + """Merge the given translations into the catalog. + + Message translations in the specified catalog override any messages + with the same identifier in the existing catalog. + + :param translations: the `Translations` instance with the messages to + merge + :return: the `Translations` instance (``self``) so that `merge` calls + can be easily chained + :rtype: `Translations` + """ + if isinstance(translations, gettext.GNUTranslations): + self._catalog.update(translations._catalog) + if isinstance(translations, Translations): + self.files.extend(translations.files) + + return self + + def dgettext(self, domain, message): + """Like ``gettext()``, but look the message up in the specified + domain. + """ + return self._domains.get(domain, self).gettext(message) + + def ldgettext(self, domain, message): + """Like ``lgettext()``, but look the message up in the specified + domain. + """ + return self._domains.get(domain, self).lgettext(message) + + def dugettext(self, domain, message): + """Like ``ugettext()``, but look the message up in the specified + domain. + """ + if PY2: + return self._domains.get(domain, self).ugettext(message) + else: + return self._domains.get(domain, self).gettext(message) + + def dngettext(self, domain, singular, plural, num): + """Like ``ngettext()``, but look the message up in the specified + domain. + """ + return self._domains.get(domain, self).ngettext(singular, plural, num) + + def ldngettext(self, domain, singular, plural, num): + """Like ``lngettext()``, but look the message up in the specified + domain. + """ + return self._domains.get(domain, self).lngettext(singular, plural, num) + + def dungettext(self, domain, singular, plural, num): + """Like ``ungettext()`` but look the message up in the specified + domain. + """ + if PY2: + return self._domains.get(domain, self).ungettext( + singular, plural, num) + else: + return self._domains.get(domain, self).ngettext( + singular, plural, num) + +class LocalizerRequestMixin(object): + @reify + def localizer(self): + """ Convenience property to return a localizer """ + registry = self.registry + + current_locale_name = self.locale_name + localizer = registry.queryUtility(ILocalizer, name=current_locale_name) + + if localizer is None: + # no localizer utility registered yet + tdirs = registry.queryUtility(ITranslationDirectories, default=[]) + localizer = make_localizer(current_locale_name, tdirs) + + registry.registerUtility(localizer, ILocalizer, + name=current_locale_name) + + return localizer + + @reify + def locale_name(self): + locale_name = negotiate_locale_name(self) + return locale_name + + diff --git a/src/pyramid/interfaces.py b/src/pyramid/interfaces.py new file mode 100644 index 000000000..4df5593f8 --- /dev/null +++ b/src/pyramid/interfaces.py @@ -0,0 +1,1354 @@ +from zope.deprecation import deprecated + +from zope.interface import ( + Attribute, + Interface, + ) + +from pyramid.compat import PY2 + +# public API interfaces + +class IContextFound(Interface): + """ An event type that is emitted after :app:`Pyramid` finds a + :term:`context` object but before it calls any view code. See the + documentation attached to :class:`pyramid.events.ContextFound` + for more information. + + .. note:: + + For backwards compatibility with versions of + :app:`Pyramid` before 1.0, this event interface can also be + imported as :class:`pyramid.interfaces.IAfterTraversal`. + """ + request = Attribute('The request object') + +IAfterTraversal = IContextFound + +class IBeforeTraversal(Interface): + """ + An event type that is emitted after :app:`Pyramid` attempted to find a + route but before it calls any traversal or view code. See the documentation + attached to :class:`pyramid.events.Routefound` for more information. + """ + request = Attribute('The request object') + +class INewRequest(Interface): + """ An event type that is emitted whenever :app:`Pyramid` + begins to process a new request. See the documentation attached + to :class:`pyramid.events.NewRequest` for more information.""" + request = Attribute('The request object') + +class INewResponse(Interface): + """ An event type that is emitted whenever any :app:`Pyramid` + view returns a response. See the + documentation attached to :class:`pyramid.events.NewResponse` + for more information.""" + request = Attribute('The request object') + response = Attribute('The response object') + +class IApplicationCreated(Interface): + """ Event issued when the + :meth:`pyramid.config.Configurator.make_wsgi_app` method + is called. See the documentation attached to + :class:`pyramid.events.ApplicationCreated` for more + information. + + .. note:: + + For backwards compatibility with :app:`Pyramid` + versions before 1.0, this interface can also be imported as + :class:`pyramid.interfaces.IWSGIApplicationCreatedEvent`. + """ + app = Attribute("Created application") + +IWSGIApplicationCreatedEvent = IApplicationCreated # b /c + +class IResponse(Interface): + """ Represents a WSGI response using the WebOb response interface. + Some attribute and method documentation of this interface references + :rfc:`2616`. + + This interface is most famously implemented by + :class:`pyramid.response.Response` and the HTTP exception classes in + :mod:`pyramid.httpexceptions`.""" + + RequestClass = Attribute( + """ Alias for :class:`pyramid.request.Request` """) + + def __call__(environ, start_response): + """ :term:`WSGI` call interface, should call the start_response + callback and should return an iterable""" + + accept_ranges = Attribute( + """Gets and sets and deletes the Accept-Ranges header. For more + information on Accept-Ranges see RFC 2616, section 14.5""") + + age = Attribute( + """Gets and sets and deletes the Age header. Converts using int. + For more information on Age see RFC 2616, section 14.6.""") + + allow = Attribute( + """Gets and sets and deletes the Allow header. Converts using + list. For more information on Allow see RFC 2616, Section 14.7.""") + + app_iter = Attribute( + """Returns the app_iter of the response. + + If body was set, this will create an app_iter from that body + (a single-item list)""") + + def app_iter_range(start, stop): + """ Return a new app_iter built from the response app_iter that + serves up only the given start:stop range. """ + + body = Attribute( + """The body of the response, as a str. This will read in the entire + app_iter if necessary.""") + + body_file = Attribute( + """A file-like object that can be used to write to the body. If you + passed in a list app_iter, that app_iter will be modified by writes.""") + + cache_control = Attribute( + """Get/set/modify the Cache-Control header (RFC 2616 section 14.9)""") + + cache_expires = Attribute( + """ Get/set the Cache-Control and Expires headers. This sets the + response to expire in the number of seconds passed when set. """) + + charset = Attribute( + """Get/set the charset (in the Content-Type)""") + + def conditional_response_app(environ, start_response): + """ Like the normal __call__ interface, but checks conditional + headers: + + - If-Modified-Since (304 Not Modified; only on GET, HEAD) + + - If-None-Match (304 Not Modified; only on GET, HEAD) + + - Range (406 Partial Content; only on GET, HEAD)""" + + content_disposition = Attribute( + """Gets and sets and deletes the Content-Disposition header. + For more information on Content-Disposition see RFC 2616 section + 19.5.1.""") + + content_encoding = Attribute( + """Gets and sets and deletes the Content-Encoding header. For more + information about Content-Encoding see RFC 2616 section 14.11.""") + + content_language = Attribute( + """Gets and sets and deletes the Content-Language header. Converts + using list. For more information about Content-Language see RFC 2616 + section 14.12.""") + + content_length = Attribute( + """Gets and sets and deletes the Content-Length header. For more + information on Content-Length see RFC 2616 section 14.17. + Converts using int. """) + + content_location = Attribute( + """Gets and sets and deletes the Content-Location header. For more + information on Content-Location see RFC 2616 section 14.14.""") + + content_md5 = Attribute( + """Gets and sets and deletes the Content-MD5 header. For more + information on Content-MD5 see RFC 2616 section 14.14.""") + + content_range = Attribute( + """Gets and sets and deletes the Content-Range header. For more + information on Content-Range see section 14.16. Converts using + ContentRange object.""") + + content_type = Attribute( + """Get/set the Content-Type header (or None), without the charset + or any parameters. If you include parameters (or ; at all) when + setting the content_type, any existing parameters will be deleted; + otherwise they will be preserved.""") + + content_type_params = Attribute( + """A dictionary of all the parameters in the content type. This is + not a view, set to change, modifications of the dict would not + be applied otherwise.""") + + def copy(): + """ Makes a copy of the response and returns the copy. """ + + date = Attribute( + """Gets and sets and deletes the Date header. For more information on + Date see RFC 2616 section 14.18. Converts using HTTP date.""") + + def delete_cookie(name, path='/', domain=None): + """ Delete a cookie from the client. Note that path and domain must + match how the cookie was originally set. This sets the cookie to the + empty string, and max_age=0 so that it should expire immediately. """ + + def encode_content(encoding='gzip', lazy=False): + """ Encode the content with the given encoding (only gzip and + identity are supported).""" + + environ = Attribute( + """Get/set the request environ associated with this response, + if any.""") + + etag = Attribute( + """ Gets and sets and deletes the ETag header. For more information + on ETag see RFC 2616 section 14.19. Converts using Entity tag.""") + + expires = Attribute( + """ Gets and sets and deletes the Expires header. For more + information on Expires see RFC 2616 section 14.21. Converts using + HTTP date.""") + + headerlist = Attribute( + """ The list of response headers. """) + + headers = Attribute( + """ The headers in a dictionary-like object """) + + last_modified = Attribute( + """ Gets and sets and deletes the Last-Modified header. For more + information on Last-Modified see RFC 2616 section 14.29. Converts + using HTTP date.""") + + location = Attribute( + """ Gets and sets and deletes the Location header. For more + information on Location see RFC 2616 section 14.30.""") + + def md5_etag(body=None, set_content_md5=False): + """ Generate an etag for the response object using an MD5 hash of the + body (the body parameter, or self.body if not given). Sets self.etag. + If set_content_md5 is True sets self.content_md5 as well """ + + def merge_cookies(resp): + """ Merge the cookies that were set on this response with the given + resp object (which can be any WSGI application). If the resp is a + webob.Response object, then the other object will be modified + in-place. """ + + pragma = Attribute( + """ Gets and sets and deletes the Pragma header. For more information + on Pragma see RFC 2616 section 14.32. """) + + request = Attribute( + """ Return the request associated with this response if any. """) + + retry_after = Attribute( + """ Gets and sets and deletes the Retry-After header. For more + information on Retry-After see RFC 2616 section 14.37. Converts + using HTTP date or delta seconds.""") + + server = Attribute( + """ Gets and sets and deletes the Server header. For more information + on Server see RFC216 section 14.38. """) + + def set_cookie(name, value='', max_age=None, path='/', domain=None, + secure=False, httponly=False, comment=None, expires=None, + overwrite=False): + """ Set (add) a cookie for the response """ + + status = Attribute( + """ The status string. """) + + status_int = Attribute( + """ The status as an integer """) + + unicode_body = Attribute( + """ Get/set the unicode value of the body (using the charset of + the Content-Type)""") + + def unset_cookie(name, strict=True): + """ Unset a cookie with the given name (remove it from the + response).""" + + vary = Attribute( + """Gets and sets and deletes the Vary header. For more information + on Vary see section 14.44. Converts using list.""") + + www_authenticate = Attribute( + """ Gets and sets and deletes the WWW-Authenticate header. For more + information on WWW-Authenticate see RFC 2616 section 14.47. Converts + using 'parse_auth' and 'serialize_auth'. """) + +class IException(Interface): # not an API + """ An interface representing a generic exception """ + +class IExceptionResponse(IException, IResponse): + """ An interface representing a WSGI response which is also an exception + object. Register an exception view using this interface as a ``context`` + to apply the registered view for all exception types raised by + :app:`Pyramid` internally (any exception that inherits from + :class:`pyramid.response.Response`, including + :class:`pyramid.httpexceptions.HTTPNotFound` and + :class:`pyramid.httpexceptions.HTTPForbidden`).""" + def prepare(environ): + """ Prepares the response for being called as a WSGI application """ + +class IDict(Interface): + # Documentation-only interface + + def __contains__(k): + """ Return ``True`` if key ``k`` exists in the dictionary.""" + + def __setitem__(k, value): + """ Set a key/value pair into the dictionary""" + + def __delitem__(k): + """ Delete an item from the dictionary which is passed to the + renderer as the renderer globals dictionary.""" + + def __getitem__(k): + """ Return the value for key ``k`` from the dictionary or raise a + KeyError if the key doesn't exist""" + + def __iter__(): + """ Return an iterator over the keys of this dictionary """ + + def get(k, default=None): + """ Return the value for key ``k`` from the renderer dictionary, or + the default if no such value exists.""" + + def items(): + """ Return a list of [(k,v)] pairs from the dictionary """ + + def keys(): + """ Return a list of keys from the dictionary """ + + def values(): + """ Return a list of values from the dictionary """ + + if PY2: + + def iterkeys(): + """ Return an iterator of keys from the dictionary """ + + def iteritems(): + """ Return an iterator of (k,v) pairs from the dictionary """ + + def itervalues(): + """ Return an iterator of values from the dictionary """ + + has_key = __contains__ + + def pop(k, default=None): + """ Pop the key k from the dictionary and return its value. If k + doesn't exist, and default is provided, return the default. If k + doesn't exist and default is not provided, raise a KeyError.""" + + def popitem(): + """ Pop the item with key k from the dictionary and return it as a + two-tuple (k, v). If k doesn't exist, raise a KeyError.""" + + def setdefault(k, default=None): + """ Return the existing value for key ``k`` in the dictionary. If no + value with ``k`` exists in the dictionary, set the ``default`` + value into the dictionary under the k name passed. If a value already + existed in the dictionary, return it. If a value did not exist in + the dictionary, return the default""" + + def update(d): + """ Update the renderer dictionary with another dictionary ``d``.""" + + def clear(): + """ Clear all values from the dictionary """ + +class IBeforeRender(IDict): + """ + Subscribers to this event may introspect and modify the set of + :term:`renderer globals` before they are passed to a :term:`renderer`. + The event object itself provides a dictionary-like interface for adding + and removing :term:`renderer globals`. The keys and values of the + dictionary are those globals. For example:: + + from repoze.events import subscriber + from pyramid.interfaces import IBeforeRender + + @subscriber(IBeforeRender) + def add_global(event): + event['mykey'] = 'foo' + + .. seealso:: + + See also :ref:`beforerender_event`. + """ + rendering_val = Attribute('The value returned by a view or passed to a ' + '``render`` method for this rendering. ' + 'This feature is new in Pyramid 1.2.') + +class IRendererInfo(Interface): + """ An object implementing this interface is passed to every + :term:`renderer factory` constructor as its only argument (conventionally + named ``info``)""" + name = Attribute('The value passed by the user as the renderer name') + package = Attribute('The "current package" when the renderer ' + 'configuration statement was found') + type = Attribute('The renderer type name') + registry = Attribute('The "current" application registry when the ' + 'renderer was created') + settings = Attribute('The deployment settings dictionary related ' + 'to the current application') + + def clone(): + """ Return a shallow copy that does not share any mutable state.""" + +class IRendererFactory(Interface): + def __call__(info): + """ Return an object that implements + :class:`pyramid.interfaces.IRenderer`. ``info`` is an + object that implements :class:`pyramid.interfaces.IRendererInfo`. + """ + +class IRenderer(Interface): + def __call__(value, system): + """ Call the renderer with the result of the + view (``value``) passed in and return a result (a string or + unicode object useful as a response body). Values computed by + the system are passed by the system in the ``system`` + parameter, which is a dictionary. Keys in the dictionary + include: ``view`` (the view callable that returned the value), + ``renderer_name`` (the template name or simple name of the + renderer), ``context`` (the context object passed to the + view), and ``request`` (the request object passed to the + view).""" + +class ITemplateRenderer(IRenderer): + def implementation(): + """ Return the object that the underlying templating system + uses to render the template; it is typically a callable that + accepts arbitrary keyword arguments and returns a string or + unicode object """ + +deprecated( + 'ITemplateRenderer', + 'As of Pyramid 1.5 the, "pyramid.interfaces.ITemplateRenderer" interface ' + 'is scheduled to be removed. It was used by the Mako and Chameleon ' + 'renderers which have been split into their own packages.' + ) + +class IViewMapper(Interface): + def __call__(self, object): + """ Provided with an arbitrary object (a function, class, or + instance), returns a callable with the call signature ``(context, + request)``. The callable returned should itself return a Response + object. An IViewMapper is returned by + :class:`pyramid.interfaces.IViewMapperFactory`.""" + +class IViewMapperFactory(Interface): + def __call__(self, **kw): + """ + Return an object which implements + :class:`pyramid.interfaces.IViewMapper`. ``kw`` will be a dictionary + containing view-specific arguments, such as ``permission``, + ``predicates``, ``attr``, ``renderer``, and other items. An + IViewMapperFactory is used by + :meth:`pyramid.config.Configurator.add_view` to provide a plugpoint + to extension developers who want to modify potential view callable + invocation signatures and response values. + """ + +class IAuthenticationPolicy(Interface): + """ An object representing a Pyramid authentication policy. """ + + def authenticated_userid(request): + """ Return the authenticated :term:`userid` or ``None`` if + no authenticated userid can be found. This method of the + policy should ensure that a record exists in whatever + persistent store is used related to the user (the user + should not have been deleted); if a record associated with + the current id does not exist in a persistent store, it + should return ``None``. + + """ + + def unauthenticated_userid(request): + """ Return the *unauthenticated* userid. This method + performs the same duty as ``authenticated_userid`` but is + permitted to return the userid based only on data present + in the request; it needn't (and shouldn't) check any + persistent store to ensure that the user record related to + the request userid exists. + + This method is intended primarily a helper to assist the + ``authenticated_userid`` method in pulling credentials out + of the request data, abstracting away the specific headers, + query strings, etc that are used to authenticate the request. + + """ + + def effective_principals(request): + """ Return a sequence representing the effective principals + typically including the :term:`userid` and any groups belonged + to by the current user, always including 'system' groups such + as ``pyramid.security.Everyone`` and + ``pyramid.security.Authenticated``. + + """ + + def remember(request, userid, **kw): + """ Return a set of headers suitable for 'remembering' the + :term:`userid` named ``userid`` when set in a response. An + individual authentication policy and its consumers can + decide on the composition and meaning of ``**kw``. + + """ + + def forget(request): + """ Return a set of headers suitable for 'forgetting' the + current user on subsequent requests. + + """ + +class IAuthorizationPolicy(Interface): + """ An object representing a Pyramid authorization policy. """ + def permits(context, principals, permission): + """ Return an instance of :class:`pyramid.security.Allowed` if any + of the ``principals`` is allowed the ``permission`` in the current + ``context``, else return an instance of + :class:`pyramid.security.Denied`. + """ + + def principals_allowed_by_permission(context, permission): + """ Return a set of principal identifiers allowed by the + ``permission`` in ``context``. This behavior is optional; if you + choose to not implement it you should define this method as + something which raises a ``NotImplementedError``. This method + will only be called when the + ``pyramid.security.principals_allowed_by_permission`` API is + used.""" + +class IMultiDict(IDict): # docs-only interface + """ + An ordered dictionary that can have multiple values for each key. A + multidict adds the methods ``getall``, ``getone``, ``mixed``, ``extend``, + ``add``, and ``dict_of_lists`` to the normal dictionary interface. A + multidict data structure is used as ``request.POST``, ``request.GET``, + and ``request.params`` within an :app:`Pyramid` application. + """ + + def add(key, value): + """ Add the key and value, not overwriting any previous value. """ + + def dict_of_lists(): + """ + Returns a dictionary where each key is associated with a list of + values. + """ + + def extend(other=None, **kwargs): + """ Add a set of keys and values, not overwriting any previous + values. The ``other`` structure may be a list of two-tuples or a + dictionary. If ``**kwargs`` is passed, its value *will* overwrite + existing values.""" + + def getall(key): + """ Return a list of all values matching the key (may be an empty + list) """ + + def getone(key): + """ Get one value matching the key, raising a KeyError if multiple + values were found. """ + + def mixed(): + """ Returns a dictionary where the values are either single values, + or a list of values when a key/value appears more than once in this + dictionary. This is similar to the kind of dictionary often used to + represent the variables in a web request. """ + +# internal interfaces + +class IRequest(Interface): + """ Request type interface attached to all request objects """ + +class ITweens(Interface): + """ Marker interface for utility registration representing the ordered + set of a configuration's tween factories""" + +class IRequestHandler(Interface): + """ """ + def __call__(self, request): + """ Must return a tuple of IReqest, IResponse or raise an exception. + The ``request`` argument will be an instance of an object that + provides IRequest.""" + +IRequest.combined = IRequest # for exception view lookups + +class IRequestExtensions(Interface): + """ Marker interface for storing request extensions (properties and + methods) which will be added to the request object.""" + descriptors = Attribute( + """A list of descriptors that will be added to each request.""") + methods = Attribute( + """A list of methods to be added to each request.""") + +class IRouteRequest(Interface): + """ *internal only* interface used as in a utility lookup to find + route-specific interfaces. Not an API.""" + +class IAcceptOrder(Interface): + """ + Marker interface for a list of accept headers with the most important + first. + + """ + +class IStaticURLInfo(Interface): + """ A policy for generating URLs to static assets """ + def add(config, name, spec, **extra): + """ Add a new static info registration """ + + def generate(path, request, **kw): + """ Generate a URL for the given path """ + + def add_cache_buster(config, spec, cache_buster): + """ Add a new cache buster to a particular set of assets """ + +class IResponseFactory(Interface): + """ A utility which generates a response """ + def __call__(request): + """ Return a response object implementing IResponse, + e.g. :class:`pyramid.response.Response`). It should handle the + case when ``request`` is ``None``.""" + +class IRequestFactory(Interface): + """ A utility which generates a request """ + def __call__(environ): + """ Return an instance of ``pyramid.request.Request``""" + + def blank(path): + """ Return an empty request object (see + :meth:`pyramid.request.Request.blank`)""" + +class IViewClassifier(Interface): + """ *Internal only* marker interface for views.""" + +class IExceptionViewClassifier(Interface): + """ *Internal only* marker interface for exception views.""" + +class IView(Interface): + def __call__(context, request): + """ Must return an object that implements IResponse. """ + +class ISecuredView(IView): + """ *Internal only* interface. Not an API. """ + def __call_permissive__(context, request): + """ Guaranteed-permissive version of __call__ """ + + def __permitted__(context, request): + """ Return True if view execution will be permitted using the + context and request, False otherwise""" + +class IMultiView(ISecuredView): + """ *internal only*. A multiview is a secured view that is a + collection of other views. Each of the views is associated with + zero or more predicates. Not an API.""" + def add(view, predicates, order, accept=None, phash=None): + """ Add a view to the multiview. """ + +class IRootFactory(Interface): + def __call__(request): + """ Return a root object based on the request """ + +class IDefaultRootFactory(Interface): + def __call__(request): + """ Return the *default* root object for an application """ + +class ITraverser(Interface): + def __call__(request): + """ Return a dictionary with (at least) the keys ``root``, + ``context``, ``view_name``, ``subpath``, ``traversed``, + ``virtual_root``, and ``virtual_root_path``. These values are + typically the result of an object graph traversal. ``root`` is the + physical root object, ``context`` will be a model object, + ``view_name`` will be the view name used (a Unicode name), + ``subpath`` will be a sequence of Unicode names that followed the + view name but were not traversed, ``traversed`` will be a sequence of + Unicode names that were traversed (including the virtual root path, + if any) ``virtual_root`` will be a model object representing the + virtual root (or the physical root if traversal was not performed), + and ``virtual_root_path`` will be a sequence representing the virtual + root path (a sequence of Unicode names) or ``None`` if traversal was + not performed. + + Extra keys for special purpose functionality can be returned as + necessary. + + All values returned in the dictionary will be made available + as attributes of the ``request`` object by the :term:`router`. + """ + +ITraverserFactory = ITraverser # b / c for 1.0 code + +class IViewPermission(Interface): + def __call__(context, request): + """ Return True if the permission allows, return False if it denies. + """ + +class IRouter(Interface): + """ + WSGI application which routes requests to 'view' code based on + a view registry. + + """ + registry = Attribute( + """Component architecture registry local to this application.""") + + def request_context(environ): + """ + Create a new request context from a WSGI environ. + + The request context is used to push/pop the threadlocals required + when processing the request. It also contains an initialized + :class:`pyramid.interfaces.IRequest` instance using the registered + :class:`pyramid.interfaces.IRequestFactory`. The context may be + used as a context manager to control the threadlocal lifecycle: + + .. code-block:: python + + with router.request_context(environ) as request: + ... + + Alternatively, the context may be used without the ``with`` statement + by manually invoking its ``begin()`` and ``end()`` methods. + + .. code-block:: python + + ctx = router.request_context(environ) + request = ctx.begin() + try: + ... + finally: + ctx.end() + + """ + + def invoke_request(request): + """ + Invoke the :app:`Pyramid` request pipeline. + + See :ref:`router_chapter` for information on the request pipeline. + + The output should be a :class:`pyramid.interfaces.IResponse` object + or a raised exception. + + """ + +class IExecutionPolicy(Interface): + def __call__(environ, router): + """ + This callable triggers the router to process a raw WSGI environ dict + into a response and controls the :app:`Pyramid` request pipeline. + + The ``environ`` is the raw WSGI environ. + + The ``router`` is an :class:`pyramid.interfaces.IRouter` object which + should be used to create a request object and send it into the + processing pipeline. + + The return value should be a :class:`pyramid.interfaces.IResponse` + object or an exception that will be handled by WSGI middleware. + + The default execution policy simply creates a request and sends it + through the pipeline, attempting to render any exception that escapes: + + .. code-block:: python + + def simple_execution_policy(environ, router): + with router.request_context(environ) as request: + try: + return router.invoke_request(request) + except Exception: + return request.invoke_exception_view(reraise=True) + """ + +class ISettings(IDict): + """ Runtime settings utility for pyramid; represents the + deployment settings for the application. Implements a mapping + interface.""" + +# this interface, even if it becomes unused within Pyramid, is +# imported by other packages (such as traversalwrapper) +class ILocation(Interface): + """Objects that have a structural location""" + __parent__ = Attribute("The parent in the location hierarchy") + __name__ = Attribute("The name within the parent") + +class IDebugLogger(Interface): + """ Interface representing a PEP 282 logger """ + +ILogger = IDebugLogger # b/c + +class IRoutePregenerator(Interface): + def __call__(request, elements, kw): + + """ A pregenerator is a function associated by a developer with a + :term:`route`. The pregenerator for a route is called by + :meth:`pyramid.request.Request.route_url` in order to adjust the set + of arguments passed to it by the user for special purposes, such as + Pylons 'subdomain' support. It will influence the URL returned by + ``route_url``. + + A pregenerator should return a two-tuple of ``(elements, kw)`` + after examining the originals passed to this function, which + are the arguments ``(request, elements, kw)``. The simplest + pregenerator is:: + + def pregenerator(request, elements, kw): + return elements, kw + + You can employ a pregenerator by passing a ``pregenerator`` + argument to the + :meth:`pyramid.config.Configurator.add_route` + function. + + """ + +class IRoute(Interface): + """ Interface representing the type of object returned from + ``IRoutesMapper.get_route``""" + name = Attribute('The route name') + pattern = Attribute('The route pattern') + factory = Attribute( + 'The :term:`root factory` used by the :app:`Pyramid` router ' + 'when this route matches (or ``None``)') + predicates = Attribute( + 'A sequence of :term:`route predicate` objects used to ' + 'determine if a request matches this route or not after ' + 'basic pattern matching has been completed.') + pregenerator = Attribute('This attribute should either be ``None`` or ' + 'a callable object implementing the ' + '``IRoutePregenerator`` interface') + + def match(path): + """ + If the ``path`` passed to this function can be matched by the + ``pattern`` of this route, return a dictionary (the + 'matchdict'), which will contain keys representing the dynamic + segment markers in the pattern mapped to values extracted from + the provided ``path``. + + If the ``path`` passed to this function cannot be matched by + the ``pattern`` of this route, return ``None``. + """ + def generate(kw): + """ + Generate a URL based on filling in the dynamic segment markers + in the pattern using the ``kw`` dictionary provided. + """ + +class IRoutesMapper(Interface): + """ Interface representing a Routes ``Mapper`` object """ + def get_routes(): + """ Return a sequence of Route objects registered in the mapper. + Static routes will not be returned in this sequence.""" + + def has_routes(): + """ Returns ``True`` if any route has been registered. """ + + def get_route(name): + """ Returns an ``IRoute`` object if a route with the name ``name`` + was registered, otherwise return ``None``.""" + + def connect(name, pattern, factory=None, predicates=(), pregenerator=None, + static=True): + """ Add a new route. """ + + def generate(name, kw): + """ Generate a URL using the route named ``name`` with the + keywords implied by kw""" + + def __call__(request): + """ Return a dictionary containing matching information for + the request; the ``route`` key of this dictionary will either + be a Route object or ``None`` if no route matched; the + ``match`` key will be the matchdict or ``None`` if no route + matched. Static routes will not be considered for matching. """ + +class IResourceURL(Interface): + virtual_path = Attribute( + 'The virtual url path of the resource as a string.' + ) + physical_path = Attribute( + 'The physical url path of the resource as a string.' + ) + virtual_path_tuple = Attribute( + 'The virtual url path of the resource as a tuple. (New in 1.5)' + ) + physical_path_tuple = Attribute( + 'The physical url path of the resource as a tuple. (New in 1.5)' + ) + +class IPEP302Loader(Interface): + """ See http://www.python.org/dev/peps/pep-0302/#id30. + """ + def get_data(path): + """ Retrieve data for and arbitrary "files" from storage backend. + + Raise IOError for not found. + + Data is returned as bytes. + """ + + def is_package(fullname): + """ Return True if the module specified by 'fullname' is a package. + """ + + def get_code(fullname): + """ Return the code object for the module identified by 'fullname'. + + Return 'None' if it's a built-in or extension module. + + If the loader doesn't have the code object but it does have the source + code, return the compiled source code. + + Raise ImportError if the module can't be found by the importer at all. + """ + + def get_source(fullname): + """ Return the source code for the module identified by 'fullname'. + + Return a string, using newline characters for line endings, or None + if the source is not available. + + Raise ImportError if the module can't be found by the importer at all. + """ + + def get_filename(fullname): + """ Return the value of '__file__' if the named module was loaded. + + If the module is not found, raise ImportError. + """ + + +class IPackageOverrides(IPEP302Loader): + """ Utility for pkg_resources overrides """ + +# VH_ROOT_KEY is an interface; its imported from other packages (e.g. +# traversalwrapper) +VH_ROOT_KEY = 'HTTP_X_VHM_ROOT' + +class ILocalizer(Interface): + """ Localizer for a specific language """ + +class ILocaleNegotiator(Interface): + def __call__(request): + """ Return a locale name """ + +class ITranslationDirectories(Interface): + """ A list object representing all known translation directories + for an application""" + +class IDefaultPermission(Interface): + """ A string object representing the default permission to be used + for all view configurations which do not explicitly declare their + own.""" + +class IDefaultCSRFOptions(Interface): + """ An object representing the default CSRF settings to be used for + all view configurations which do not explicitly declare their own.""" + require_csrf = Attribute( + 'Boolean attribute. If ``True``, then CSRF checks will be enabled by ' + 'default for the view unless overridden.') + token = Attribute('The key to be matched in the body of the request.') + header = Attribute('The header to be matched with the CSRF token.') + safe_methods = Attribute('A set of safe methods that skip CSRF checks.') + callback = Attribute('A callback to disable CSRF checks per-request.') + +class ISessionFactory(Interface): + """ An interface representing a factory which accepts a request object and + returns an ISession object """ + def __call__(request): + """ Return an ISession object """ + +class ISession(IDict): + """ An interface representing a session (a web session object, + usually accessed via ``request.session``. + + Keys and values of a session must be pickleable. + + .. warning:: + + In :app:`Pyramid` 2.0 the session will only be required to support + types that can be serialized using JSON. It's recommended to switch any + session implementations to support only JSON and to only store primitive + types in sessions. See :ref:`pickle_session_deprecation` for more + information about why this change is being made. + + .. versionchanged:: 1.9 + + Sessions are no longer required to implement ``get_csrf_token`` and + ``new_csrf_token``. CSRF token support was moved to the pluggable + :class:`pyramid.interfaces.ICSRFStoragePolicy` configuration hook. + + """ + + # attributes + + created = Attribute('Integer representing Epoch time when created.') + new = Attribute('Boolean attribute. If ``True``, the session is new.') + + # special methods + + def invalidate(): + """ Invalidate the session. The action caused by + ``invalidate`` is implementation-dependent, but it should have + the effect of completely dissociating any data stored in the + session with the current request. It might set response + values (such as one which clears a cookie), or it might not. + + An invalidated session may be used after the call to ``invalidate`` + with the effect that a new session is created to store the data. This + enables workflows requiring an entirely new session, such as in the + case of changing privilege levels or preventing fixation attacks. + """ + + def changed(): + """ Mark the session as changed. A user of a session should + call this method after he or she mutates a mutable object that + is *a value of the session* (it should not be required after + mutating the session itself). For example, if the user has + stored a dictionary in the session under the key ``foo``, and + he or she does ``session['foo'] = {}``, ``changed()`` needn't + be called. However, if subsequently he or she does + ``session['foo']['a'] = 1``, ``changed()`` must be called for + the sessioning machinery to notice the mutation of the + internal dictionary.""" + + def flash(msg, queue='', allow_duplicate=True): + """ Push a flash message onto the end of the flash queue represented + by ``queue``. An alternate flash message queue can used by passing + an optional ``queue``, which must be a string. If + ``allow_duplicate`` is false, if the ``msg`` already exists in the + queue, it will not be re-added.""" + + def pop_flash(queue=''): + """ Pop a queue from the flash storage. The queue is removed from + flash storage after this message is called. The queue is returned; + it is a list of flash messages added by + :meth:`pyramid.interfaces.ISession.flash`""" + + def peek_flash(queue=''): + """ Peek at a queue in the flash storage. The queue remains in + flash storage after this message is called. The queue is returned; + it is a list of flash messages added by + :meth:`pyramid.interfaces.ISession.flash` + """ + + +class ICSRFStoragePolicy(Interface): + """ An object that offers the ability to verify CSRF tokens and generate + new ones.""" + + def new_csrf_token(request): + """ Create and return a new, random cross-site request forgery + protection token. The token will be an ascii-compatible unicode + string. + + """ + + def get_csrf_token(request): + """ Return a cross-site request forgery protection token. It + will be an ascii-compatible unicode string. If a token was previously + set for this user via ``new_csrf_token``, that token will be returned. + If no CSRF token was previously set, ``new_csrf_token`` will be + called, which will create and set a token, and this token will be + returned. + + """ + + def check_csrf_token(request, token): + """ Determine if the supplied ``token`` is valid. Most implementations + should simply compare the ``token`` to the current value of + ``get_csrf_token`` but it is possible to verify the token using + any mechanism necessary using this method. + + Returns ``True`` if the ``token`` is valid, otherwise ``False``. + + """ + + +class IIntrospector(Interface): + def get(category_name, discriminator, default=None): + """ Get the IIntrospectable related to the category_name and the + discriminator (or discriminator hash) ``discriminator``. If it does + not exist in the introspector, return the value of ``default`` """ + + def get_category(category_name, default=None, sort_key=None): + """ Get a sequence of dictionaries in the form + ``[{'introspectable':IIntrospectable, 'related':[sequence of related + IIntrospectables]}, ...]`` where each introspectable is part of the + category associated with ``category_name`` . + + If the category named ``category_name`` does not exist in the + introspector the value passed as ``default`` will be returned. + + If ``sort_key`` is ``None``, the sequence will be returned in the + order the introspectables were added to the introspector. Otherwise, + sort_key should be a function that accepts an IIntrospectable and + returns a value from it (ala the ``key`` function of Python's + ``sorted`` callable).""" + + def categories(): + """ Return a sorted sequence of category names known by + this introspector """ + + def categorized(sort_key=None): + """ Get a sequence of tuples in the form ``[(category_name, + [{'introspectable':IIntrospectable, 'related':[sequence of related + IIntrospectables]}, ...])]`` representing all known + introspectables. If ``sort_key`` is ``None``, each introspectables + sequence will be returned in the order the introspectables were added + to the introspector. Otherwise, sort_key should be a function that + accepts an IIntrospectable and returns a value from it (ala the + ``key`` function of Python's ``sorted`` callable).""" + + def remove(category_name, discriminator): + """ Remove the IIntrospectable related to ``category_name`` and + ``discriminator`` from the introspector, and fix up any relations + that the introspectable participates in. This method will not raise + an error if an introspectable related to the category name and + discriminator does not exist.""" + + def related(intr): + """ Return a sequence of IIntrospectables related to the + IIntrospectable ``intr``. Return the empty sequence if no relations + for exist.""" + + def add(intr): + """ Add the IIntrospectable ``intr`` (use instead of + :meth:`pyramid.interfaces.IIntrospector.add` when you have a custom + IIntrospectable). Replaces any existing introspectable registered + using the same category/discriminator. + + This method is not typically called directly, instead it's called + indirectly by :meth:`pyramid.interfaces.IIntrospector.register`""" + + def relate(*pairs): + """ Given any number of ``(category_name, discriminator)`` pairs + passed as positional arguments, relate the associated introspectables + to each other. The introspectable related to each pair must have + already been added via ``.add`` or ``.add_intr``; a :exc:`KeyError` + will result if this is not true. An error will not be raised if any + pair has already been associated with another. + + This method is not typically called directly, instead it's called + indirectly by :meth:`pyramid.interfaces.IIntrospector.register` + """ + + def unrelate(*pairs): + """ Given any number of ``(category_name, discriminator)`` pairs + passed as positional arguments, unrelate the associated introspectables + from each other. The introspectable related to each pair must have + already been added via ``.add`` or ``.add_intr``; a :exc:`KeyError` + will result if this is not true. An error will not be raised if any + pair is not already related to another. + + This method is not typically called directly, instead it's called + indirectly by :meth:`pyramid.interfaces.IIntrospector.register` + """ + + +class IIntrospectable(Interface): + """ An introspectable object used for configuration introspection. In + addition to the methods below, objects which implement this interface + must also implement all the methods of Python's + ``collections.MutableMapping`` (the "dictionary interface"), and must be + hashable.""" + + title = Attribute('Text title describing this introspectable') + type_name = Attribute('Text type name describing this introspectable') + order = Attribute('integer order in which registered with introspector ' + '(managed by introspector, usually)') + category_name = Attribute('introspection category name') + discriminator = Attribute('introspectable discriminator (within category) ' + '(must be hashable)') + discriminator_hash = Attribute('an integer hash of the discriminator') + action_info = Attribute('An IActionInfo object representing the caller ' + 'that invoked the creation of this introspectable ' + '(usually a sentinel until updated during ' + 'self.register)') + + def relate(category_name, discriminator): + """ Indicate an intent to relate this IIntrospectable with another + IIntrospectable (the one associated with the ``category_name`` and + ``discriminator``) during action execution. + """ + + def unrelate(category_name, discriminator): + """ Indicate an intent to break the relationship between this + IIntrospectable with another IIntrospectable (the one associated with + the ``category_name`` and ``discriminator``) during action execution. + """ + + def register(introspector, action_info): + """ Register this IIntrospectable with an introspector. This method + is invoked during action execution. Adds the introspectable and its + relations to the introspector. ``introspector`` should be an object + implementing IIntrospector. ``action_info`` should be a object + implementing the interface :class:`pyramid.interfaces.IActionInfo` + representing the call that registered this introspectable. + Pseudocode for an implementation of this method: + + .. code-block:: python + + def register(self, introspector, action_info): + self.action_info = action_info + introspector.add(self) + for methodname, category_name, discriminator in self._relations: + method = getattr(introspector, methodname) + method((i.category_name, i.discriminator), + (category_name, discriminator)) + """ + + def __hash__(): + + """ Introspectables must be hashable. The typical implementation of + an introsepectable's __hash__ is:: + + return hash((self.category_name,) + (self.discriminator,)) + """ + +class IActionInfo(Interface): + """ Class which provides code introspection capability associated with an + action. The ParserInfo class used by ZCML implements the same interface.""" + file = Attribute( + 'Filename of action-invoking code as a string') + line = Attribute( + 'Starting line number in file (as an integer) of action-invoking code.' + 'This will be ``None`` if the value could not be determined.') + + def __str__(): + """ Return a representation of the action information (including + source code from file, if possible) """ + +class IAssetDescriptor(Interface): + """ + Describes an :term:`asset`. + """ + + def absspec(): + """ + Returns the absolute asset specification for this asset + (e.g. ``mypackage:templates/foo.pt``). + """ + + def abspath(): + """ + Returns an absolute path in the filesystem to the asset. + """ + + def stream(): + """ + Returns an input stream for reading asset contents. Raises an + exception if the asset is a directory or does not exist. + """ + + def isdir(): + """ + Returns True if the asset is a directory, otherwise returns False. + """ + + def listdir(): + """ + Returns iterable of filenames of directory contents. Raises an + exception if asset is not a directory. + """ + + def exists(): + """ + Returns True if asset exists, otherwise returns False. + """ + +class IJSONAdapter(Interface): + """ + Marker interface for objects that can convert an arbitrary object + into a JSON-serializable primitive. + """ + +class IPredicateList(Interface): + """ Interface representing a predicate list """ + +class IViewDeriver(Interface): + options = Attribute('A list of supported options to be passed to ' + ':meth:`pyramid.config.Configurator.add_view`. ' + 'This attribute is optional.') + + def __call__(view, info): + """ + Derive a new view from the supplied view. + + View options, package information and registry are available on + ``info``, an instance of :class:`pyramid.interfaces.IViewDeriverInfo`. + + The ``view`` is a callable accepting ``(context, request)``. + + """ + +class IViewDeriverInfo(Interface): + """ An object implementing this interface is passed to every + :term:`view deriver` during configuration.""" + registry = Attribute('The "current" application registry where the ' + 'view was created') + package = Attribute('The "current package" where the view ' + 'configuration statement was found') + settings = Attribute('The deployment settings dictionary related ' + 'to the current application') + options = Attribute('The view options passed to the view, including any ' + 'default values that were not overriden') + predicates = Attribute('The list of predicates active on the view') + original_view = Attribute('The original view object being wrapped') + exception_only = Attribute('The view will only be invoked for exceptions') + +class IViewDerivers(Interface): + """ Interface for view derivers list """ + +class ICacheBuster(Interface): + """ + A cache buster modifies the URL generation machinery for + :meth:`~pyramid.request.Request.static_url`. See :ref:`cache_busting`. + + .. versionadded:: 1.6 + """ + def __call__(request, subpath, kw): + """ + Modifies a subpath and/or keyword arguments from which a static asset + URL will be computed during URL generation. + + The ``subpath`` argument is a path of ``/``-delimited segments that + represent the portion of the asset URL which is used to find the asset. + The ``kw`` argument is a dict of keywords that are to be passed + eventually to :meth:`~pyramid.request.Request.static_url` for URL + generation. The return value should be a two-tuple of + ``(subpath, kw)`` where ``subpath`` is the relative URL from where the + file is served and ``kw`` is the same input argument. The return value + should be modified to include the cache bust token in the generated + URL. + + The ``kw`` dictionary contains extra arguments passed to + :meth:`~pyramid.request.Request.static_url` as well as some extra + items that may be usful including: + + - ``pathspec`` is the path specification for the resource + to be cache busted. + + - ``rawspec`` is the original location of the file, ignoring + any calls to :meth:`pyramid.config.Configurator.override_asset`. + + The ``pathspec`` and ``rawspec`` values are only different in cases + where an asset has been mounted into a virtual location using + :meth:`pyramid.config.Configurator.override_asset`. For example, with + a call to ``request.static_url('myapp:static/foo.png'), the + ``pathspec`` is ``myapp:static/foo.png`` whereas the ``rawspec`` may + be ``themepkg:bar.png``, assuming a call to + ``config.override_asset('myapp:static/foo.png', 'themepkg:bar.png')``. + """ + +# configuration phases: a lower phase number means the actions associated +# with this phase will be executed earlier than those with later phase +# numbers. The default phase number is 0, FTR. + +PHASE0_CONFIG = -30 +PHASE1_CONFIG = -20 +PHASE2_CONFIG = -10 +PHASE3_CONFIG = 0 diff --git a/src/pyramid/location.py b/src/pyramid/location.py new file mode 100644 index 000000000..4124895a5 --- /dev/null +++ b/src/pyramid/location.py @@ -0,0 +1,66 @@ +############################################################################## +# +# Copyright (c) 2003 Zope Corporation and Contributors. +# All Rights Reserved. +# +# This software is subject to the provisions of the Zope Public License, +# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. +# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED +# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS +# FOR A PARTICULAR PURPOSE. +# +############################################################################## + +def inside(resource1, resource2): + """Is ``resource1`` 'inside' ``resource2``? Return ``True`` if so, else + ``False``. + + ``resource1`` is 'inside' ``resource2`` if ``resource2`` is a + :term:`lineage` ancestor of ``resource1``. It is a lineage ancestor + if its parent (or one of its parent's parents, etc.) is an + ancestor. + """ + while resource1 is not None: + if resource1 is resource2: + return True + resource1 = resource1.__parent__ + + return False + +def lineage(resource): + """ + Return a generator representing the :term:`lineage` of the + :term:`resource` object implied by the ``resource`` argument. The + generator first returns ``resource`` unconditionally. Then, if + ``resource`` supplies a ``__parent__`` attribute, return the resource + represented by ``resource.__parent__``. If *that* resource has a + ``__parent__`` attribute, return that resource's parent, and so on, + until the resource being inspected either has no ``__parent__`` + attribute or which has a ``__parent__`` attribute of ``None``. + For example, if the resource tree is:: + + thing1 = Thing() + thing2 = Thing() + thing2.__parent__ = thing1 + + Calling ``lineage(thing2)`` will return a generator. When we turn + it into a list, we will get:: + + list(lineage(thing2)) + [ , ] + """ + while resource is not None: + yield resource + # The common case is that the AttributeError exception below + # is exceptional as long as the developer is a "good citizen" + # who has a root object with a __parent__ of None. Using an + # exception here instead of a getattr with a default is an + # important micro-optimization, because this function is + # called in any non-trivial application over and over again to + # generate URLs and paths. + try: + resource = resource.__parent__ + except AttributeError: + resource = None + diff --git a/src/pyramid/paster.py b/src/pyramid/paster.py new file mode 100644 index 000000000..f7544f0c5 --- /dev/null +++ b/src/pyramid/paster.py @@ -0,0 +1,111 @@ +from pyramid.scripting import prepare +from pyramid.scripts.common import get_config_loader + +def setup_logging(config_uri, global_conf=None): + """ + Set up Python logging with the filename specified via ``config_uri`` + (a string in the form ``filename#sectionname``). + + Extra defaults can optionally be specified as a dict in ``global_conf``. + """ + loader = get_config_loader(config_uri) + loader.setup_logging(global_conf) + +def get_app(config_uri, name=None, options=None): + """ Return the WSGI application named ``name`` in the PasteDeploy + config file specified by ``config_uri``. + + ``options``, if passed, should be a dictionary used as variable assignments + like ``{'http_port': 8080}``. This is useful if e.g. ``%(http_port)s`` is + used in the config file. + + If the ``name`` is None, this will attempt to parse the name from + the ``config_uri`` string expecting the format ``inifile#name``. + If no name is found, the name will default to "main". + + """ + loader = get_config_loader(config_uri) + return loader.get_wsgi_app(name, options) + +def get_appsettings(config_uri, name=None, options=None): + """ Return a dictionary representing the key/value pairs in an ``app`` + section within the file represented by ``config_uri``. + + ``options``, if passed, should be a dictionary used as variable assignments + like ``{'http_port': 8080}``. This is useful if e.g. ``%(http_port)s`` is + used in the config file. + + If the ``name`` is None, this will attempt to parse the name from + the ``config_uri`` string expecting the format ``inifile#name``. + If no name is found, the name will default to "main". + + """ + loader = get_config_loader(config_uri) + return loader.get_wsgi_app_settings(name, options) + +def bootstrap(config_uri, request=None, options=None): + """ Load a WSGI application from the PasteDeploy config file specified + by ``config_uri``. The environment will be configured as if it is + currently serving ``request``, leaving a natural environment in place + to write scripts that can generate URLs and utilize renderers. + + This function returns a dictionary with ``app``, ``root``, ``closer``, + ``request``, and ``registry`` keys. ``app`` is the WSGI app loaded + (based on the ``config_uri``), ``root`` is the traversal root resource + of the Pyramid application, and ``closer`` is a parameterless callback + that may be called when your script is complete (it pops a threadlocal + stack). + + .. note:: + + Most operations within :app:`Pyramid` expect to be invoked within the + context of a WSGI request, thus it's important when loading your + application to anchor it when executing scripts and other code that is + not normally invoked during active WSGI requests. + + .. note:: + + For a complex config file containing multiple :app:`Pyramid` + applications, this function will setup the environment under the context + of the last-loaded :app:`Pyramid` application. You may load a specific + application yourself by using the lower-level functions + :meth:`pyramid.paster.get_app` and :meth:`pyramid.scripting.prepare` in + conjunction with :attr:`pyramid.config.global_registries`. + + ``config_uri`` -- specifies the PasteDeploy config file to use for the + interactive shell. The format is ``inifile#name``. If the name is left + off, ``main`` will be assumed. + + ``request`` -- specified to anchor the script to a given set of WSGI + parameters. For example, most people would want to specify the host, + scheme and port such that their script will generate URLs in relation + to those parameters. A request with default parameters is constructed + for you if none is provided. You can mutate the request's ``environ`` + later to setup a specific host/port/scheme/etc. + + ``options`` Is passed to get_app for use as variable assignments like + {'http_port': 8080} and then use %(http_port)s in the + config file. + + This function may be used as a context manager to call the ``closer`` + automatically: + + .. code-block:: python + + with bootstrap('development.ini') as env: + request = env['request'] + # ... + + See :ref:`writing_a_script` for more information about how to use this + function. + + .. versionchanged:: 1.8 + + Added the ability to use the return value as a context manager. + + """ + app = get_app(config_uri, options=options) + env = prepare(request) + env['app'] = app + return env + diff --git a/src/pyramid/path.py b/src/pyramid/path.py new file mode 100644 index 000000000..3fac7e940 --- /dev/null +++ b/src/pyramid/path.py @@ -0,0 +1,436 @@ +import os +import pkg_resources +import sys +import imp + +from zope.interface import implementer + +from pyramid.interfaces import IAssetDescriptor + +from pyramid.compat import string_types + +ignore_types = [ imp.C_EXTENSION, imp.C_BUILTIN ] +init_names = [ '__init__%s' % x[0] for x in imp.get_suffixes() if + x[0] and x[2] not in ignore_types ] + +def caller_path(path, level=2): + if not os.path.isabs(path): + module = caller_module(level + 1) + prefix = package_path(module) + path = os.path.join(prefix, path) + return path + +def caller_module(level=2, sys=sys): + module_globals = sys._getframe(level).f_globals + module_name = module_globals.get('__name__') or '__main__' + module = sys.modules[module_name] + return module + +def package_name(pkg_or_module): + """ If this function is passed a module, return the dotted Python + package name of the package in which the module lives. If this + function is passed a package, return the dotted Python package + name of the package itself.""" + if pkg_or_module is None or pkg_or_module.__name__ == '__main__': + return '__main__' + pkg_name = pkg_or_module.__name__ + pkg_filename = getattr(pkg_or_module, '__file__', None) + if pkg_filename is None: + # Namespace packages do not have __init__.py* files, + # and so have no __file__ attribute + return pkg_name + splitted = os.path.split(pkg_filename) + if splitted[-1] in init_names: + # it's a package + return pkg_name + return pkg_name.rsplit('.', 1)[0] + +def package_of(pkg_or_module): + """ Return the package of a module or return the package itself """ + pkg_name = package_name(pkg_or_module) + __import__(pkg_name) + return sys.modules[pkg_name] + +def caller_package(level=2, caller_module=caller_module): + # caller_module in arglist for tests + module = caller_module(level + 1) + f = getattr(module, '__file__', '') + if (('__init__.py' in f) or ('__init__$py' in f)): # empty at >>> + # Module is a package + return module + # Go up one level to get package + package_name = module.__name__.rsplit('.', 1)[0] + return sys.modules[package_name] + +def package_path(package): + # computing the abspath is actually kinda expensive so we memoize + # the result + prefix = getattr(package, '__abspath__', None) + if prefix is None: + prefix = pkg_resources.resource_filename(package.__name__, '') + # pkg_resources doesn't care whether we feed it a package + # name or a module name within the package, the result + # will be the same: a directory name to the package itself + try: + package.__abspath__ = prefix + except Exception: + # this is only an optimization, ignore any error + pass + return prefix + +class _CALLER_PACKAGE(object): + def __repr__(self): # pragma: no cover (for docs) + return 'pyramid.path.CALLER_PACKAGE' + +CALLER_PACKAGE = _CALLER_PACKAGE() + +class Resolver(object): + def __init__(self, package=CALLER_PACKAGE): + if package in (None, CALLER_PACKAGE): + self.package = package + else: + if isinstance(package, string_types): + try: + __import__(package) + except ImportError: + raise ValueError( + 'The dotted name %r cannot be imported' % (package,) + ) + package = sys.modules[package] + self.package = package_of(package) + + def get_package_name(self): + if self.package is CALLER_PACKAGE: + package_name = caller_package().__name__ + else: + package_name = self.package.__name__ + return package_name + + def get_package(self): + if self.package is CALLER_PACKAGE: + package = caller_package() + else: + package = self.package + return package + + +class AssetResolver(Resolver): + """ A class used to resolve an :term:`asset specification` to an + :term:`asset descriptor`. + + .. versionadded:: 1.3 + + The constructor accepts a single argument named ``package`` which may be + any of: + + - A fully qualified (not relative) dotted name to a module or package + + - a Python module or package object + + - The value ``None`` + + - The constant value :attr:`pyramid.path.CALLER_PACKAGE`. + + The default value is :attr:`pyramid.path.CALLER_PACKAGE`. + + The ``package`` is used when a relative asset specification is supplied + to the :meth:`~pyramid.path.AssetResolver.resolve` method. An asset + specification without a colon in it is treated as relative. + + If ``package`` is ``None``, the resolver will + only be able to resolve fully qualified (not relative) asset + specifications. Any attempt to resolve a relative asset specification + will result in an :exc:`ValueError` exception. + + If ``package`` is :attr:`pyramid.path.CALLER_PACKAGE`, + the resolver will treat relative asset specifications as + relative to the caller of the :meth:`~pyramid.path.AssetResolver.resolve` + method. + + If ``package`` is a *module* or *module name* (as opposed to a package or + package name), its containing package is computed and this + package is used to derive the package name (all names are resolved relative + to packages, never to modules). For example, if the ``package`` argument + to this type was passed the string ``xml.dom.expatbuilder``, and + ``template.pt`` is supplied to the + :meth:`~pyramid.path.AssetResolver.resolve` method, the resulting absolute + asset spec would be ``xml.minidom:template.pt``, because + ``xml.dom.expatbuilder`` is a module object, not a package object. + + If ``package`` is a *package* or *package name* (as opposed to a module or + module name), this package will be used to compute relative + asset specifications. For example, if the ``package`` argument to this + type was passed the string ``xml.dom``, and ``template.pt`` is supplied + to the :meth:`~pyramid.path.AssetResolver.resolve` method, the resulting + absolute asset spec would be ``xml.minidom:template.pt``. + """ + def resolve(self, spec): + """ + Resolve the asset spec named as ``spec`` to an object that has the + attributes and methods described in + :class:`pyramid.interfaces.IAssetDescriptor`. + + If ``spec`` is an absolute filename + (e.g. ``/path/to/myproject/templates/foo.pt``) or an absolute asset + spec (e.g. ``myproject:templates.foo.pt``), an asset descriptor is + returned without taking into account the ``package`` passed to this + class' constructor. + + If ``spec`` is a *relative* asset specification (an asset + specification without a ``:`` in it, e.g. ``templates/foo.pt``), the + ``package`` argument of the constructor is used as the package + portion of the asset spec. For example: + + .. code-block:: python + + a = AssetResolver('myproject') + resolver = a.resolve('templates/foo.pt') + print(resolver.abspath()) + # -> /path/to/myproject/templates/foo.pt + + If the AssetResolver is constructed without a ``package`` argument of + ``None``, and a relative asset specification is passed to + ``resolve``, an :exc:`ValueError` exception is raised. + """ + if os.path.isabs(spec): + return FSAssetDescriptor(spec) + path = spec + if ':' in path: + package_name, path = spec.split(':', 1) + else: + if self.package is CALLER_PACKAGE: + package_name = caller_package().__name__ + else: + package_name = getattr(self.package, '__name__', None) + if package_name is None: + raise ValueError( + 'relative spec %r irresolveable without package' % (spec,) + ) + return PkgResourcesAssetDescriptor(package_name, path) + +class DottedNameResolver(Resolver): + """ A class used to resolve a :term:`dotted Python name` to a package or + module object. + + .. versionadded:: 1.3 + + The constructor accepts a single argument named ``package`` which may be + any of: + + - A fully qualified (not relative) dotted name to a module or package + + - a Python module or package object + + - The value ``None`` + + - The constant value :attr:`pyramid.path.CALLER_PACKAGE`. + + The default value is :attr:`pyramid.path.CALLER_PACKAGE`. + + The ``package`` is used when a relative dotted name is supplied to the + :meth:`~pyramid.path.DottedNameResolver.resolve` method. A dotted name + which has a ``.`` (dot) or ``:`` (colon) as its first character is + treated as relative. + + If ``package`` is ``None``, the resolver will only be able to resolve + fully qualified (not relative) names. Any attempt to resolve a + relative name will result in an :exc:`ValueError` exception. + + If ``package`` is :attr:`pyramid.path.CALLER_PACKAGE`, + the resolver will treat relative dotted names as relative to + the caller of the :meth:`~pyramid.path.DottedNameResolver.resolve` + method. + + If ``package`` is a *module* or *module name* (as opposed to a package or + package name), its containing package is computed and this + package used to derive the package name (all names are resolved relative + to packages, never to modules). For example, if the ``package`` argument + to this type was passed the string ``xml.dom.expatbuilder``, and + ``.mindom`` is supplied to the + :meth:`~pyramid.path.DottedNameResolver.resolve` method, the resulting + import would be for ``xml.minidom``, because ``xml.dom.expatbuilder`` is + a module object, not a package object. + + If ``package`` is a *package* or *package name* (as opposed to a module or + module name), this package will be used to relative compute + dotted names. For example, if the ``package`` argument to this type was + passed the string ``xml.dom``, and ``.minidom`` is supplied to the + :meth:`~pyramid.path.DottedNameResolver.resolve` method, the resulting + import would be for ``xml.minidom``. + """ + def resolve(self, dotted): + """ + This method resolves a dotted name reference to a global Python + object (an object which can be imported) to the object itself. + + Two dotted name styles are supported: + + - ``pkg_resources``-style dotted names where non-module attributes + of a package are separated from the rest of the path using a ``:`` + e.g. ``package.module:attr``. + + - ``zope.dottedname``-style dotted names where non-module + attributes of a package are separated from the rest of the path + using a ``.`` e.g. ``package.module.attr``. + + These styles can be used interchangeably. If the supplied name + contains a ``:`` (colon), the ``pkg_resources`` resolution + mechanism will be chosen, otherwise the ``zope.dottedname`` + resolution mechanism will be chosen. + + If the ``dotted`` argument passed to this method is not a string, a + :exc:`ValueError` will be raised. + + When a dotted name cannot be resolved, a :exc:`ValueError` error is + raised. + + Example: + + .. code-block:: python + + r = DottedNameResolver() + v = r.resolve('xml') # v is the xml module + + """ + if not isinstance(dotted, string_types): + raise ValueError('%r is not a string' % (dotted,)) + package = self.package + if package is CALLER_PACKAGE: + package = caller_package() + return self._resolve(dotted, package) + + def maybe_resolve(self, dotted): + """ + This method behaves just like + :meth:`~pyramid.path.DottedNameResolver.resolve`, except if the + ``dotted`` value passed is not a string, it is simply returned. For + example: + + .. code-block:: python + + import xml + r = DottedNameResolver() + v = r.maybe_resolve(xml) + # v is the xml module; no exception raised + """ + if isinstance(dotted, string_types): + package = self.package + if package is CALLER_PACKAGE: + package = caller_package() + return self._resolve(dotted, package) + return dotted + + def _resolve(self, dotted, package): + if ':' in dotted: + return self._pkg_resources_style(dotted, package) + else: + return self._zope_dottedname_style(dotted, package) + + def _pkg_resources_style(self, value, package): + """ package.module:attr style """ + if value.startswith(('.', ':')): + if not package: + raise ValueError( + 'relative name %r irresolveable without package' % (value,) + ) + if value in ['.', ':']: + value = package.__name__ + else: + value = package.__name__ + value + # Calling EntryPoint.load with an argument is deprecated. + # See https://pythonhosted.org/setuptools/history.html#id8 + ep = pkg_resources.EntryPoint.parse('x=%s' % value) + if hasattr(ep, 'resolve'): + # setuptools>=10.2 + return ep.resolve() # pragma: NO COVER + else: + return ep.load(False) # pragma: NO COVER + + def _zope_dottedname_style(self, value, package): + """ package.module.attr style """ + module = getattr(package, '__name__', None) # package may be None + if not module: + module = None + if value == '.': + if module is None: + raise ValueError( + 'relative name %r irresolveable without package' % (value,) + ) + name = module.split('.') + else: + name = value.split('.') + if not name[0]: + if module is None: + raise ValueError( + 'relative name %r irresolveable without ' + 'package' % (value,) + ) + module = module.split('.') + name.pop(0) + while not name[0]: + module.pop() + name.pop(0) + name = module + name + + used = name.pop(0) + found = __import__(used) + for n in name: + used += '.' + n + try: + found = getattr(found, n) + except AttributeError: + __import__(used) + found = getattr(found, n) # pragma: no cover + + return found + +@implementer(IAssetDescriptor) +class PkgResourcesAssetDescriptor(object): + pkg_resources = pkg_resources + + def __init__(self, pkg_name, path): + self.pkg_name = pkg_name + self.path = path + + def absspec(self): + return '%s:%s' % (self.pkg_name, self.path) + + def abspath(self): + return os.path.abspath( + self.pkg_resources.resource_filename(self.pkg_name, self.path)) + + def stream(self): + return self.pkg_resources.resource_stream(self.pkg_name, self.path) + + def isdir(self): + return self.pkg_resources.resource_isdir(self.pkg_name, self.path) + + def listdir(self): + return self.pkg_resources.resource_listdir(self.pkg_name, self.path) + + def exists(self): + return self.pkg_resources.resource_exists(self.pkg_name, self.path) + +@implementer(IAssetDescriptor) +class FSAssetDescriptor(object): + + def __init__(self, path): + self.path = os.path.abspath(path) + + def absspec(self): + raise NotImplementedError + + def abspath(self): + return self.path + + def stream(self): + return open(self.path, 'rb') + + def isdir(self): + return os.path.isdir(self.path) + + def listdir(self): + return os.listdir(self.path) + + def exists(self): + return os.path.exists(self.path) diff --git a/src/pyramid/predicates.py b/src/pyramid/predicates.py new file mode 100644 index 000000000..97edae8a0 --- /dev/null +++ b/src/pyramid/predicates.py @@ -0,0 +1,336 @@ +import re + +from pyramid.exceptions import ConfigurationError + +from pyramid.compat import is_nonstr_iter + +from pyramid.csrf import check_csrf_token +from pyramid.traversal import ( + find_interface, + traversal_path, + resource_path_tuple + ) + +from pyramid.urldispatch import _compile_route +from pyramid.util import ( + as_sorted_tuple, + object_description, +) + +_marker = object() + +class XHRPredicate(object): + def __init__(self, val, config): + self.val = bool(val) + + def text(self): + return 'xhr = %s' % self.val + + phash = text + + def __call__(self, context, request): + return bool(request.is_xhr) is self.val + +class RequestMethodPredicate(object): + def __init__(self, val, config): + request_method = as_sorted_tuple(val) + if 'GET' in request_method and 'HEAD' not in request_method: + # GET implies HEAD too + request_method = as_sorted_tuple(request_method + ('HEAD',)) + self.val = request_method + + def text(self): + return 'request_method = %s' % (','.join(self.val)) + + phash = text + + def __call__(self, context, request): + return request.method in self.val + +class PathInfoPredicate(object): + def __init__(self, val, config): + self.orig = val + try: + val = re.compile(val) + except re.error as why: + raise ConfigurationError(why.args[0]) + self.val = val + + def text(self): + return 'path_info = %s' % (self.orig,) + + phash = text + + def __call__(self, context, request): + return self.val.match(request.upath_info) is not None + +class RequestParamPredicate(object): + def __init__(self, val, config): + val = as_sorted_tuple(val) + reqs = [] + for p in val: + k = p + v = None + if p.startswith('='): + if '=' in p[1:]: + k, v = p[1:].split('=', 1) + k = '=' + k + k, v = k.strip(), v.strip() + elif '=' in p: + k, v = p.split('=', 1) + k, v = k.strip(), v.strip() + reqs.append((k, v)) + self.val = val + self.reqs = reqs + + def text(self): + return 'request_param %s' % ','.join( + ['%s=%s' % (x,y) if y else x for x, y in self.reqs] + ) + + phash = text + + def __call__(self, context, request): + for k, v in self.reqs: + actual = request.params.get(k) + if actual is None: + return False + if v is not None and actual != v: + return False + return True + +class HeaderPredicate(object): + def __init__(self, val, config): + name = val + v = None + if ':' in name: + name, val_str = name.split(':', 1) + try: + v = re.compile(val_str) + except re.error as why: + raise ConfigurationError(why.args[0]) + if v is None: + self._text = 'header %s' % (name,) + else: + self._text = 'header %s=%s' % (name, val_str) + self.name = name + self.val = v + + def text(self): + return self._text + + phash = text + + def __call__(self, context, request): + if self.val is None: + return self.name in request.headers + val = request.headers.get(self.name) + if val is None: + return False + return self.val.match(val) is not None + +class AcceptPredicate(object): + _is_using_deprecated_ranges = False + + def __init__(self, values, config): + if not is_nonstr_iter(values): + values = (values,) + # deprecated media ranges were only supported in versions of the + # predicate that didn't support lists, so check it here + if len(values) == 1 and '*' in values[0]: + self._is_using_deprecated_ranges = True + self.values = values + + def text(self): + return 'accept = %s' % (', '.join(self.values),) + + phash = text + + def __call__(self, context, request): + if self._is_using_deprecated_ranges: + return self.values[0] in request.accept + return bool(request.accept.acceptable_offers(self.values)) + +class ContainmentPredicate(object): + def __init__(self, val, config): + self.val = config.maybe_dotted(val) + + def text(self): + return 'containment = %s' % (self.val,) + + phash = text + + def __call__(self, context, request): + ctx = getattr(request, 'context', context) + return find_interface(ctx, self.val) is not None + +class RequestTypePredicate(object): + def __init__(self, val, config): + self.val = val + + def text(self): + return 'request_type = %s' % (self.val,) + + phash = text + + def __call__(self, context, request): + return self.val.providedBy(request) + +class MatchParamPredicate(object): + def __init__(self, val, config): + val = as_sorted_tuple(val) + self.val = val + reqs = [ p.split('=', 1) for p in val ] + self.reqs = [ (x.strip(), y.strip()) for x, y in reqs ] + + def text(self): + return 'match_param %s' % ','.join( + ['%s=%s' % (x,y) for x, y in self.reqs] + ) + + phash = text + + def __call__(self, context, request): + if not request.matchdict: + # might be None + return False + for k, v in self.reqs: + if request.matchdict.get(k) != v: + return False + return True + +class CustomPredicate(object): + def __init__(self, func, config): + self.func = func + + def text(self): + return getattr( + self.func, + '__text__', + 'custom predicate: %s' % object_description(self.func) + ) + + def phash(self): + # using hash() here rather than id() is intentional: we + # want to allow custom predicates that are part of + # frameworks to be able to define custom __hash__ + # functions for custom predicates, so that the hash output + # of predicate instances which are "logically the same" + # may compare equal. + return 'custom:%r' % hash(self.func) + + def __call__(self, context, request): + return self.func(context, request) + + +class TraversePredicate(object): + # Can only be used as a *route* "predicate"; it adds 'traverse' to the + # matchdict if it's specified in the routing args. This causes the + # ResourceTreeTraverser to use the resolved traverse pattern as the + # traversal path. + def __init__(self, val, config): + _, self.tgenerate = _compile_route(val) + self.val = val + + def text(self): + return 'traverse matchdict pseudo-predicate' + + def phash(self): + # This isn't actually a predicate, it's just a infodict modifier that + # injects ``traverse`` into the matchdict. As a result, we don't + # need to update the hash. + return '' + + def __call__(self, context, request): + if 'traverse' in context: + return True + m = context['match'] + tvalue = self.tgenerate(m) # tvalue will be urlquoted string + m['traverse'] = traversal_path(tvalue) + # This isn't actually a predicate, it's just a infodict modifier that + # injects ``traverse`` into the matchdict. As a result, we just + # return True. + return True + +class CheckCSRFTokenPredicate(object): + + check_csrf_token = staticmethod(check_csrf_token) # testing + + def __init__(self, val, config): + self.val = val + + def text(self): + return 'check_csrf = %s' % (self.val,) + + phash = text + + def __call__(self, context, request): + val = self.val + if val: + if val is True: + val = 'csrf_token' + return self.check_csrf_token(request, val, raises=False) + return True + +class PhysicalPathPredicate(object): + def __init__(self, val, config): + if is_nonstr_iter(val): + self.val = tuple(val) + else: + val = tuple(filter(None, val.split('/'))) + self.val = ('',) + val + + def text(self): + return 'physical_path = %s' % (self.val,) + + phash = text + + def __call__(self, context, request): + if getattr(context, '__name__', _marker) is not _marker: + return resource_path_tuple(context) == self.val + return False + +class EffectivePrincipalsPredicate(object): + def __init__(self, val, config): + if is_nonstr_iter(val): + self.val = set(val) + else: + self.val = set((val,)) + + def text(self): + return 'effective_principals = %s' % sorted(list(self.val)) + + phash = text + + def __call__(self, context, request): + req_principals = request.effective_principals + if is_nonstr_iter(req_principals): + rpset = set(req_principals) + if self.val.issubset(rpset): + return True + return False + +class Notted(object): + def __init__(self, predicate): + self.predicate = predicate + + def _notted_text(self, val): + # if the underlying predicate doesnt return a value, it's not really + # a predicate, it's just something pretending to be a predicate, + # so dont update the hash + if val: + val = '!' + val + return val + + def text(self): + return self._notted_text(self.predicate.text()) + + def phash(self): + return self._notted_text(self.predicate.phash()) + + def __call__(self, context, request): + result = self.predicate(context, request) + phash = self.phash() + if phash: + result = not result + return result diff --git a/src/pyramid/registry.py b/src/pyramid/registry.py new file mode 100644 index 000000000..a741c495e --- /dev/null +++ b/src/pyramid/registry.py @@ -0,0 +1,297 @@ +import operator +import threading + +from zope.interface import implementer +from zope.interface.registry import Components + +from pyramid.compat import text_ +from pyramid.decorator import reify + +from pyramid.interfaces import ( + IIntrospector, + IIntrospectable, + ISettings, +) + +from pyramid.path import ( + CALLER_PACKAGE, + caller_package, +) + +empty = text_('') + +class Registry(Components, dict): + """ A registry object is an :term:`application registry`. + + It is used by the framework itself to perform mappings of URLs to view + callables, as well as servicing other various framework duties. A registry + has its own internal API, but this API is rarely used by Pyramid + application developers (it's usually only used by developers of the + Pyramid framework and Pyramid addons). But it has a number of attributes + that may be useful to application developers within application code, + such as ``settings``, which is a dictionary containing application + deployment settings. + + For information about the purpose and usage of the application registry, + see :ref:`zca_chapter`. + + The registry may be used both as an :class:`pyramid.interfaces.IDict` and + as a Zope component registry. + These two ways of storing configuration are independent. + Applications will tend to prefer to store information as key-values + whereas addons may prefer to use the component registry to avoid naming + conflicts and to provide more complex lookup mechanisms. + + The application registry is usually accessed as ``request.registry`` in + application code. By the time a registry is used to handle requests it + should be considered frozen and read-only. Any changes to its internal + state should be done with caution and concern for thread-safety. + + """ + + # for optimization purposes, if no listeners are listening, don't try + # to notify them + has_listeners = False + + _settings = None + + def __init__(self, package_name=CALLER_PACKAGE, *args, **kw): + # add a registry-instance-specific lock, which is used when the lookup + # cache is mutated + self._lock = threading.Lock() + # add a view lookup cache + self._clear_view_lookup_cache() + if package_name is CALLER_PACKAGE: + package_name = caller_package().__name__ + Components.__init__(self, package_name, *args, **kw) + dict.__init__(self) + + def _clear_view_lookup_cache(self): + self._view_lookup_cache = {} + + def __nonzero__(self): + # defeat bool determination via dict.__len__ + return True + + @reify + def package_name(self): + return self.__name__ + + def registerSubscriptionAdapter(self, *arg, **kw): + result = Components.registerSubscriptionAdapter(self, *arg, **kw) + self.has_listeners = True + return result + + def registerSelfAdapter(self, required=None, provided=None, name=empty, + info=empty, event=True): + # registerAdapter analogue which always returns the object itself + # when required is matched + return self.registerAdapter(lambda x: x, required=required, + provided=provided, name=name, + info=info, event=event) + + def queryAdapterOrSelf(self, object, interface, default=None): + # queryAdapter analogue which returns the object if it implements + # the interface, otherwise it will return an adaptation to the + # interface + if not interface.providedBy(object): + return self.queryAdapter(object, interface, default=default) + return object + + def registerHandler(self, *arg, **kw): + result = Components.registerHandler(self, *arg, **kw) + self.has_listeners = True + return result + + def notify(self, *events): + if self.has_listeners: + # iterating over subscribers assures they get executed + [ _ for _ in self.subscribers(events, None) ] + + # backwards compatibility for code that wants to look up a settings + # object via ``registry.getUtility(ISettings)`` + def _get_settings(self): + return self._settings + + def _set_settings(self, settings): + self.registerUtility(settings, ISettings) + self._settings = settings + + settings = property(_get_settings, _set_settings) + +@implementer(IIntrospector) +class Introspector(object): + def __init__(self): + self._refs = {} + self._categories = {} + self._counter = 0 + + def add(self, intr): + category = self._categories.setdefault(intr.category_name, {}) + category[intr.discriminator] = intr + category[intr.discriminator_hash] = intr + intr.order = self._counter + self._counter += 1 + + def get(self, category_name, discriminator, default=None): + category = self._categories.setdefault(category_name, {}) + intr = category.get(discriminator, default) + return intr + + def get_category(self, category_name, default=None, sort_key=None): + if sort_key is None: + sort_key = operator.attrgetter('order') + category = self._categories.get(category_name) + if category is None: + return default + values = category.values() + values = sorted(set(values), key=sort_key) + return [ + {'introspectable': intr, + 'related': self.related(intr)} + for intr in values + ] + + def categorized(self, sort_key=None): + L = [] + for category_name in self.categories(): + L.append((category_name, self.get_category(category_name, + sort_key=sort_key))) + return L + + def categories(self): + return sorted(self._categories.keys()) + + def remove(self, category_name, discriminator): + intr = self.get(category_name, discriminator) + if intr is None: + return + L = self._refs.pop(intr, []) + for d in L: + L2 = self._refs[d] + L2.remove(intr) + category = self._categories[intr.category_name] + del category[intr.discriminator] + del category[intr.discriminator_hash] + + def _get_intrs_by_pairs(self, pairs): + introspectables = [] + for pair in pairs: + category_name, discriminator = pair + intr = self._categories.get(category_name, {}).get(discriminator) + if intr is None: + raise KeyError((category_name, discriminator)) + introspectables.append(intr) + return introspectables + + def relate(self, *pairs): + introspectables = self._get_intrs_by_pairs(pairs) + relatable = ((x,y) for x in introspectables for y in introspectables) + for x, y in relatable: + L = self._refs.setdefault(x, []) + if x is not y and y not in L: + L.append(y) + + def unrelate(self, *pairs): + introspectables = self._get_intrs_by_pairs(pairs) + relatable = ((x,y) for x in introspectables for y in introspectables) + for x, y in relatable: + L = self._refs.get(x, []) + if y in L: + L.remove(y) + + def related(self, intr): + category_name, discriminator = intr.category_name, intr.discriminator + intr = self._categories.get(category_name, {}).get(discriminator) + if intr is None: + raise KeyError((category_name, discriminator)) + return self._refs.get(intr, []) + +@implementer(IIntrospectable) +class Introspectable(dict): + + order = 0 # mutated by introspector.add + action_info = None # mutated by self.register + + def __init__(self, category_name, discriminator, title, type_name): + self.category_name = category_name + self.discriminator = discriminator + self.title = title + self.type_name = type_name + self._relations = [] + + def relate(self, category_name, discriminator): + self._relations.append((True, category_name, discriminator)) + + def unrelate(self, category_name, discriminator): + self._relations.append((False, category_name, discriminator)) + + def _assert_resolved(self): + assert undefer(self.discriminator) is self.discriminator + + @property + def discriminator_hash(self): + self._assert_resolved() + return hash(self.discriminator) + + def __hash__(self): + self._assert_resolved() + return hash((self.category_name,) + (self.discriminator,)) + + def __repr__(self): + self._assert_resolved() + return '<%s category %r, discriminator %r>' % (self.__class__.__name__, + self.category_name, + self.discriminator) + + def __nonzero__(self): + return True + + __bool__ = __nonzero__ # py3 + + def register(self, introspector, action_info): + self.discriminator = undefer(self.discriminator) + self.action_info = action_info + introspector.add(self) + for relate, category_name, discriminator in self._relations: + discriminator = undefer(discriminator) + if relate: + method = introspector.relate + else: + method = introspector.unrelate + method( + (self.category_name, self.discriminator), + (category_name, discriminator) + ) + +class Deferred(object): + """ Can be used by a third-party configuration extender to wrap a + :term:`discriminator` during configuration if an immediately hashable + discriminator cannot be computed because it relies on unresolved values. + The function should accept no arguments and should return a hashable + discriminator.""" + def __init__(self, func): + self.func = func + + @reify + def value(self): + result = self.func() + del self.func + return result + + def resolve(self): + return self.value + +def undefer(v): + """ Function which accepts an object and returns it unless it is a + :class:`pyramid.registry.Deferred` instance. If it is an instance of + that class, its ``resolve`` method is called, and the result of the + method is returned.""" + if isinstance(v, Deferred): + v = v.resolve() + return v + +class predvalseq(tuple): + """ A subtype of tuple used to represent a sequence of predicate values """ + +global_registry = Registry('global') diff --git a/src/pyramid/renderers.py b/src/pyramid/renderers.py new file mode 100644 index 000000000..d1c85b371 --- /dev/null +++ b/src/pyramid/renderers.py @@ -0,0 +1,529 @@ +from functools import partial +import json +import os +import re + +from zope.interface import ( + implementer, + providedBy, + ) +from zope.interface.registry import Components + +from pyramid.interfaces import ( + IJSONAdapter, + IRendererFactory, + IRendererInfo, + ) + +from pyramid.compat import ( + string_types, + text_type, + ) + +from pyramid.csrf import get_csrf_token +from pyramid.decorator import reify + +from pyramid.events import BeforeRender + +from pyramid.httpexceptions import HTTPBadRequest + +from pyramid.path import caller_package + +from pyramid.response import _get_response_factory +from pyramid.threadlocal import get_current_registry +from pyramid.util import hide_attrs + +# API + +def render(renderer_name, value, request=None, package=None): + """ Using the renderer ``renderer_name`` (a template + or a static renderer), render the value (or set of values) present + in ``value``. Return the result of the renderer's ``__call__`` + method (usually a string or Unicode). + + If the ``renderer_name`` refers to a file on disk, such as when the + renderer is a template, it's usually best to supply the name as an + :term:`asset specification` + (e.g. ``packagename:path/to/template.pt``). + + You may supply a relative asset spec as ``renderer_name``. If + the ``package`` argument is supplied, a relative renderer path + will be converted to an absolute asset specification by + combining the package ``package`` with the relative + asset specification ``renderer_name``. If ``package`` + is ``None`` (the default), the package name of the *caller* of + this function will be used as the package. + + The ``value`` provided will be supplied as the input to the + renderer. Usually, for template renderings, this should be a + dictionary. For other renderers, this will need to be whatever + sort of value the renderer expects. + + The 'system' values supplied to the renderer will include a basic set of + top-level system names, such as ``request``, ``context``, + ``renderer_name``, and ``view``. See :ref:`renderer_system_values` for + the full list. If :term:`renderer globals` have been specified, these + will also be used to augment the value. + + Supply a ``request`` parameter in order to provide the renderer + with the most correct 'system' values (``request`` and ``context`` + in particular). + + """ + try: + registry = request.registry + except AttributeError: + registry = None + if package is None: + package = caller_package() + helper = RendererHelper(name=renderer_name, package=package, + registry=registry) + + with hide_attrs(request, 'response'): + result = helper.render(value, None, request=request) + + return result + +def render_to_response(renderer_name, + value, + request=None, + package=None, + response=None): + """ Using the renderer ``renderer_name`` (a template + or a static renderer), render the value (or set of values) using + the result of the renderer's ``__call__`` method (usually a string + or Unicode) as the response body. + + If the renderer name refers to a file on disk (such as when the + renderer is a template), it's usually best to supply the name as a + :term:`asset specification`. + + You may supply a relative asset spec as ``renderer_name``. If + the ``package`` argument is supplied, a relative renderer name + will be converted to an absolute asset specification by + combining the package ``package`` with the relative + asset specification ``renderer_name``. If you do + not supply a ``package`` (or ``package`` is ``None``) the package + name of the *caller* of this function will be used as the package. + + The ``value`` provided will be supplied as the input to the + renderer. Usually, for template renderings, this should be a + dictionary. For other renderers, this will need to be whatever + sort of value the renderer expects. + + The 'system' values supplied to the renderer will include a basic set of + top-level system names, such as ``request``, ``context``, + ``renderer_name``, and ``view``. See :ref:`renderer_system_values` for + the full list. If :term:`renderer globals` have been specified, these + will also be used to argument the value. + + Supply a ``request`` parameter in order to provide the renderer + with the most correct 'system' values (``request`` and ``context`` + in particular). Keep in mind that any changes made to ``request.response`` + prior to calling this function will not be reflected in the resulting + response object. A new response object will be created for each call + unless one is passed as the ``response`` argument. + + .. versionchanged:: 1.6 + In previous versions, any changes made to ``request.response`` outside + of this function call would affect the returned response. This is no + longer the case. If you wish to send in a pre-initialized response + then you may pass one in the ``response`` argument. + + """ + try: + registry = request.registry + except AttributeError: + registry = None + if package is None: + package = caller_package() + helper = RendererHelper(name=renderer_name, package=package, + registry=registry) + + with hide_attrs(request, 'response'): + if response is not None: + request.response = response + result = helper.render_to_response(value, None, request=request) + + return result + +def get_renderer(renderer_name, package=None, registry=None): + """ Return the renderer object for the renderer ``renderer_name``. + + You may supply a relative asset spec as ``renderer_name``. If + the ``package`` argument is supplied, a relative renderer name + will be converted to an absolute asset specification by + combining the package ``package`` with the relative + asset specification ``renderer_name``. If ``package`` is ``None`` + (the default), the package name of the *caller* of this function + will be used as the package. + + You may directly supply an :term:`application registry` using the + ``registry`` argument, and it will be used to look up the renderer. + Otherwise, the current thread-local registry (obtained via + :func:`~pyramid.threadlocal.get_current_registry`) will be used. + """ + if package is None: + package = caller_package() + helper = RendererHelper(name=renderer_name, package=package, + registry=registry) + return helper.renderer + +# concrete renderer factory implementations (also API) + +def string_renderer_factory(info): + def _render(value, system): + if not isinstance(value, string_types): + value = str(value) + request = system.get('request') + if request is not None: + response = request.response + ct = response.content_type + if ct == response.default_content_type: + response.content_type = 'text/plain' + return value + return _render + +_marker = object() + +class JSON(object): + """ Renderer that returns a JSON-encoded string. + + Configure a custom JSON renderer using the + :meth:`~pyramid.config.Configurator.add_renderer` API at application + startup time: + + .. code-block:: python + + from pyramid.config import Configurator + + config = Configurator() + config.add_renderer('myjson', JSON(indent=4)) + + Once this renderer is registered as above, you can use + ``myjson`` as the ``renderer=`` parameter to ``@view_config`` or + :meth:`~pyramid.config.Configurator.add_view`: + + .. code-block:: python + + from pyramid.view import view_config + + @view_config(renderer='myjson') + def myview(request): + return {'greeting':'Hello world'} + + Custom objects can be serialized using the renderer by either + implementing the ``__json__`` magic method, or by registering + adapters with the renderer. See + :ref:`json_serializing_custom_objects` for more information. + + .. note:: + + The default serializer uses ``json.JSONEncoder``. A different + serializer can be specified via the ``serializer`` argument. Custom + serializers should accept the object, a callback ``default``, and any + extra ``kw`` keyword arguments passed during renderer construction. + This feature isn't widely used but it can be used to replace the + stock JSON serializer with, say, simplejson. If all you want to + do, however, is serialize custom objects, you should use the method + explained in :ref:`json_serializing_custom_objects` instead + of replacing the serializer. + + .. versionadded:: 1.4 + Prior to this version, there was no public API for supplying options + to the underlying serializer without defining a custom renderer. + """ + + def __init__(self, serializer=json.dumps, adapters=(), **kw): + """ Any keyword arguments will be passed to the ``serializer`` + function.""" + self.serializer = serializer + self.kw = kw + self.components = Components() + for type, adapter in adapters: + self.add_adapter(type, adapter) + + def add_adapter(self, type_or_iface, adapter): + """ When an object of the type (or interface) ``type_or_iface`` fails + to automatically encode using the serializer, the renderer will use + the adapter ``adapter`` to convert it into a JSON-serializable + object. The adapter must accept two arguments: the object and the + currently active request. + + .. code-block:: python + + class Foo(object): + x = 5 + + def foo_adapter(obj, request): + return obj.x + + renderer = JSON(indent=4) + renderer.add_adapter(Foo, foo_adapter) + + When you've done this, the JSON renderer will be able to serialize + instances of the ``Foo`` class when they're encountered in your view + results.""" + + self.components.registerAdapter(adapter, (type_or_iface,), + IJSONAdapter) + + def __call__(self, info): + """ Returns a plain JSON-encoded string with content-type + ``application/json``. The content-type may be overridden by + setting ``request.response.content_type``.""" + def _render(value, system): + request = system.get('request') + if request is not None: + response = request.response + ct = response.content_type + if ct == response.default_content_type: + response.content_type = 'application/json' + default = self._make_default(request) + return self.serializer(value, default=default, **self.kw) + + return _render + + def _make_default(self, request): + def default(obj): + if hasattr(obj, '__json__'): + return obj.__json__(request) + obj_iface = providedBy(obj) + adapters = self.components.adapters + result = adapters.lookup((obj_iface,), IJSONAdapter, + default=_marker) + if result is _marker: + raise TypeError('%r is not JSON serializable' % (obj,)) + return result(obj, request) + return default + +json_renderer_factory = JSON() # bw compat + +JSONP_VALID_CALLBACK = re.compile(r"^[$a-z_][$0-9a-z_\.\[\]]+[^.]$", re.I) + +class JSONP(JSON): + """ `JSONP `_ renderer factory helper + which implements a hybrid json/jsonp renderer. JSONP is useful for + making cross-domain AJAX requests. + + Configure a JSONP renderer using the + :meth:`pyramid.config.Configurator.add_renderer` API at application + startup time: + + .. code-block:: python + + from pyramid.config import Configurator + + config = Configurator() + config.add_renderer('jsonp', JSONP(param_name='callback')) + + The class' constructor also accepts arbitrary keyword arguments. All + keyword arguments except ``param_name`` are passed to the ``json.dumps`` + function as its keyword arguments. + + .. code-block:: python + + from pyramid.config import Configurator + + config = Configurator() + config.add_renderer('jsonp', JSONP(param_name='callback', indent=4)) + + .. versionchanged:: 1.4 + The ability of this class to accept a ``**kw`` in its constructor. + + The arguments passed to this class' constructor mean the same thing as + the arguments passed to :class:`pyramid.renderers.JSON` (including + ``serializer`` and ``adapters``). + + Once this renderer is registered via + :meth:`~pyramid.config.Configurator.add_renderer` as above, you can use + ``jsonp`` as the ``renderer=`` parameter to ``@view_config`` or + :meth:`pyramid.config.Configurator.add_view``: + + .. code-block:: python + + from pyramid.view import view_config + + @view_config(renderer='jsonp') + def myview(request): + return {'greeting':'Hello world'} + + When a view is called that uses the JSONP renderer: + + - If there is a parameter in the request's HTTP query string that matches + the ``param_name`` of the registered JSONP renderer (by default, + ``callback``), the renderer will return a JSONP response. + + - If there is no callback parameter in the request's query string, the + renderer will return a 'plain' JSON response. + + .. versionadded:: 1.1 + + .. seealso:: + + See also :ref:`jsonp_renderer`. + """ + + def __init__(self, param_name='callback', **kw): + self.param_name = param_name + JSON.__init__(self, **kw) + + def __call__(self, info): + """ Returns JSONP-encoded string with content-type + ``application/javascript`` if query parameter matching + ``self.param_name`` is present in request.GET; otherwise returns + plain-JSON encoded string with content-type ``application/json``""" + def _render(value, system): + request = system.get('request') + default = self._make_default(request) + val = self.serializer(value, default=default, **self.kw) + ct = 'application/json' + body = val + if request is not None: + callback = request.GET.get(self.param_name) + + if callback is not None: + if not JSONP_VALID_CALLBACK.match(callback): + raise HTTPBadRequest('Invalid JSONP callback function name.') + + ct = 'application/javascript' + body = '/**/{0}({1});'.format(callback, val) + response = request.response + if response.content_type == response.default_content_type: + response.content_type = ct + return body + return _render + +@implementer(IRendererInfo) +class RendererHelper(object): + def __init__(self, name=None, package=None, registry=None): + if name and '.' in name: + rtype = os.path.splitext(name)[1] + else: + # important.. must be a string; cannot be None; see issue 249 + rtype = name or '' + + if registry is None: + registry = get_current_registry() + + self.name = name + self.package = package + self.type = rtype + self.registry = registry + + @reify + def settings(self): + settings = self.registry.settings + if settings is None: + settings = {} + return settings + + @reify + def renderer(self): + factory = self.registry.queryUtility(IRendererFactory, name=self.type) + if factory is None: + raise ValueError( + 'No such renderer factory %s' % str(self.type)) + return factory(self) + + def get_renderer(self): + return self.renderer + + def render_view(self, request, response, view, context): + system = {'view':view, + 'renderer_name':self.name, # b/c + 'renderer_info':self, + 'context':context, + 'request':request, + 'req':request, + 'get_csrf_token':partial(get_csrf_token, request), + } + return self.render_to_response(response, system, request=request) + + def render(self, value, system_values, request=None): + renderer = self.renderer + if system_values is None: + system_values = { + 'view':None, + 'renderer_name':self.name, # b/c + 'renderer_info':self, + 'context':getattr(request, 'context', None), + 'request':request, + 'req':request, + 'get_csrf_token':partial(get_csrf_token, request), + } + + system_values = BeforeRender(system_values, value) + + registry = self.registry + registry.notify(system_values) + result = renderer(value, system_values) + return result + + def render_to_response(self, value, system_values, request=None): + result = self.render(value, system_values, request=request) + return self._make_response(result, request) + + def _make_response(self, result, request): + # broken out of render_to_response as a separate method for testing + # purposes + response = getattr(request, 'response', None) + if response is None: + # request is None or request is not a pyramid.response.Response + registry = self.registry + response_factory = _get_response_factory(registry) + response = response_factory(request) + + if result is not None: + if isinstance(result, text_type): + response.text = result + elif isinstance(result, bytes): + response.body = result + elif hasattr(result, '__iter__'): + response.app_iter = result + else: + response.body = result + + return response + + def clone(self, name=None, package=None, registry=None): + if name is None: + name = self.name + if package is None: + package = self.package + if registry is None: + registry = self.registry + return self.__class__(name=name, package=package, registry=registry) + +class NullRendererHelper(RendererHelper): + """ Special renderer helper that has render_* methods which simply return + the value they are fed rather than converting them to response objects; + useful for testing purposes and special case view configuration + registrations that want to use the view configuration machinery but do + not want actual rendering to happen .""" + def __init__(self, name=None, package=None, registry=None): + # we override the initializer to avoid calling get_current_registry + # (it will return a reference to the global registry when this + # thing is called at module scope; we don't want that). + self.name = None + self.package = None + self.type = '' + self.registry = None + + @property + def settings(self): + return {} + + def render_view(self, request, value, view, context): + return value + + def render(self, value, system_values, request=None): + return value + + def render_to_response(self, value, system_values, request=None): + return value + + def clone(self, name=None, package=None, registry=None): + return self + +null_renderer = NullRendererHelper() diff --git a/src/pyramid/request.py b/src/pyramid/request.py new file mode 100644 index 000000000..201f1d648 --- /dev/null +++ b/src/pyramid/request.py @@ -0,0 +1,334 @@ +from collections import deque +import json + +from zope.interface import implementer +from zope.interface.interface import InterfaceClass + +from webob import BaseRequest + +from pyramid.interfaces import ( + IRequest, + IRequestExtensions, + IResponse, + ISessionFactory, + ) + +from pyramid.compat import ( + text_, + bytes_, + native_, + iteritems_, + ) + +from pyramid.decorator import reify +from pyramid.i18n import LocalizerRequestMixin +from pyramid.response import Response, _get_response_factory +from pyramid.security import ( + AuthenticationAPIMixin, + AuthorizationAPIMixin, + ) +from pyramid.url import URLMethodsMixin +from pyramid.util import ( + InstancePropertyHelper, + InstancePropertyMixin, +) +from pyramid.view import ViewMethodsMixin + +class TemplateContext(object): + pass + +class CallbackMethodsMixin(object): + @reify + def finished_callbacks(self): + return deque() + + @reify + def response_callbacks(self): + return deque() + + def add_response_callback(self, callback): + """ + Add a callback to the set of callbacks to be called by the + :term:`router` at a point after a :term:`response` object is + successfully created. :app:`Pyramid` does not have a + global response object: this functionality allows an + application to register an action to be performed against the + response once one is created. + + A 'callback' is a callable which accepts two positional + parameters: ``request`` and ``response``. For example: + + .. code-block:: python + :linenos: + + def cache_callback(request, response): + 'Set the cache_control max_age for the response' + response.cache_control.max_age = 360 + request.add_response_callback(cache_callback) + + Response callbacks are called in the order they're added + (first-to-most-recently-added). No response callback is + called if an exception happens in application code, or if the + response object returned by :term:`view` code is invalid. + + All response callbacks are called *after* the tweens and + *before* the :class:`pyramid.events.NewResponse` event is sent. + + Errors raised by callbacks are not handled specially. They + will be propagated to the caller of the :app:`Pyramid` + router application. + + .. seealso:: + + See also :ref:`using_response_callbacks`. + """ + + self.response_callbacks.append(callback) + + def _process_response_callbacks(self, response): + callbacks = self.response_callbacks + while callbacks: + callback = callbacks.popleft() + callback(self, response) + + def add_finished_callback(self, callback): + """ + Add a callback to the set of callbacks to be called + unconditionally by the :term:`router` at the very end of + request processing. + + ``callback`` is a callable which accepts a single positional + parameter: ``request``. For example: + + .. code-block:: python + :linenos: + + import transaction + + def commit_callback(request): + '''commit or abort the transaction associated with request''' + if request.exception is not None: + transaction.abort() + else: + transaction.commit() + request.add_finished_callback(commit_callback) + + Finished callbacks are called in the order they're added ( + first- to most-recently- added). Finished callbacks (unlike + response callbacks) are *always* called, even if an exception + happens in application code that prevents a response from + being generated. + + The set of finished callbacks associated with a request are + called *very late* in the processing of that request; they are + essentially the last thing called by the :term:`router`. They + are called after response processing has already occurred in a + top-level ``finally:`` block within the router request + processing code. As a result, mutations performed to the + ``request`` provided to a finished callback will have no + meaningful effect, because response processing will have + already occurred, and the request's scope will expire almost + immediately after all finished callbacks have been processed. + + Errors raised by finished callbacks are not handled specially. + They will be propagated to the caller of the :app:`Pyramid` + router application. + + .. seealso:: + + See also :ref:`using_finished_callbacks`. + """ + self.finished_callbacks.append(callback) + + def _process_finished_callbacks(self): + callbacks = self.finished_callbacks + while callbacks: + callback = callbacks.popleft() + callback(self) + +@implementer(IRequest) +class Request( + BaseRequest, + URLMethodsMixin, + CallbackMethodsMixin, + InstancePropertyMixin, + LocalizerRequestMixin, + AuthenticationAPIMixin, + AuthorizationAPIMixin, + ViewMethodsMixin, + ): + """ + A subclass of the :term:`WebOb` Request class. An instance of + this class is created by the :term:`router` and is provided to a + view callable (and to other subsystems) as the ``request`` + argument. + + The documentation below (save for the ``add_response_callback`` and + ``add_finished_callback`` methods, which are defined in this subclass + itself, and the attributes ``context``, ``registry``, ``root``, + ``subpath``, ``traversed``, ``view_name``, ``virtual_root`` , and + ``virtual_root_path``, each of which is added to the request by the + :term:`router` at request ingress time) are autogenerated from the WebOb + source code used when this documentation was generated. + + Due to technical constraints, we can't yet display the WebOb + version number from which this documentation is autogenerated, but + it will be the 'prevailing WebOb version' at the time of the + release of this :app:`Pyramid` version. See + https://webob.org/ for further information. + """ + exception = None + exc_info = None + matchdict = None + matched_route = None + request_iface = IRequest + + ResponseClass = Response + + @reify + def tmpl_context(self): + # docs-deprecated template context for Pylons-like apps; do not + # remove. + return TemplateContext() + + @reify + def session(self): + """ Obtain the :term:`session` object associated with this + request. If a :term:`session factory` has not been registered + during application configuration, a + :class:`pyramid.exceptions.ConfigurationError` will be raised""" + factory = self.registry.queryUtility(ISessionFactory) + if factory is None: + raise AttributeError( + 'No session factory registered ' + '(see the Sessions chapter of the Pyramid documentation)') + return factory(self) + + @reify + def response(self): + """This attribute is actually a "reified" property which returns an + instance of the :class:`pyramid.response.Response`. class. The + response object returned does not exist until this attribute is + accessed. Subsequent accesses will return the same Response object. + + The ``request.response`` API is used by renderers. A render obtains + the response object it will return from a view that uses that renderer + by accessing ``request.response``. Therefore, it's possible to use the + ``request.response`` API to set up a response object with "the + right" attributes (e.g. by calling ``request.response.set_cookie()``) + within a view that uses a renderer. Mutations to this response object + will be preserved in the response sent to the client.""" + response_factory = _get_response_factory(self.registry) + return response_factory(self) + + def is_response(self, ob): + """ Return ``True`` if the object passed as ``ob`` is a valid + response object, ``False`` otherwise.""" + if ob.__class__ is Response: + return True + registry = self.registry + adapted = registry.queryAdapterOrSelf(ob, IResponse) + if adapted is None: + return False + return adapted is ob + + @property + def json_body(self): + return json.loads(text_(self.body, self.charset)) + + +def route_request_iface(name, bases=()): + # zope.interface treats the __name__ as the __doc__ and changes __name__ + # to None for interfaces that contain spaces if you do not pass a + # nonempty __doc__ (insane); see + # zope.interface.interface.Element.__init__ and + # https://github.com/Pylons/pyramid/issues/232; as a result, always pass + # __doc__ to the InterfaceClass constructor. + iface = InterfaceClass('%s_IRequest' % name, bases=bases, + __doc__="route_request_iface-generated interface") + # for exception view lookups + iface.combined = InterfaceClass( + '%s_combined_IRequest' % name, + bases=(iface, IRequest), + __doc__='route_request_iface-generated combined interface') + return iface + + +def add_global_response_headers(request, headerlist): + def add_headers(request, response): + for k, v in headerlist: + response.headerlist.append((k, v)) + request.add_response_callback(add_headers) + +def call_app_with_subpath_as_path_info(request, app): + # Copy the request. Use the source request's subpath (if it exists) as + # the new request's PATH_INFO. Set the request copy's SCRIPT_NAME to the + # prefix before the subpath. Call the application with the new request + # and return a response. + # + # Postconditions: + # - SCRIPT_NAME and PATH_INFO are empty or start with / + # - At least one of SCRIPT_NAME or PATH_INFO are set. + # - SCRIPT_NAME is not '/' (it should be '', and PATH_INFO should + # be '/'). + + environ = request.environ + script_name = environ.get('SCRIPT_NAME', '') + path_info = environ.get('PATH_INFO', '/') + subpath = list(getattr(request, 'subpath', ())) + + new_script_name = '' + + # compute new_path_info + new_path_info = '/' + '/'.join([native_(x.encode('utf-8'), 'latin-1') + for x in subpath]) + + if new_path_info != '/': # don't want a sole double-slash + if path_info != '/': # if orig path_info is '/', we're already done + if path_info.endswith('/'): + # readd trailing slash stripped by subpath (traversal) + # conversion + new_path_info += '/' + + # compute new_script_name + workback = (script_name + path_info).split('/') + + tmp = [] + while workback: + if tmp == subpath: + break + el = workback.pop() + if el: + tmp.insert(0, text_(bytes_(el, 'latin-1'), 'utf-8')) + + # strip all trailing slashes from workback to avoid appending undue slashes + # to end of script_name + while workback and (workback[-1] == ''): + workback = workback[:-1] + + new_script_name = '/'.join(workback) + + new_request = request.copy() + new_request.environ['SCRIPT_NAME'] = new_script_name + new_request.environ['PATH_INFO'] = new_path_info + + return new_request.get_response(app) + +def apply_request_extensions(request, extensions=None): + """Apply request extensions (methods and properties) to an instance of + :class:`pyramid.interfaces.IRequest`. This method is dependent on the + ``request`` containing a properly initialized registry. + + After invoking this method, the ``request`` should have the methods + and properties that were defined using + :meth:`pyramid.config.Configurator.add_request_method`. + """ + if extensions is None: + extensions = request.registry.queryUtility(IRequestExtensions) + if extensions is not None: + for name, fn in iteritems_(extensions.methods): + method = fn.__get__(request, request.__class__) + setattr(request, name, method) + + InstancePropertyHelper.apply_properties( + request, extensions.descriptors) diff --git a/src/pyramid/resource.py b/src/pyramid/resource.py new file mode 100644 index 000000000..986c75e37 --- /dev/null +++ b/src/pyramid/resource.py @@ -0,0 +1,5 @@ +""" Backwards compatibility shim module (forever). """ +from pyramid.asset import * # b/w compat +resolve_resource_spec = resolve_asset_spec +resource_spec_from_abspath = asset_spec_from_abspath +abspath_from_resource_spec = abspath_from_asset_spec diff --git a/src/pyramid/response.py b/src/pyramid/response.py new file mode 100644 index 000000000..1e2546ed0 --- /dev/null +++ b/src/pyramid/response.py @@ -0,0 +1,211 @@ +import mimetypes +from os.path import ( + getmtime, + getsize, + ) + +import venusian + +from webob import Response as _Response +from zope.interface import implementer +from pyramid.interfaces import IResponse, IResponseFactory + + +def init_mimetypes(mimetypes): + # this is a function so it can be unittested + if hasattr(mimetypes, 'init'): + mimetypes.init() + return True + return False + +# See http://bugs.python.org/issue5853 which is a recursion bug +# that seems to effect Python 2.6, Python 2.6.1, and 2.6.2 (a fix +# has been applied on the Python 2 trunk). +init_mimetypes(mimetypes) + +_BLOCK_SIZE = 4096 * 64 # 256K + +@implementer(IResponse) +class Response(_Response): + pass + +class FileResponse(Response): + """ + A Response object that can be used to serve a static file from disk + simply. + + ``path`` is a file path on disk. + + ``request`` must be a Pyramid :term:`request` object. Note + that a request *must* be passed if the response is meant to attempt to + use the ``wsgi.file_wrapper`` feature of the web server that you're using + to serve your Pyramid application. + + ``cache_max_age`` is the number of seconds that should be used + to HTTP cache this response. + + ``content_type`` is the content_type of the response. + + ``content_encoding`` is the content_encoding of the response. + It's generally safe to leave this set to ``None`` if you're serving a + binary file. This argument will be ignored if you also leave + ``content-type`` as ``None``. + """ + def __init__(self, path, request=None, cache_max_age=None, + content_type=None, content_encoding=None): + if content_type is None: + content_type, content_encoding = _guess_type(path) + super(FileResponse, self).__init__( + conditional_response=True, + content_type=content_type, + content_encoding=content_encoding + ) + self.last_modified = getmtime(path) + content_length = getsize(path) + f = open(path, 'rb') + app_iter = None + if request is not None: + environ = request.environ + if 'wsgi.file_wrapper' in environ: + app_iter = environ['wsgi.file_wrapper'](f, _BLOCK_SIZE) + if app_iter is None: + app_iter = FileIter(f, _BLOCK_SIZE) + self.app_iter = app_iter + # assignment of content_length must come after assignment of app_iter + self.content_length = content_length + if cache_max_age is not None: + self.cache_expires = cache_max_age + +class FileIter(object): + """ A fixed-block-size iterator for use as a WSGI app_iter. + + ``file`` is a Python file pointer (or at least an object with a ``read`` + method that takes a size hint). + + ``block_size`` is an optional block size for iteration. + """ + def __init__(self, file, block_size=_BLOCK_SIZE): + self.file = file + self.block_size = block_size + + def __iter__(self): + return self + + def next(self): + val = self.file.read(self.block_size) + if not val: + raise StopIteration + return val + + __next__ = next # py3 + + def close(self): + self.file.close() + + +class response_adapter(object): + """ Decorator activated via a :term:`scan` which treats the function + being decorated as a :term:`response adapter` for the set of types or + interfaces passed as ``*types_or_ifaces`` to the decorator constructor. + + For example, if you scan the following response adapter: + + .. code-block:: python + + from pyramid.response import Response + from pyramid.response import response_adapter + + @response_adapter(int) + def myadapter(i): + return Response(status=i) + + You can then return an integer from your view callables, and it will be + converted into a response with the integer as the status code. + + More than one type or interface can be passed as a constructor argument. + The decorated response adapter will be called for each type or interface. + + .. code-block:: python + + import json + + from pyramid.response import Response + from pyramid.response import response_adapter + + @response_adapter(dict, list) + def myadapter(ob): + return Response(json.dumps(ob)) + + This method will have no effect until a :term:`scan` is performed + agains the package or module which contains it, ala: + + .. code-block:: python + + from pyramid.config import Configurator + config = Configurator() + config.scan('somepackage_containing_adapters') + + Two additional keyword arguments which will be passed to the + :term:`venusian` ``attach`` function are ``_depth`` and ``_category``. + + ``_depth`` is provided for people who wish to reuse this class from another + decorator. The default value is ``0`` and should be specified relative to + the ``response_adapter`` invocation. It will be passed in to the + :term:`venusian` ``attach`` function as the depth of the callstack when + Venusian checks if the decorator is being used in a class or module + context. It's not often used, but it can be useful in this circumstance. + + ``_category`` sets the decorator category name. It can be useful in + combination with the ``category`` argument of ``scan`` to control which + views should be processed. + + See the :py:func:`venusian.attach` function in Venusian for more + information about the ``_depth`` and ``_category`` arguments. + + .. versionchanged:: 1.9.1 + Added the ``_depth`` and ``_category`` arguments. + + """ + venusian = venusian # for unit testing + + def __init__(self, *types_or_ifaces, **kwargs): + self.types_or_ifaces = types_or_ifaces + self.depth = kwargs.pop('_depth', 0) + self.category = kwargs.pop('_category', 'pyramid') + self.kwargs = kwargs + + def register(self, scanner, name, wrapped): + config = scanner.config + for type_or_iface in self.types_or_ifaces: + config.add_response_adapter(wrapped, type_or_iface, **self.kwargs) + + def __call__(self, wrapped): + self.venusian.attach(wrapped, self.register, category=self.category, + depth=self.depth + 1) + return wrapped + + +def _get_response_factory(registry): + """ Obtain a :class: `pyramid.response.Response` using the + `pyramid.interfaces.IResponseFactory`. + """ + response_factory = registry.queryUtility( + IResponseFactory, + default=lambda r: Response() + ) + + return response_factory + + +def _guess_type(path): + content_type, content_encoding = mimetypes.guess_type( + path, + strict=False + ) + if content_type is None: + content_type = 'application/octet-stream' + # str-ifying content_type is a workaround for a bug in Python 2.7.7 + # on Windows where mimetypes.guess_type returns unicode for the + # content_type. + content_type = str(content_type) + return content_type, content_encoding diff --git a/src/pyramid/router.py b/src/pyramid/router.py new file mode 100644 index 000000000..49b7b601b --- /dev/null +++ b/src/pyramid/router.py @@ -0,0 +1,278 @@ +from zope.interface import ( + implementer, + providedBy, + ) + +from pyramid.interfaces import ( + IDebugLogger, + IExecutionPolicy, + IRequest, + IRequestExtensions, + IRootFactory, + IRouteRequest, + IRouter, + IRequestFactory, + IRoutesMapper, + ITraverser, + ITweens, + ) + +from pyramid.events import ( + ContextFound, + NewRequest, + NewResponse, + BeforeTraversal, + ) + +from pyramid.httpexceptions import HTTPNotFound +from pyramid.request import Request +from pyramid.view import _call_view +from pyramid.request import apply_request_extensions +from pyramid.threadlocal import RequestContext + +from pyramid.traversal import ( + DefaultRootFactory, + ResourceTreeTraverser, + ) + +@implementer(IRouter) +class Router(object): + + debug_notfound = False + debug_routematch = False + + def __init__(self, registry): + q = registry.queryUtility + self.logger = q(IDebugLogger) + self.root_factory = q(IRootFactory, default=DefaultRootFactory) + self.routes_mapper = q(IRoutesMapper) + self.request_factory = q(IRequestFactory, default=Request) + self.request_extensions = q(IRequestExtensions) + self.execution_policy = q( + IExecutionPolicy, default=default_execution_policy) + self.orig_handle_request = self.handle_request + tweens = q(ITweens) + if tweens is not None: + self.handle_request = tweens(self.handle_request, registry) + self.root_policy = self.root_factory # b/w compat + self.registry = registry + settings = registry.settings + if settings is not None: + self.debug_notfound = settings['debug_notfound'] + self.debug_routematch = settings['debug_routematch'] + + def handle_request(self, request): + attrs = request.__dict__ + registry = attrs['registry'] + + request.request_iface = IRequest + context = None + routes_mapper = self.routes_mapper + debug_routematch = self.debug_routematch + adapters = registry.adapters + has_listeners = registry.has_listeners + notify = registry.notify + logger = self.logger + + has_listeners and notify(NewRequest(request)) + # find the root object + root_factory = self.root_factory + if routes_mapper is not None: + info = routes_mapper(request) + match, route = info['match'], info['route'] + if route is None: + if debug_routematch: + msg = ('no route matched for url %s' % + request.url) + logger and logger.debug(msg) + else: + attrs['matchdict'] = match + attrs['matched_route'] = route + + if debug_routematch: + msg = ( + 'route matched for url %s; ' + 'route_name: %r, ' + 'path_info: %r, ' + 'pattern: %r, ' + 'matchdict: %r, ' + 'predicates: %r' % ( + request.url, + route.name, + request.path_info, + route.pattern, + match, + ', '.join([p.text() for p in route.predicates])) + ) + logger and logger.debug(msg) + + request.request_iface = registry.queryUtility( + IRouteRequest, + name=route.name, + default=IRequest) + + root_factory = route.factory or self.root_factory + + # Notify anyone listening that we are about to start traversal + # + # Notify before creating root_factory in case we want to do something + # special on a route we may have matched. See + # https://github.com/Pylons/pyramid/pull/1876 for ideas of what is + # possible. + has_listeners and notify(BeforeTraversal(request)) + + # Create the root factory + root = root_factory(request) + attrs['root'] = root + + # We are about to traverse and find a context + traverser = adapters.queryAdapter(root, ITraverser) + if traverser is None: + traverser = ResourceTreeTraverser(root) + tdict = traverser(request) + + context, view_name, subpath, traversed, vroot, vroot_path = ( + tdict['context'], + tdict['view_name'], + tdict['subpath'], + tdict['traversed'], + tdict['virtual_root'], + tdict['virtual_root_path'] + ) + + attrs.update(tdict) + + # Notify anyone listening that we have a context and traversal is + # complete + has_listeners and notify(ContextFound(request)) + + # find a view callable + context_iface = providedBy(context) + response = _call_view( + registry, + request, + context, + context_iface, + view_name + ) + + if response is None: + if self.debug_notfound: + msg = ( + 'debug_notfound of url %s; path_info: %r, ' + 'context: %r, view_name: %r, subpath: %r, ' + 'traversed: %r, root: %r, vroot: %r, ' + 'vroot_path: %r' % ( + request.url, request.path_info, context, + view_name, subpath, traversed, root, vroot, + vroot_path) + ) + logger and logger.debug(msg) + else: + msg = request.path_info + raise HTTPNotFound(msg) + + return response + + def invoke_subrequest(self, request, use_tweens=False): + """Obtain a response object from the Pyramid application based on + information in the ``request`` object provided. The ``request`` + object must be an object that implements the Pyramid request + interface (such as a :class:`pyramid.request.Request` instance). If + ``use_tweens`` is ``True``, the request will be sent to the + :term:`tween` in the tween stack closest to the request ingress. If + ``use_tweens`` is ``False``, the request will be sent to the main + router handler, and no tweens will be invoked. + + See the API for pyramid.request for complete documentation. + """ + request.registry = self.registry + request.invoke_subrequest = self.invoke_subrequest + extensions = self.request_extensions + if extensions is not None: + apply_request_extensions(request, extensions=extensions) + with RequestContext(request): + return self.invoke_request(request, _use_tweens=use_tweens) + + def request_context(self, environ): + """ + Create a new request context from a WSGI environ. + + The request context is used to push/pop the threadlocals required + when processing the request. It also contains an initialized + :class:`pyramid.interfaces.IRequest` instance using the registered + :class:`pyramid.interfaces.IRequestFactory`. The context may be + used as a context manager to control the threadlocal lifecycle: + + .. code-block:: python + + with router.request_context(environ) as request: + ... + + Alternatively, the context may be used without the ``with`` statement + by manually invoking its ``begin()`` and ``end()`` methods. + + .. code-block:: python + + ctx = router.request_context(environ) + request = ctx.begin() + try: + ... + finally: + ctx.end() + + """ + request = self.request_factory(environ) + request.registry = self.registry + request.invoke_subrequest = self.invoke_subrequest + extensions = self.request_extensions + if extensions is not None: + apply_request_extensions(request, extensions=extensions) + return RequestContext(request) + + def invoke_request(self, request, _use_tweens=True): + """ + Execute a request through the request processing pipeline and + return the generated response. + + """ + registry = self.registry + has_listeners = registry.has_listeners + notify = registry.notify + + if _use_tweens: + handle_request = self.handle_request + else: + handle_request = self.orig_handle_request + + try: + response = handle_request(request) + + if request.response_callbacks: + request._process_response_callbacks(response) + + has_listeners and notify(NewResponse(request, response)) + + return response + + finally: + if request.finished_callbacks: + request._process_finished_callbacks() + + def __call__(self, environ, start_response): + """ + Accept ``environ`` and ``start_response``; create a + :term:`request` and route the request to a :app:`Pyramid` + view based on introspection of :term:`view configuration` + within the application registry; call ``start_response`` and + return an iterable. + """ + response = self.execution_policy(environ, self) + return response(environ, start_response) + +def default_execution_policy(environ, router): + with router.request_context(environ) as request: + try: + return router.invoke_request(request) + except Exception: + return request.invoke_exception_view(reraise=True) diff --git a/src/pyramid/scaffolds/__init__.py b/src/pyramid/scaffolds/__init__.py new file mode 100644 index 000000000..71a220e22 --- /dev/null +++ b/src/pyramid/scaffolds/__init__.py @@ -0,0 +1,65 @@ +import binascii +import os +from textwrap import dedent + +from pyramid.compat import native_ + +from pyramid.scaffolds.template import Template # API + +class PyramidTemplate(Template): + """ + A class that can be used as a base class for Pyramid scaffolding + templates. + """ + def pre(self, command, output_dir, vars): + """ Overrides :meth:`pyramid.scaffolds.template.Template.pre`, adding + several variables to the default variables list (including + ``random_string``, and ``package_logger``). It also prevents common + misnamings (such as naming a package "site" or naming a package + logger "root". + """ + vars['random_string'] = native_(binascii.hexlify(os.urandom(20))) + package_logger = vars['package'] + if package_logger == 'root': + # Rename the app logger in the rare case a project is named 'root' + package_logger = 'app' + vars['package_logger'] = package_logger + return Template.pre(self, command, output_dir, vars) + + def post(self, command, output_dir, vars): # pragma: no cover + """ Overrides :meth:`pyramid.scaffolds.template.Template.post`, to + print "Welcome to Pyramid. Sorry for the convenience." after a + successful scaffolding rendering.""" + + separator = "=" * 79 + msg = dedent( + """ + %(separator)s + Tutorials: https://docs.pylonsproject.org/projects/pyramid_tutorials/en/latest/ + Documentation: https://docs.pylonsproject.org/projects/pyramid/en/latest/ + Twitter: https://twitter.com/PylonsProject + Mailing List: https://groups.google.com/forum/#!forum/pylons-discuss + + Welcome to Pyramid. Sorry for the convenience. + %(separator)s + """ % {'separator': separator}) + + self.out(msg) + return Template.post(self, command, output_dir, vars) + + def out(self, msg): # pragma: no cover (replaceable testing hook) + print(msg) + +class StarterProjectTemplate(PyramidTemplate): + _template_dir = 'starter' + summary = 'Pyramid starter project using URL dispatch and Jinja2' + +class ZODBProjectTemplate(PyramidTemplate): + _template_dir = 'zodb' + summary = 'Pyramid project using ZODB, traversal, and Chameleon' + +class AlchemyProjectTemplate(PyramidTemplate): + _template_dir = 'alchemy' + summary = ( + 'Pyramid project using SQLAlchemy, SQLite, URL dispatch, and ' + 'Jinja2') diff --git a/src/pyramid/scaffolds/alchemy/+dot+coveragerc_tmpl b/src/pyramid/scaffolds/alchemy/+dot+coveragerc_tmpl new file mode 100644 index 000000000..273a4a580 --- /dev/null +++ b/src/pyramid/scaffolds/alchemy/+dot+coveragerc_tmpl @@ -0,0 +1,3 @@ +[run] +source = {{package}} +omit = {{package}}/test* diff --git a/src/pyramid/scaffolds/alchemy/+package+/__init__.py b/src/pyramid/scaffolds/alchemy/+package+/__init__.py new file mode 100644 index 000000000..4dab44823 --- /dev/null +++ b/src/pyramid/scaffolds/alchemy/+package+/__init__.py @@ -0,0 +1,12 @@ +from pyramid.config import Configurator + + +def main(global_config, **settings): + """ This function returns a Pyramid WSGI application. + """ + config = Configurator(settings=settings) + config.include('pyramid_jinja2') + config.include('.models') + config.include('.routes') + config.scan() + return config.make_wsgi_app() diff --git a/src/pyramid/scaffolds/alchemy/+package+/models/__init__.py_tmpl b/src/pyramid/scaffolds/alchemy/+package+/models/__init__.py_tmpl new file mode 100644 index 000000000..521816ce7 --- /dev/null +++ b/src/pyramid/scaffolds/alchemy/+package+/models/__init__.py_tmpl @@ -0,0 +1,74 @@ +from sqlalchemy import engine_from_config +from sqlalchemy.orm import sessionmaker +from sqlalchemy.orm import configure_mappers +import zope.sqlalchemy + +# import or define all models here to ensure they are attached to the +# Base.metadata prior to any initialization routines +from .mymodel import MyModel # noqa + +# run configure_mappers after defining all of the models to ensure +# all relationships can be setup +configure_mappers() + + +def get_engine(settings, prefix='sqlalchemy.'): + return engine_from_config(settings, prefix) + + +def get_session_factory(engine): + factory = sessionmaker() + factory.configure(bind=engine) + return factory + + +def get_tm_session(session_factory, transaction_manager): + """ + Get a ``sqlalchemy.orm.Session`` instance backed by a transaction. + + This function will hook the session to the transaction manager which + will take care of committing any changes. + + - When using pyramid_tm it will automatically be committed or aborted + depending on whether an exception is raised. + + - When using scripts you should wrap the session in a manager yourself. + For example:: + + import transaction + + engine = get_engine(settings) + session_factory = get_session_factory(engine) + with transaction.manager: + dbsession = get_tm_session(session_factory, transaction.manager) + + """ + dbsession = session_factory() + zope.sqlalchemy.register( + dbsession, transaction_manager=transaction_manager) + return dbsession + + +def includeme(config): + """ + Initialize the model for a Pyramid app. + + Activate this setup using ``config.include('{{project}}.models')``. + + """ + settings = config.get_settings() + settings['tm.manager_hook'] = 'pyramid_tm.explicit_manager' + + # use pyramid_tm to hook the transaction lifecycle to the request + config.include('pyramid_tm') + + session_factory = get_session_factory(get_engine(settings)) + config.registry['dbsession_factory'] = session_factory + + # make request.dbsession available for use in Pyramid + config.add_request_method( + # r.tm is the transaction manager used by pyramid_tm + lambda r: get_tm_session(session_factory, r.tm), + 'dbsession', + reify=True + ) diff --git a/src/pyramid/scaffolds/alchemy/+package+/models/meta.py b/src/pyramid/scaffolds/alchemy/+package+/models/meta.py new file mode 100644 index 000000000..0682247b5 --- /dev/null +++ b/src/pyramid/scaffolds/alchemy/+package+/models/meta.py @@ -0,0 +1,16 @@ +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.schema import MetaData + +# Recommended naming convention used by Alembic, as various different database +# providers will autogenerate vastly different names making migrations more +# difficult. See: http://alembic.zzzcomputing.com/en/latest/naming.html +NAMING_CONVENTION = { + "ix": 'ix_%(column_0_label)s', + "uq": "uq_%(table_name)s_%(column_0_name)s", + "ck": "ck_%(table_name)s_%(constraint_name)s", + "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", + "pk": "pk_%(table_name)s" +} + +metadata = MetaData(naming_convention=NAMING_CONVENTION) +Base = declarative_base(metadata=metadata) diff --git a/src/pyramid/scaffolds/alchemy/+package+/models/mymodel.py b/src/pyramid/scaffolds/alchemy/+package+/models/mymodel.py new file mode 100644 index 000000000..d65a01a42 --- /dev/null +++ b/src/pyramid/scaffolds/alchemy/+package+/models/mymodel.py @@ -0,0 +1,18 @@ +from sqlalchemy import ( + Column, + Index, + Integer, + Text, +) + +from .meta import Base + + +class MyModel(Base): + __tablename__ = 'models' + id = Column(Integer, primary_key=True) + name = Column(Text) + value = Column(Integer) + + +Index('my_index', MyModel.name, unique=True, mysql_length=255) diff --git a/src/pyramid/scaffolds/alchemy/+package+/routes.py b/src/pyramid/scaffolds/alchemy/+package+/routes.py new file mode 100644 index 000000000..25504ad4d --- /dev/null +++ b/src/pyramid/scaffolds/alchemy/+package+/routes.py @@ -0,0 +1,3 @@ +def includeme(config): + config.add_static_view('static', 'static', cache_max_age=3600) + config.add_route('home', '/') diff --git a/src/pyramid/scaffolds/alchemy/+package+/scripts/__init__.py b/src/pyramid/scaffolds/alchemy/+package+/scripts/__init__.py new file mode 100644 index 000000000..5bb534f79 --- /dev/null +++ b/src/pyramid/scaffolds/alchemy/+package+/scripts/__init__.py @@ -0,0 +1 @@ +# package diff --git a/src/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py b/src/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py new file mode 100644 index 000000000..7307ecc5c --- /dev/null +++ b/src/pyramid/scaffolds/alchemy/+package+/scripts/initializedb.py @@ -0,0 +1,45 @@ +import os +import sys +import transaction + +from pyramid.paster import ( + get_appsettings, + setup_logging, + ) + +from pyramid.scripts.common import parse_vars + +from ..models.meta import Base +from ..models import ( + get_engine, + get_session_factory, + get_tm_session, + ) +from ..models import MyModel + + +def usage(argv): + cmd = os.path.basename(argv[0]) + print('usage: %s [var=value]\n' + '(example: "%s development.ini")' % (cmd, cmd)) + sys.exit(1) + + +def main(argv=sys.argv): + if len(argv) < 2: + usage(argv) + config_uri = argv[1] + options = parse_vars(argv[2:]) + setup_logging(config_uri) + settings = get_appsettings(config_uri, options=options) + + engine = get_engine(settings) + Base.metadata.create_all(engine) + + session_factory = get_session_factory(engine) + + with transaction.manager: + dbsession = get_tm_session(session_factory, transaction.manager) + + model = MyModel(name='one', value=1) + dbsession.add(model) diff --git a/src/pyramid/scaffolds/alchemy/+package+/static/pyramid-16x16.png b/src/pyramid/scaffolds/alchemy/+package+/static/pyramid-16x16.png new file mode 100644 index 000000000..979203112 Binary files /dev/null and b/src/pyramid/scaffolds/alchemy/+package+/static/pyramid-16x16.png differ diff --git a/src/pyramid/scaffolds/alchemy/+package+/static/pyramid.png b/src/pyramid/scaffolds/alchemy/+package+/static/pyramid.png new file mode 100644 index 000000000..4ab837be9 Binary files /dev/null and b/src/pyramid/scaffolds/alchemy/+package+/static/pyramid.png differ diff --git a/src/pyramid/scaffolds/alchemy/+package+/static/theme.css b/src/pyramid/scaffolds/alchemy/+package+/static/theme.css new file mode 100644 index 000000000..0f4b1a4d4 --- /dev/null +++ b/src/pyramid/scaffolds/alchemy/+package+/static/theme.css @@ -0,0 +1,154 @@ +@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700); +body { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; + color: #ffffff; + background: #bc2131; +} +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; +} +p { + font-weight: 300; +} +.font-normal { + font-weight: 400; +} +.font-semi-bold { + font-weight: 600; +} +.font-bold { + font-weight: 700; +} +.starter-template { + margin-top: 250px; +} +.starter-template .content { + margin-left: 10px; +} +.starter-template .content h1 { + margin-top: 10px; + font-size: 60px; +} +.starter-template .content h1 .smaller { + font-size: 40px; + color: #f2b7bd; +} +.starter-template .content .lead { + font-size: 25px; + color: #f2b7bd; +} +.starter-template .content .lead .font-normal { + color: #ffffff; +} +.starter-template .links { + float: right; + right: 0; + margin-top: 125px; +} +.starter-template .links ul { + display: block; + padding: 0; + margin: 0; +} +.starter-template .links ul li { + list-style: none; + display: inline; + margin: 0 10px; +} +.starter-template .links ul li:first-child { + margin-left: 0; +} +.starter-template .links ul li:last-child { + margin-right: 0; +} +.starter-template .links ul li.current-version { + color: #f2b7bd; + font-weight: 400; +} +.starter-template .links ul li a, a { + color: #f2b7bd; + text-decoration: underline; +} +.starter-template .links ul li a:hover, a:hover { + color: #ffffff; + text-decoration: underline; +} +.starter-template .links ul li .icon-muted { + color: #eb8b95; + margin-right: 5px; +} +.starter-template .links ul li:hover .icon-muted { + color: #ffffff; +} +.starter-template .copyright { + margin-top: 10px; + font-size: 0.9em; + color: #f2b7bd; + text-transform: lowercase; + float: right; + right: 0; +} +@media (max-width: 1199px) { + .starter-template .content h1 { + font-size: 45px; + } + .starter-template .content h1 .smaller { + font-size: 30px; + } + .starter-template .content .lead { + font-size: 20px; + } +} +@media (max-width: 991px) { + .starter-template { + margin-top: 0; + } + .starter-template .logo { + margin: 40px auto; + } + .starter-template .content { + margin-left: 0; + text-align: center; + } + .starter-template .content h1 { + margin-bottom: 20px; + } + .starter-template .links { + float: none; + text-align: center; + margin-top: 60px; + } + .starter-template .copyright { + float: none; + text-align: center; + } +} +@media (max-width: 767px) { + .starter-template .content h1 .smaller { + font-size: 25px; + display: block; + } + .starter-template .content .lead { + font-size: 16px; + } + .starter-template .links { + margin-top: 40px; + } + .starter-template .links ul li { + display: block; + margin: 0; + } + .starter-template .links ul li .icon-muted { + display: none; + } + .starter-template .copyright { + margin-top: 20px; + } +} diff --git a/src/pyramid/scaffolds/alchemy/+package+/templates/404.jinja2_tmpl b/src/pyramid/scaffolds/alchemy/+package+/templates/404.jinja2_tmpl new file mode 100644 index 000000000..1917f83c7 --- /dev/null +++ b/src/pyramid/scaffolds/alchemy/+package+/templates/404.jinja2_tmpl @@ -0,0 +1,8 @@ +{% extends "layout.jinja2" %} + +{% block content %} +
+

Pyramid Alchemy scaffold

+

404 Page Not Found

+
+{% endblock content %} diff --git a/src/pyramid/scaffolds/alchemy/+package+/templates/layout.jinja2_tmpl b/src/pyramid/scaffolds/alchemy/+package+/templates/layout.jinja2_tmpl new file mode 100644 index 000000000..d6b3ca9c6 --- /dev/null +++ b/src/pyramid/scaffolds/alchemy/+package+/templates/layout.jinja2_tmpl @@ -0,0 +1,66 @@ + + + + + + + + + + + Alchemy Scaffold for The Pyramid Web Framework + + + + + + + + + + + + + +
+ + + + + + + + diff --git a/src/pyramid/scaffolds/alchemy/+package+/templates/mytemplate.jinja2_tmpl b/src/pyramid/scaffolds/alchemy/+package+/templates/mytemplate.jinja2_tmpl new file mode 100644 index 000000000..01fe5b8e3 --- /dev/null +++ b/src/pyramid/scaffolds/alchemy/+package+/templates/mytemplate.jinja2_tmpl @@ -0,0 +1,8 @@ +{% extends "layout.jinja2" %} + +{% block content %} +
+

Pyramid Alchemy scaffold

+

Welcome to \{\{project\}\}, an application generated by
the Pyramid Web Framework {{pyramid_version}}.

+
+{% endblock content %} diff --git a/src/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl b/src/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl new file mode 100644 index 000000000..072eab5b2 --- /dev/null +++ b/src/pyramid/scaffolds/alchemy/+package+/tests.py_tmpl @@ -0,0 +1,65 @@ +import unittest +import transaction + +from pyramid import testing + + +def dummy_request(dbsession): + return testing.DummyRequest(dbsession=dbsession) + + +class BaseTest(unittest.TestCase): + def setUp(self): + self.config = testing.setUp(settings={ + 'sqlalchemy.url': 'sqlite:///:memory:' + }) + self.config.include('.models') + settings = self.config.get_settings() + + from .models import ( + get_engine, + get_session_factory, + get_tm_session, + ) + + self.engine = get_engine(settings) + session_factory = get_session_factory(self.engine) + + self.session = get_tm_session(session_factory, transaction.manager) + + def init_database(self): + from .models.meta import Base + Base.metadata.create_all(self.engine) + + def tearDown(self): + from .models.meta import Base + + testing.tearDown() + transaction.abort() + Base.metadata.drop_all(self.engine) + + +class TestMyViewSuccessCondition(BaseTest): + + def setUp(self): + super(TestMyViewSuccessCondition, self).setUp() + self.init_database() + + from .models import MyModel + + model = MyModel(name='one', value=55) + self.session.add(model) + + def test_passing_view(self): + from .views.default import my_view + info = my_view(dummy_request(self.session)) + self.assertEqual(info['one'].name, 'one') + self.assertEqual(info['project'], '{{project}}') + + +class TestMyViewFailureCondition(BaseTest): + + def test_failing_view(self): + from .views.default import my_view + info = my_view(dummy_request(self.session)) + self.assertEqual(info.status_int, 500) diff --git a/src/pyramid/scaffolds/alchemy/+package+/views/__init__.py b/src/pyramid/scaffolds/alchemy/+package+/views/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/pyramid/scaffolds/alchemy/+package+/views/default.py_tmpl b/src/pyramid/scaffolds/alchemy/+package+/views/default.py_tmpl new file mode 100644 index 000000000..7bf0026e5 --- /dev/null +++ b/src/pyramid/scaffolds/alchemy/+package+/views/default.py_tmpl @@ -0,0 +1,33 @@ +from pyramid.response import Response +from pyramid.view import view_config + +from sqlalchemy.exc import DBAPIError + +from ..models import MyModel + + +@view_config(route_name='home', renderer='../templates/mytemplate.jinja2') +def my_view(request): + try: + query = request.dbsession.query(MyModel) + one = query.filter(MyModel.name == 'one').first() + except DBAPIError: + return Response(db_err_msg, content_type='text/plain', status=500) + return {'one': one, 'project': '{{project}}'} + + +db_err_msg = """\ +Pyramid is having a problem using your SQL database. The problem +might be caused by one of the following things: + +1. You may need to run the "initialize_{{project}}_db" script + to initialize your database tables. Check your virtual + environment's "bin" directory for this script and try to run it. + +2. Your database server may not be running. Check that the + database server referred to by the "sqlalchemy.url" setting in + your "development.ini" file is running. + +After you fix the problem, please restart the Pyramid application to +try it again. +""" diff --git a/src/pyramid/scaffolds/alchemy/+package+/views/notfound.py_tmpl b/src/pyramid/scaffolds/alchemy/+package+/views/notfound.py_tmpl new file mode 100644 index 000000000..69d6e2804 --- /dev/null +++ b/src/pyramid/scaffolds/alchemy/+package+/views/notfound.py_tmpl @@ -0,0 +1,7 @@ +from pyramid.view import notfound_view_config + + +@notfound_view_config(renderer='../templates/404.jinja2') +def notfound_view(request): + request.response.status = 404 + return {} diff --git a/src/pyramid/scaffolds/alchemy/CHANGES.txt_tmpl b/src/pyramid/scaffolds/alchemy/CHANGES.txt_tmpl new file mode 100644 index 000000000..35a34f332 --- /dev/null +++ b/src/pyramid/scaffolds/alchemy/CHANGES.txt_tmpl @@ -0,0 +1,4 @@ +0.0 +--- + +- Initial version diff --git a/src/pyramid/scaffolds/alchemy/MANIFEST.in_tmpl b/src/pyramid/scaffolds/alchemy/MANIFEST.in_tmpl new file mode 100644 index 000000000..f93f45544 --- /dev/null +++ b/src/pyramid/scaffolds/alchemy/MANIFEST.in_tmpl @@ -0,0 +1,2 @@ +include *.txt *.ini *.cfg *.rst +recursive-include {{package}} *.ico *.png *.css *.gif *.jpg *.jinja2 *.pt *.txt *.mak *.mako *.js *.html *.xml diff --git a/src/pyramid/scaffolds/alchemy/README.txt_tmpl b/src/pyramid/scaffolds/alchemy/README.txt_tmpl new file mode 100644 index 000000000..83c37edea --- /dev/null +++ b/src/pyramid/scaffolds/alchemy/README.txt_tmpl @@ -0,0 +1,14 @@ +{{project}} README +================== + +Getting Started +--------------- + +- cd + +- $VENV/bin/pip install -e . + +- $VENV/bin/initialize_{{project}}_db development.ini + +- $VENV/bin/pserve development.ini + diff --git a/src/pyramid/scaffolds/alchemy/development.ini_tmpl b/src/pyramid/scaffolds/alchemy/development.ini_tmpl new file mode 100644 index 000000000..3cfb3996d --- /dev/null +++ b/src/pyramid/scaffolds/alchemy/development.ini_tmpl @@ -0,0 +1,69 @@ +### +# app configuration +# https://docs.pylonsproject.org/projects/pyramid/en/{{pyramid_docs_branch}}/narr/environment.html +### + +[app:main] +use = egg:{{project}} + +pyramid.reload_templates = true +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en +pyramid.includes = + pyramid_debugtoolbar + +sqlalchemy.url = sqlite:///%(here)s/{{project}}.sqlite + +# By default, the toolbar only appears for clients from IP addresses +# '127.0.0.1' and '::1'. +# debugtoolbar.hosts = 127.0.0.1 ::1 + +### +# wsgi server configuration +### + +[server:main] +use = egg:waitress#main +listen = localhost:6543 + +### +# logging configuration +# https://docs.pylonsproject.org/projects/pyramid/en/{{pyramid_docs_branch}}/narr/logging.html +### + +[loggers] +keys = root, {{package_logger}}, sqlalchemy + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[logger_{{package_logger}}] +level = DEBUG +handlers = +qualname = {{package}} + +[logger_sqlalchemy] +level = INFO +handlers = +qualname = sqlalchemy.engine +# "level = INFO" logs SQL queries. +# "level = DEBUG" logs SQL queries and results. +# "level = WARN" logs neither. (Recommended for production systems.) + +[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/src/pyramid/scaffolds/alchemy/production.ini_tmpl b/src/pyramid/scaffolds/alchemy/production.ini_tmpl new file mode 100644 index 000000000..043229a71 --- /dev/null +++ b/src/pyramid/scaffolds/alchemy/production.ini_tmpl @@ -0,0 +1,59 @@ +### +# app configuration +# https://docs.pylonsproject.org/projects/pyramid/en/{{pyramid_docs_branch}}/narr/environment.html +### + +[app:main] +use = egg:{{project}} + +pyramid.reload_templates = false +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en + +sqlalchemy.url = sqlite:///%(here)s/{{project}}.sqlite + +[server:main] +use = egg:waitress#main +listen = *:6543 + +### +# logging configuration +# https://docs.pylonsproject.org/projects/pyramid/en/{{pyramid_docs_branch}}/narr/logging.html +### + +[loggers] +keys = root, {{package_logger}}, sqlalchemy + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console + +[logger_{{package_logger}}] +level = WARN +handlers = +qualname = {{package}} + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine +# "level = INFO" logs SQL queries. +# "level = DEBUG" logs SQL queries and results. +# "level = WARN" logs neither. (Recommended for production systems.) + +[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/src/pyramid/scaffolds/alchemy/pytest.ini_tmpl b/src/pyramid/scaffolds/alchemy/pytest.ini_tmpl new file mode 100644 index 000000000..a30c8bcad --- /dev/null +++ b/src/pyramid/scaffolds/alchemy/pytest.ini_tmpl @@ -0,0 +1,3 @@ +[pytest] +testpaths = {{package}} +python_files = *.py diff --git a/src/pyramid/scaffolds/alchemy/setup.py_tmpl b/src/pyramid/scaffolds/alchemy/setup.py_tmpl new file mode 100644 index 000000000..9318817dc --- /dev/null +++ b/src/pyramid/scaffolds/alchemy/setup.py_tmpl @@ -0,0 +1,55 @@ +import os + +from setuptools import setup, find_packages + +here = os.path.abspath(os.path.dirname(__file__)) +with open(os.path.join(here, 'README.txt')) as f: + README = f.read() +with open(os.path.join(here, 'CHANGES.txt')) as f: + CHANGES = f.read() + +requires = [ + 'pyramid', + 'pyramid_jinja2', + 'pyramid_debugtoolbar', + 'pyramid_tm', + 'SQLAlchemy', + 'transaction', + 'zope.sqlalchemy', + 'waitress', + ] + +tests_require = [ + 'WebTest >= 1.3.1', # py3 compat + 'pytest', # includes virtualenv + 'pytest-cov', + ] + +setup(name='{{project}}', + version='0.0', + description='{{project}}', + long_description=README + '\n\n' + CHANGES, + classifiers=[ + "Programming Language :: Python", + "Framework :: Pyramid", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", + ], + author='', + author_email='', + url='', + keywords='web wsgi bfg pylons pyramid', + packages=find_packages(), + include_package_data=True, + zip_safe=False, + extras_require={ + 'testing': tests_require, + }, + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = {{package}}:main + [console_scripts] + initialize_{{project}}_db = {{package}}.scripts.initializedb:main + """, + ) diff --git a/src/pyramid/scaffolds/copydir.py b/src/pyramid/scaffolds/copydir.py new file mode 100644 index 000000000..0914bb0d4 --- /dev/null +++ b/src/pyramid/scaffolds/copydir.py @@ -0,0 +1,301 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste +# (http://pythonpaste.org) Licensed under the MIT license: +# http://www.opensource.org/licenses/mit-license.php + +import os +import sys +import pkg_resources + +from pyramid.compat import ( + input_, + native_, + url_quote as compat_url_quote, + escape, + ) + +fsenc = sys.getfilesystemencoding() + + +class SkipTemplate(Exception): + """ + Raised to indicate that the template should not be copied over. + Raise this exception during the substitution of your template + """ + +def copy_dir(source, dest, vars, verbosity, simulate, indent=0, + sub_vars=True, interactive=False, overwrite=True, + template_renderer=None, out_=sys.stdout): + """ + Copies the ``source`` directory to the ``dest`` directory. + + ``vars``: A dictionary of variables to use in any substitutions. + + ``verbosity``: Higher numbers will show more about what is happening. + + ``simulate``: If true, then don't actually *do* anything. + + ``indent``: Indent any messages by this amount. + + ``sub_vars``: If true, variables in ``_tmpl`` files and ``+var+`` + in filenames will be substituted. + + ``overwrite``: If false, then don't every overwrite anything. + + ``interactive``: If you are overwriting a file and interactive is + true, then ask before overwriting. + + ``template_renderer``: This is a function for rendering templates (if you + don't want to use string.Template). It should have the signature + ``template_renderer(content_as_string, vars_as_dict, + filename=filename)``. + """ + def out(msg): + out_.write(msg) + out_.write('\n') + out_.flush() + # This allows you to use a leading +dot+ in filenames which would + # otherwise be skipped because leading dots make the file hidden: + vars.setdefault('dot', '.') + vars.setdefault('plus', '+') + use_pkg_resources = isinstance(source, tuple) + if use_pkg_resources: + names = sorted(pkg_resources.resource_listdir(source[0], source[1])) + else: + names = sorted(os.listdir(source)) + pad = ' ' * (indent * 2) + if not os.path.exists(dest): + if verbosity >= 1: + out('%sCreating %s/' % (pad, dest)) + if not simulate: + makedirs(dest, verbosity=verbosity, pad=pad) + elif verbosity >= 2: + out('%sDirectory %s exists' % (pad, dest)) + for name in names: + if use_pkg_resources: + full = '/'.join([source[1], name]) + else: + full = os.path.join(source, name) + reason = should_skip_file(name) + if reason: + if verbosity >= 2: + reason = pad + reason % {'filename': full} + out(reason) + continue # pragma: no cover + if sub_vars: + dest_full = os.path.join(dest, substitute_filename(name, vars)) + sub_file = False + if dest_full.endswith('_tmpl'): + dest_full = dest_full[:-5] + sub_file = sub_vars + if use_pkg_resources and pkg_resources.resource_isdir(source[0], full): + if verbosity: + out('%sRecursing into %s' % (pad, os.path.basename(full))) + copy_dir((source[0], full), dest_full, vars, verbosity, simulate, + indent=indent + 1, sub_vars=sub_vars, + interactive=interactive, overwrite=overwrite, + template_renderer=template_renderer, out_=out_) + continue + elif not use_pkg_resources and os.path.isdir(full): + if verbosity: + out('%sRecursing into %s' % (pad, os.path.basename(full))) + copy_dir(full, dest_full, vars, verbosity, simulate, + indent=indent + 1, sub_vars=sub_vars, + interactive=interactive, overwrite=overwrite, + template_renderer=template_renderer, out_=out_) + continue + elif use_pkg_resources: + content = pkg_resources.resource_string(source[0], full) + else: + with open(full, 'rb') as f: + content = f.read() + if sub_file: + try: + content = substitute_content( + content, vars, filename=full, + template_renderer=template_renderer + ) + except SkipTemplate: + continue # pragma: no cover + if content is None: + continue # pragma: no cover + already_exists = os.path.exists(dest_full) + if already_exists: + with open(dest_full, 'rb') as f: + old_content = f.read() + if old_content == content: + if verbosity: + out('%s%s already exists (same content)' % + (pad, dest_full)) + continue # pragma: no cover + if interactive: + if not query_interactive( + native_(full, fsenc), native_(dest_full, fsenc), + native_(content, fsenc), native_(old_content, fsenc), + simulate=simulate, out_=out_): + continue + elif not overwrite: + continue # pragma: no cover + if verbosity and use_pkg_resources: + out('%sCopying %s to %s' % (pad, full, dest_full)) + elif verbosity: + out( + '%sCopying %s to %s' % (pad, os.path.basename(full), + dest_full)) + if not simulate: + with open(dest_full, 'wb') as f: + f.write(content) + +def should_skip_file(name): + """ + Checks if a file should be skipped based on its name. + + If it should be skipped, returns the reason, otherwise returns + None. + """ + if name.startswith('.'): + return 'Skipping hidden file %(filename)s' + if name.endswith(('~', '.bak')): + return 'Skipping backup file %(filename)s' + if name.endswith(('.pyc', '.pyo')): + return 'Skipping %s file ' % os.path.splitext(name)[1] + '%(filename)s' + if name.endswith('$py.class'): + return 'Skipping $py.class file %(filename)s' + if name in ('CVS', '_darcs'): + return 'Skipping version control directory %(filename)s' + return None + +# Overridden on user's request: +all_answer = None + +def query_interactive(src_fn, dest_fn, src_content, dest_content, + simulate, out_=sys.stdout): + def out(msg): + out_.write(msg) + out_.write('\n') + out_.flush() + global all_answer + from difflib import unified_diff, context_diff + u_diff = list(unified_diff( + dest_content.splitlines(), + src_content.splitlines(), + dest_fn, src_fn)) + c_diff = list(context_diff( + dest_content.splitlines(), + src_content.splitlines(), + dest_fn, src_fn)) + added = len([l for l in u_diff if l.startswith('+') and + not l.startswith('+++')]) + removed = len([l for l in u_diff if l.startswith('-') and + not l.startswith('---')]) + if added > removed: + msg = '; %i lines added' % (added - removed) + elif removed > added: + msg = '; %i lines removed' % (removed - added) + else: + msg = '' + out('Replace %i bytes with %i bytes (%i/%i lines changed%s)' % ( + len(dest_content), len(src_content), + removed, len(dest_content.splitlines()), msg)) + prompt = 'Overwrite %s [y/n/d/B/?] ' % dest_fn + while 1: + if all_answer is None: + response = input_(prompt).strip().lower() + else: + response = all_answer + if not response or response[0] == 'b': + import shutil + new_dest_fn = dest_fn + '.bak' + n = 0 + while os.path.exists(new_dest_fn): + n += 1 + new_dest_fn = dest_fn + '.bak' + str(n) + out('Backing up %s to %s' % (dest_fn, new_dest_fn)) + if not simulate: + shutil.copyfile(dest_fn, new_dest_fn) + return True + elif response.startswith('all '): + rest = response[4:].strip() + if not rest or rest[0] not in ('y', 'n', 'b'): + out(query_usage) + continue + response = all_answer = rest[0] + if response[0] == 'y': + return True + elif response[0] == 'n': + return False + elif response == 'dc': + out('\n'.join(c_diff)) + elif response[0] == 'd': + out('\n'.join(u_diff)) + else: + out(query_usage) + +query_usage = """\ +Responses: + Y(es): Overwrite the file with the new content. + N(o): Do not overwrite the file. + D(iff): Show a unified diff of the proposed changes (dc=context diff) + B(ackup): Save the current file contents to a .bak file + (and overwrite) + Type "all Y/N/B" to use Y/N/B for answer to all future questions +""" + +def makedirs(dir, verbosity, pad): + parent = os.path.dirname(os.path.abspath(dir)) + if not os.path.exists(parent): + makedirs(parent, verbosity, pad) # pragma: no cover + os.mkdir(dir) + +def substitute_filename(fn, vars): + for var, value in vars.items(): + fn = fn.replace('+%s+' % var, str(value)) + return fn + +def substitute_content(content, vars, filename='', + template_renderer=None): + v = standard_vars.copy() + v.update(vars) + return template_renderer(content, v, filename=filename) + +def html_quote(s): + if s is None: + return '' + return escape(str(s), 1) + +def url_quote(s): + if s is None: + return '' + return compat_url_quote(str(s)) + +def test(conf, true_cond, false_cond=None): + if conf: + return true_cond + else: + return false_cond + +def skip_template(condition=True, *args): + """ + Raise SkipTemplate, which causes copydir to skip the template + being processed. If you pass in a condition, only raise if that + condition is true (allows you to use this with string.Template) + + If you pass any additional arguments, they will be used to + instantiate SkipTemplate (generally use like + ``skip_template(license=='GPL', 'Skipping file; not using GPL')``) + """ + if condition: + raise SkipTemplate(*args) + +standard_vars = { + 'nothing': None, + 'html_quote': html_quote, + 'url_quote': url_quote, + 'empty': '""', + 'test': test, + 'repr': repr, + 'str': str, + 'bool': bool, + 'SkipTemplate': SkipTemplate, + 'skip_template': skip_template, + } + diff --git a/src/pyramid/scaffolds/starter/+dot+coveragerc_tmpl b/src/pyramid/scaffolds/starter/+dot+coveragerc_tmpl new file mode 100644 index 000000000..273a4a580 --- /dev/null +++ b/src/pyramid/scaffolds/starter/+dot+coveragerc_tmpl @@ -0,0 +1,3 @@ +[run] +source = {{package}} +omit = {{package}}/test* diff --git a/src/pyramid/scaffolds/starter/+package+/__init__.py b/src/pyramid/scaffolds/starter/+package+/__init__.py new file mode 100644 index 000000000..49dde36d4 --- /dev/null +++ b/src/pyramid/scaffolds/starter/+package+/__init__.py @@ -0,0 +1,12 @@ +from pyramid.config import Configurator + + +def main(global_config, **settings): + """ This function returns a Pyramid WSGI application. + """ + config = Configurator(settings=settings) + config.include('pyramid_jinja2') + config.add_static_view('static', 'static', cache_max_age=3600) + config.add_route('home', '/') + config.scan() + return config.make_wsgi_app() diff --git a/src/pyramid/scaffolds/starter/+package+/static/pyramid-16x16.png b/src/pyramid/scaffolds/starter/+package+/static/pyramid-16x16.png new file mode 100644 index 000000000..979203112 Binary files /dev/null and b/src/pyramid/scaffolds/starter/+package+/static/pyramid-16x16.png differ diff --git a/src/pyramid/scaffolds/starter/+package+/static/pyramid.png b/src/pyramid/scaffolds/starter/+package+/static/pyramid.png new file mode 100644 index 000000000..4ab837be9 Binary files /dev/null and b/src/pyramid/scaffolds/starter/+package+/static/pyramid.png differ diff --git a/src/pyramid/scaffolds/starter/+package+/static/theme.css b/src/pyramid/scaffolds/starter/+package+/static/theme.css new file mode 100644 index 000000000..be50ad420 --- /dev/null +++ b/src/pyramid/scaffolds/starter/+package+/static/theme.css @@ -0,0 +1,152 @@ +@import url(//fonts.googleapis.com/css?family=Open+Sans:300,400,600,700); +body { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; + color: #ffffff; + background: #bc2131; +} +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 300; +} +p { + font-weight: 300; +} +.font-normal { + font-weight: 400; +} +.font-semi-bold { + font-weight: 600; +} +.font-bold { + font-weight: 700; +} +.starter-template { + margin-top: 250px; +} +.starter-template .content { + margin-left: 10px; +} +.starter-template .content h1 { + margin-top: 10px; + font-size: 60px; +} +.starter-template .content h1 .smaller { + font-size: 40px; + color: #f2b7bd; +} +.starter-template .content .lead { + font-size: 25px; + color: #f2b7bd; +} +.starter-template .content .lead .font-normal { + color: #ffffff; +} +.starter-template .links { + float: right; + right: 0; + margin-top: 125px; +} +.starter-template .links ul { + display: block; + padding: 0; + margin: 0; +} +.starter-template .links ul li { + list-style: none; + display: inline; + margin: 0 10px; +} +.starter-template .links ul li:first-child { + margin-left: 0; +} +.starter-template .links ul li:last-child { + margin-right: 0; +} +.starter-template .links ul li.current-version { + color: #f2b7bd; + font-weight: 400; +} +.starter-template .links ul li a { + color: #ffffff; +} +.starter-template .links ul li a:hover { + text-decoration: underline; +} +.starter-template .links ul li .icon-muted { + color: #eb8b95; + margin-right: 5px; +} +.starter-template .links ul li:hover .icon-muted { + color: #ffffff; +} +.starter-template .copyright { + margin-top: 10px; + font-size: 0.9em; + color: #f2b7bd; + text-transform: lowercase; + float: right; + right: 0; +} +@media (max-width: 1199px) { + .starter-template .content h1 { + font-size: 45px; + } + .starter-template .content h1 .smaller { + font-size: 30px; + } + .starter-template .content .lead { + font-size: 20px; + } +} +@media (max-width: 991px) { + .starter-template { + margin-top: 0; + } + .starter-template .logo { + margin: 40px auto; + } + .starter-template .content { + margin-left: 0; + text-align: center; + } + .starter-template .content h1 { + margin-bottom: 20px; + } + .starter-template .links { + float: none; + text-align: center; + margin-top: 60px; + } + .starter-template .copyright { + float: none; + text-align: center; + } +} +@media (max-width: 767px) { + .starter-template .content h1 .smaller { + font-size: 25px; + display: block; + } + .starter-template .content .lead { + font-size: 16px; + } + .starter-template .links { + margin-top: 40px; + } + .starter-template .links ul li { + display: block; + margin: 0; + } + .starter-template .links ul li .icon-muted { + display: none; + } + .starter-template .copyright { + margin-top: 20px; + } +} diff --git a/src/pyramid/scaffolds/starter/+package+/templates/layout.jinja2_tmpl b/src/pyramid/scaffolds/starter/+package+/templates/layout.jinja2_tmpl new file mode 100644 index 000000000..54baf7a2a --- /dev/null +++ b/src/pyramid/scaffolds/starter/+package+/templates/layout.jinja2_tmpl @@ -0,0 +1,66 @@ + + + + + + + + + + + Starter Scaffold for The Pyramid Web Framework + + + + + + + + + + + + + +
+
+
+
+ +
+
+ {% block content %} +

No content

+ {% endblock content %} +
+
+
+ +
+
+ +
+
+
+ + + + + + + + diff --git a/src/pyramid/scaffolds/starter/+package+/templates/mytemplate.jinja2_tmpl b/src/pyramid/scaffolds/starter/+package+/templates/mytemplate.jinja2_tmpl new file mode 100644 index 000000000..f826ff9e7 --- /dev/null +++ b/src/pyramid/scaffolds/starter/+package+/templates/mytemplate.jinja2_tmpl @@ -0,0 +1,8 @@ +{% extends "layout.jinja2" %} + +{% block content%} +
+

Pyramid Starter scaffold

+

Welcome to \{\{project\}\}, an application generated by
the Pyramid Web Framework {{pyramid_version}}.

+
+{% endblock content %} diff --git a/src/pyramid/scaffolds/starter/+package+/tests.py_tmpl b/src/pyramid/scaffolds/starter/+package+/tests.py_tmpl new file mode 100644 index 000000000..30f3f0430 --- /dev/null +++ b/src/pyramid/scaffolds/starter/+package+/tests.py_tmpl @@ -0,0 +1,29 @@ +import unittest + +from pyramid import testing + + +class ViewTests(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def test_my_view(self): + from .views import my_view + request = testing.DummyRequest() + info = my_view(request) + self.assertEqual(info['project'], '{{project}}') + + +class FunctionalTests(unittest.TestCase): + def setUp(self): + from {{package}} import main + app = main({}) + from webtest import TestApp + self.testapp = TestApp(app) + + def test_root(self): + res = self.testapp.get('/', status=200) + self.assertTrue(b'Pyramid' in res.body) diff --git a/src/pyramid/scaffolds/starter/+package+/views.py_tmpl b/src/pyramid/scaffolds/starter/+package+/views.py_tmpl new file mode 100644 index 000000000..01b9d0130 --- /dev/null +++ b/src/pyramid/scaffolds/starter/+package+/views.py_tmpl @@ -0,0 +1,6 @@ +from pyramid.view import view_config + + +@view_config(route_name='home', renderer='templates/mytemplate.jinja2') +def my_view(request): + return {'project': '{{project}}'} diff --git a/src/pyramid/scaffolds/starter/CHANGES.txt_tmpl b/src/pyramid/scaffolds/starter/CHANGES.txt_tmpl new file mode 100644 index 000000000..35a34f332 --- /dev/null +++ b/src/pyramid/scaffolds/starter/CHANGES.txt_tmpl @@ -0,0 +1,4 @@ +0.0 +--- + +- Initial version diff --git a/src/pyramid/scaffolds/starter/MANIFEST.in_tmpl b/src/pyramid/scaffolds/starter/MANIFEST.in_tmpl new file mode 100644 index 000000000..4d1c86b44 --- /dev/null +++ b/src/pyramid/scaffolds/starter/MANIFEST.in_tmpl @@ -0,0 +1,2 @@ +include *.txt *.ini *.cfg *.rst +recursive-include {{package}} *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml *.jinja2 diff --git a/src/pyramid/scaffolds/starter/README.txt_tmpl b/src/pyramid/scaffolds/starter/README.txt_tmpl new file mode 100644 index 000000000..127ad7595 --- /dev/null +++ b/src/pyramid/scaffolds/starter/README.txt_tmpl @@ -0,0 +1,12 @@ +{{project}} README +================== + +Getting Started +--------------- + +- cd + +- $VENV/bin/pip install -e . + +- $VENV/bin/pserve development.ini + diff --git a/src/pyramid/scaffolds/starter/development.ini_tmpl b/src/pyramid/scaffolds/starter/development.ini_tmpl new file mode 100644 index 000000000..c6e42d97c --- /dev/null +++ b/src/pyramid/scaffolds/starter/development.ini_tmpl @@ -0,0 +1,59 @@ +### +# app configuration +# https://docs.pylonsproject.org/projects/pyramid/en/{{pyramid_docs_branch}}/narr/environment.html +### + +[app:main] +use = egg:{{project}} + +pyramid.reload_templates = true +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en +pyramid.includes = + pyramid_debugtoolbar + +# By default, the toolbar only appears for clients from IP addresses +# '127.0.0.1' and '::1'. +# debugtoolbar.hosts = 127.0.0.1 ::1 + +### +# wsgi server configuration +### + +[server:main] +use = egg:waitress#main +listen = localhost:6543 + +### +# logging configuration +# https://docs.pylonsproject.org/projects/pyramid/en/{{pyramid_docs_branch}}/narr/logging.html +### + +[loggers] +keys = root, {{package_logger}} + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[logger_{{package_logger}}] +level = DEBUG +handlers = +qualname = {{package}} + +[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/src/pyramid/scaffolds/starter/production.ini_tmpl b/src/pyramid/scaffolds/starter/production.ini_tmpl new file mode 100644 index 000000000..1107a6b2f --- /dev/null +++ b/src/pyramid/scaffolds/starter/production.ini_tmpl @@ -0,0 +1,53 @@ +### +# app configuration +# https://docs.pylonsproject.org/projects/pyramid/en/{{pyramid_docs_branch}}/narr/environment.html +### + +[app:main] +use = egg:{{project}} + +pyramid.reload_templates = false +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en + +### +# wsgi server configuration +### + +[server:main] +use = egg:waitress#main +listen = *:6543 + +### +# logging configuration +# https://docs.pylonsproject.org/projects/pyramid/en/{{pyramid_docs_branch}}/narr/logging.html +### + +[loggers] +keys = root, {{package_logger}} + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console + +[logger_{{package_logger}}] +level = WARN +handlers = +qualname = {{package}} + +[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/src/pyramid/scaffolds/starter/pytest.ini_tmpl b/src/pyramid/scaffolds/starter/pytest.ini_tmpl new file mode 100644 index 000000000..a30c8bcad --- /dev/null +++ b/src/pyramid/scaffolds/starter/pytest.ini_tmpl @@ -0,0 +1,3 @@ +[pytest] +testpaths = {{package}} +python_files = *.py diff --git a/src/pyramid/scaffolds/starter/setup.py_tmpl b/src/pyramid/scaffolds/starter/setup.py_tmpl new file mode 100644 index 000000000..7f50bbbc2 --- /dev/null +++ b/src/pyramid/scaffolds/starter/setup.py_tmpl @@ -0,0 +1,49 @@ +import os + +from setuptools import setup, find_packages + +here = os.path.abspath(os.path.dirname(__file__)) +with open(os.path.join(here, 'README.txt')) as f: + README = f.read() +with open(os.path.join(here, 'CHANGES.txt')) as f: + CHANGES = f.read() + +requires = [ + 'pyramid', + 'pyramid_jinja2', + 'pyramid_debugtoolbar', + 'waitress', + ] + +tests_require = [ + 'WebTest >= 1.3.1', # py3 compat + 'pytest', # includes virtualenv + 'pytest-cov', + ] + +setup(name='{{project}}', + version='0.0', + description='{{project}}', + long_description=README + '\n\n' + CHANGES, + classifiers=[ + "Programming Language :: Python", + "Framework :: Pyramid", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", + ], + author='', + author_email='', + url='', + keywords='web pyramid pylons', + packages=find_packages(), + include_package_data=True, + zip_safe=False, + extras_require={ + 'testing': tests_require, + }, + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = {{package}}:main + """, + ) diff --git a/src/pyramid/scaffolds/template.py b/src/pyramid/scaffolds/template.py new file mode 100644 index 000000000..e5098e815 --- /dev/null +++ b/src/pyramid/scaffolds/template.py @@ -0,0 +1,172 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste +# (http://pythonpaste.org) Licensed under the MIT license: +# http://www.opensource.org/licenses/mit-license.php + +import re +import sys +import os + +from pyramid.compat import ( + native_, + bytes_, + ) + +from pyramid.scaffolds import copydir + +fsenc = sys.getfilesystemencoding() + +class Template(object): + """ Inherit from this base class and override methods to use the Pyramid + scaffolding system.""" + copydir = copydir # for testing + _template_dir = None + + def __init__(self, name): + self.name = name + + def render_template(self, content, vars, filename=None): + """ Return a bytestring representing a templated file based on the + input (content) and the variable names defined (vars). ``filename`` + is used for exception reporting.""" + # this method must not be named "template_renderer" fbo of extension + # scaffolds that need to work under pyramid 1.2 and 1.3, and which + # need to do "template_renderer = + # staticmethod(paste_script_template_renderer)" + content = native_(content, fsenc) + try: + return bytes_( + substitute_escaped_double_braces( + substitute_double_braces(content, TypeMapper(vars))), fsenc) + except Exception as e: + _add_except(e, ' in file %s' % filename) + raise + + def module_dir(self): + mod = sys.modules[self.__class__.__module__] + return os.path.dirname(mod.__file__) + + def template_dir(self): + """ Return the template directory of the scaffold. By default, it + returns the value of ``os.path.join(self.module_dir(), + self._template_dir)`` (``self.module_dir()`` returns the module in + which your subclass has been defined). If ``self._template_dir`` is + a tuple this method just returns the value instead of trying to + construct a path. If _template_dir is a tuple, it should be a + 2-element tuple: ``(package_name, package_relative_path)``.""" + assert self._template_dir is not None, ( + "Template %r didn't set _template_dir" % self) + if isinstance(self._template_dir, tuple): + return self._template_dir + else: + return os.path.join(self.module_dir(), self._template_dir) + + def run(self, command, output_dir, vars): + self.pre(command, output_dir, vars) + self.write_files(command, output_dir, vars) + self.post(command, output_dir, vars) + + def pre(self, command, output_dir, vars): # pragma: no cover + """ + Called before template is applied. + """ + pass + + def post(self, command, output_dir, vars): # pragma: no cover + """ + Called after template is applied. + """ + pass + + def write_files(self, command, output_dir, vars): + template_dir = self.template_dir() + if not self.exists(output_dir): + self.out("Creating directory %s" % output_dir) + if not command.args.simulate: + # Don't let copydir create this top-level directory, + # since copydir will svn add it sometimes: + self.makedirs(output_dir) + self.copydir.copy_dir( + template_dir, + output_dir, + vars, + verbosity=command.verbosity, + simulate=command.args.simulate, + interactive=command.args.interactive, + overwrite=command.args.overwrite, + indent=1, + template_renderer=self.render_template, + ) + + def makedirs(self, dir): # pragma: no cover + return os.makedirs(dir) + + def exists(self, path): # pragma: no cover + return os.path.exists(path) + + def out(self, msg): # pragma: no cover + print(msg) + + # hair for exit with usage when paster create is used under 1.3 instead + # of pcreate for extension scaffolds which need to support multiple + # versions of pyramid; the check_vars method is called by pastescript + # only as the result of "paster create"; pyramid doesn't use it. the + # required_templates tuple is required to allow it to get as far as + # calling check_vars. + required_templates = () + def check_vars(self, vars, other): + raise RuntimeError( + 'Under Pyramid 1.3, you should use the "pcreate" command rather ' + 'than "paster create"') + +class TypeMapper(dict): + + def __getitem__(self, item): + options = item.split('|') + for op in options[:-1]: + try: + value = eval_with_catch(op, dict(self.items())) + break + except (NameError, KeyError): + pass + else: + value = eval(options[-1], dict(self.items())) + if value is None: + return '' + else: + return str(value) + +def eval_with_catch(expr, vars): + try: + return eval(expr, vars) + except Exception as e: + _add_except(e, 'in expression %r' % expr) + raise + +double_brace_pattern = re.compile(r'{{(?P.*?)}}') + +def substitute_double_braces(content, values): + def double_bracerepl(match): + value = match.group('braced').strip() + return values[value] + return double_brace_pattern.sub(double_bracerepl, content) + +escaped_double_brace_pattern = re.compile(r'\\{\\{(?P[^\\]*?)\\}\\}') + +def substitute_escaped_double_braces(content): + def escaped_double_bracerepl(match): + value = match.group('escape_braced').strip() + return "{{%(value)s}}" % locals() + return escaped_double_brace_pattern.sub(escaped_double_bracerepl, content) + +def _add_except(exc, info): # pragma: no cover + if not hasattr(exc, 'args') or exc.args is None: + return + args = list(exc.args) + if args: + args[0] += ' ' + info + else: + args = [info] + exc.args = tuple(args) + return + + diff --git a/src/pyramid/scaffolds/tests.py b/src/pyramid/scaffolds/tests.py new file mode 100644 index 000000000..44680a464 --- /dev/null +++ b/src/pyramid/scaffolds/tests.py @@ -0,0 +1,75 @@ +import sys +import os +import shutil +import subprocess +import tempfile +import time + +try: + import http.client as httplib +except ImportError: + import httplib + + +class TemplateTest(object): + def make_venv(self, directory): # pragma: no cover + import virtualenv + from virtualenv import Logger + logger = Logger([(Logger.level_for_integer(2), sys.stdout)]) + virtualenv.logger = logger + virtualenv.create_environment(directory, + site_packages=False, + clear=False, + unzip_setuptools=True) + + def install(self, tmpl_name): # pragma: no cover + try: + self.old_cwd = os.getcwd() + self.directory = tempfile.mkdtemp() + self.make_venv(self.directory) + here = os.path.abspath(os.path.dirname(__file__)) + os.chdir(os.path.dirname(os.path.dirname(here))) + pip = os.path.join(self.directory, 'bin', 'pip') + subprocess.check_call([pip, 'install', '-e', '.']) + os.chdir(self.directory) + subprocess.check_call(['bin/pcreate', '-s', tmpl_name, 'Dingle']) + os.chdir('Dingle') + subprocess.check_call([pip, 'install', '.[testing]']) + if tmpl_name == 'alchemy': + populate = os.path.join(self.directory, 'bin', + 'initialize_Dingle_db') + subprocess.check_call([populate, 'development.ini']) + subprocess.check_call([ + os.path.join(self.directory, 'bin', 'py.test')]) + pserve = os.path.join(self.directory, 'bin', 'pserve') + for ininame, hastoolbar in (('development.ini', True), + ('production.ini', False)): + proc = subprocess.Popen([pserve, ininame]) + try: + time.sleep(5) + proc.poll() + if proc.returncode is not None: + raise RuntimeError('%s didnt start' % ininame) + conn = httplib.HTTPConnection('localhost:6543') + conn.request('GET', '/') + resp = conn.getresponse() + assert resp.status == 200, ininame + data = resp.read() + toolbarchunk = b'
+ + + + + + + + + + ZODB Scaffold for The Pyramid Web Framework + + + + + + + + + + + + + +
+
+
+
+ +
+
+
+

Pyramid ZODB scaffold

+

Welcome to ${project}, an application generated by
the Pyramid Web Framework {{pyramid_version}}.

+
+
+
+
+ +
+
+ +
+
+
+ + + + + + + + diff --git a/src/pyramid/scaffolds/zodb/+package+/tests.py_tmpl b/src/pyramid/scaffolds/zodb/+package+/tests.py_tmpl new file mode 100644 index 000000000..94912a850 --- /dev/null +++ b/src/pyramid/scaffolds/zodb/+package+/tests.py_tmpl @@ -0,0 +1,17 @@ +import unittest + +from pyramid import testing + + +class ViewTests(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def test_my_view(self): + from .views import my_view + request = testing.DummyRequest() + info = my_view(request) + self.assertEqual(info['project'], '{{project}}') diff --git a/src/pyramid/scaffolds/zodb/+package+/views.py_tmpl b/src/pyramid/scaffolds/zodb/+package+/views.py_tmpl new file mode 100644 index 000000000..1e8a9b65a --- /dev/null +++ b/src/pyramid/scaffolds/zodb/+package+/views.py_tmpl @@ -0,0 +1,7 @@ +from pyramid.view import view_config +from .models import MyModel + + +@view_config(context=MyModel, renderer='templates/mytemplate.pt') +def my_view(request): + return {'project': '{{project}}'} diff --git a/src/pyramid/scaffolds/zodb/CHANGES.txt_tmpl b/src/pyramid/scaffolds/zodb/CHANGES.txt_tmpl new file mode 100644 index 000000000..35a34f332 --- /dev/null +++ b/src/pyramid/scaffolds/zodb/CHANGES.txt_tmpl @@ -0,0 +1,4 @@ +0.0 +--- + +- Initial version diff --git a/src/pyramid/scaffolds/zodb/MANIFEST.in_tmpl b/src/pyramid/scaffolds/zodb/MANIFEST.in_tmpl new file mode 100644 index 000000000..0ff6eb7a0 --- /dev/null +++ b/src/pyramid/scaffolds/zodb/MANIFEST.in_tmpl @@ -0,0 +1,2 @@ +include *.txt *.ini *.cfg *.rst +recursive-include {{package}} *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml diff --git a/src/pyramid/scaffolds/zodb/README.txt_tmpl b/src/pyramid/scaffolds/zodb/README.txt_tmpl new file mode 100644 index 000000000..127ad7595 --- /dev/null +++ b/src/pyramid/scaffolds/zodb/README.txt_tmpl @@ -0,0 +1,12 @@ +{{project}} README +================== + +Getting Started +--------------- + +- cd + +- $VENV/bin/pip install -e . + +- $VENV/bin/pserve development.ini + diff --git a/src/pyramid/scaffolds/zodb/development.ini_tmpl b/src/pyramid/scaffolds/zodb/development.ini_tmpl new file mode 100644 index 000000000..7d898bcd4 --- /dev/null +++ b/src/pyramid/scaffolds/zodb/development.ini_tmpl @@ -0,0 +1,64 @@ +### +# app configuration +# https://docs.pylonsproject.org/projects/pyramid/en/{{pyramid_docs_branch}}/narr/environment.html +### + +[app:main] +use = egg:{{project}} + +pyramid.reload_templates = true +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en +pyramid.includes = + pyramid_debugtoolbar + pyramid_zodbconn + pyramid_tm + +tm.attempts = 3 +zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000 + +# By default, the toolbar only appears for clients from IP addresses +# '127.0.0.1' and '::1'. +# debugtoolbar.hosts = 127.0.0.1 ::1 + +### +# wsgi server configuration +### + +[server:main] +use = egg:waitress#main +listen = localhost:6543 + +### +# logging configuration +# https://docs.pylonsproject.org/projects/pyramid/en/{{pyramid_docs_branch}}/narr/logging.html +### + +[loggers] +keys = root, {{package_logger}} + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[logger_{{package_logger}}] +level = DEBUG +handlers = +qualname = {{package}} + +[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/src/pyramid/scaffolds/zodb/production.ini_tmpl b/src/pyramid/scaffolds/zodb/production.ini_tmpl new file mode 100644 index 000000000..7c2e90c2e --- /dev/null +++ b/src/pyramid/scaffolds/zodb/production.ini_tmpl @@ -0,0 +1,59 @@ +### +# app configuration +# https://docs.pylonsproject.org/projects/pyramid/en/{{pyramid_docs_branch}}/narr/environment.html +### + +[app:main] +use = egg:{{project}} + +pyramid.reload_templates = false +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.default_locale_name = en +pyramid.includes = + pyramid_tm + pyramid_zodbconn + +tm.attempts = 3 +zodbconn.uri = file://%(here)s/Data.fs?connection_cache_size=20000 + +### +# wsgi server configuration +### + +[server:main] +use = egg:waitress#main +listen = *:6543 + +### +# logging configuration +# https://docs.pylonsproject.org/projects/pyramid/en/{{pyramid_docs_branch}}/narr/logging.html +### + +[loggers] +keys = root, {{package_logger}} + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console + +[logger_{{package_logger}}] +level = WARN +handlers = +qualname = {{package}} + +[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/src/pyramid/scaffolds/zodb/pytest.ini_tmpl b/src/pyramid/scaffolds/zodb/pytest.ini_tmpl new file mode 100644 index 000000000..a30c8bcad --- /dev/null +++ b/src/pyramid/scaffolds/zodb/pytest.ini_tmpl @@ -0,0 +1,3 @@ +[pytest] +testpaths = {{package}} +python_files = *.py diff --git a/src/pyramid/scaffolds/zodb/setup.py_tmpl b/src/pyramid/scaffolds/zodb/setup.py_tmpl new file mode 100644 index 000000000..19771d756 --- /dev/null +++ b/src/pyramid/scaffolds/zodb/setup.py_tmpl @@ -0,0 +1,53 @@ +import os + +from setuptools import setup, find_packages + +here = os.path.abspath(os.path.dirname(__file__)) +with open(os.path.join(here, 'README.txt')) as f: + README = f.read() +with open(os.path.join(here, 'CHANGES.txt')) as f: + CHANGES = f.read() + +requires = [ + 'pyramid', + 'pyramid_chameleon', + 'pyramid_debugtoolbar', + 'pyramid_tm', + 'pyramid_zodbconn', + 'transaction', + 'ZODB3', + 'waitress', + ] + +tests_require = [ + 'WebTest >= 1.3.1', # py3 compat + 'pytest', # includes virtualenv + 'pytest-cov', + ] + +setup(name='{{project}}', + version='0.0', + description='{{project}}', + long_description=README + '\n\n' + CHANGES, + classifiers=[ + "Programming Language :: Python", + "Framework :: Pyramid", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", + ], + author='', + author_email='', + url='', + keywords='web pylons pyramid', + packages=find_packages(), + include_package_data=True, + zip_safe=False, + extras_require={ + 'testing': tests_require, + }, + install_requires=requires, + entry_points="""\ + [paste.app_factory] + main = {{package}}:main + """, + ) diff --git a/src/pyramid/scripting.py b/src/pyramid/scripting.py new file mode 100644 index 000000000..087b55ccb --- /dev/null +++ b/src/pyramid/scripting.py @@ -0,0 +1,141 @@ +from pyramid.config import global_registries +from pyramid.exceptions import ConfigurationError + +from pyramid.interfaces import ( + IRequestFactory, + IRootFactory, + ) +from pyramid.request import Request +from pyramid.request import apply_request_extensions + +from pyramid.threadlocal import RequestContext +from pyramid.traversal import DefaultRootFactory + +def get_root(app, request=None): + """ Return a tuple composed of ``(root, closer)`` when provided a + :term:`router` instance as the ``app`` argument. The ``root`` + returned is the application root object. The ``closer`` returned + is a callable (accepting no arguments) that should be called when + your scripting application is finished using the root. + + ``request`` is passed to the :app:`Pyramid` application root + factory to compute the root. If ``request`` is None, a default + will be constructed using the registry's :term:`Request Factory` + via the :meth:`pyramid.interfaces.IRequestFactory.blank` method. + """ + registry = app.registry + if request is None: + request = _make_request('/', registry) + request.registry = registry + ctx = RequestContext(request) + ctx.begin() + def closer(): + ctx.end() + root = app.root_factory(request) + return root, closer + +def prepare(request=None, registry=None): + """ This function pushes data onto the Pyramid threadlocal stack + (request and registry), making those objects 'current'. It + returns a dictionary useful for bootstrapping a Pyramid + application in a scripting environment. + + ``request`` is passed to the :app:`Pyramid` application root + factory to compute the root. If ``request`` is None, a default + will be constructed using the registry's :term:`Request Factory` + via the :meth:`pyramid.interfaces.IRequestFactory.blank` method. + + If ``registry`` is not supplied, the last registry loaded from + :attr:`pyramid.config.global_registries` will be used. If you + have loaded more than one :app:`Pyramid` application in the + current process, you may not want to use the last registry + loaded, thus you can search the ``global_registries`` and supply + the appropriate one based on your own criteria. + + The function returns a dictionary composed of ``root``, + ``closer``, ``registry``, ``request`` and ``root_factory``. The + ``root`` returned is the application's root resource object. The + ``closer`` returned is a callable (accepting no arguments) that + should be called when your scripting application is finished + using the root. ``registry`` is the resolved registry object. + ``request`` is the request object passed or the constructed request + if no request is passed. ``root_factory`` is the root factory used + to construct the root. + + This function may be used as a context manager to call the ``closer`` + automatically: + + .. code-block:: python + + registry = config.registry + with prepare(registry) as env: + request = env['request'] + # ... + + .. versionchanged:: 1.8 + + Added the ability to use the return value as a context manager. + + """ + if registry is None: + registry = getattr(request, 'registry', global_registries.last) + if registry is None: + raise ConfigurationError('No valid Pyramid applications could be ' + 'found, make sure one has been created ' + 'before trying to activate it.') + if request is None: + request = _make_request('/', registry) + # NB: even though _make_request might have already set registry on + # request, we reset it in case someone has passed in their own + # request. + request.registry = registry + ctx = RequestContext(request) + ctx.begin() + apply_request_extensions(request) + def closer(): + ctx.end() + root_factory = registry.queryUtility(IRootFactory, + default=DefaultRootFactory) + root = root_factory(request) + if getattr(request, 'context', None) is None: + request.context = root + return AppEnvironment( + root=root, + closer=closer, + registry=registry, + request=request, + root_factory=root_factory, + ) + +class AppEnvironment(dict): + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + self['closer']() + +def _make_request(path, registry=None): + """ Return a :meth:`pyramid.request.Request` object anchored at a + given path. The object returned will be generated from the supplied + registry's :term:`Request Factory` using the + :meth:`pyramid.interfaces.IRequestFactory.blank` method. + + This request object can be passed to :meth:`pyramid.scripting.get_root` + or :meth:`pyramid.scripting.prepare` to initialize an application in + preparation for executing a script with a proper environment setup. + URLs can then be generated with the object, as well as rendering + templates. + + If ``registry`` is not supplied, the last registry loaded from + :attr:`pyramid.config.global_registries` will be used. If you have + loaded more than one :app:`Pyramid` application in the current + process, you may not want to use the last registry loaded, thus + you can search the ``global_registries`` and supply the appropriate + one based on your own criteria. + """ + if registry is None: + registry = global_registries.last + request_factory = registry.queryUtility(IRequestFactory, default=Request) + request = request_factory.blank(path) + request.registry = registry + return request diff --git a/src/pyramid/scripts/__init__.py b/src/pyramid/scripts/__init__.py new file mode 100644 index 000000000..5bb534f79 --- /dev/null +++ b/src/pyramid/scripts/__init__.py @@ -0,0 +1 @@ +# package diff --git a/src/pyramid/scripts/common.py b/src/pyramid/scripts/common.py new file mode 100644 index 000000000..f4b8027db --- /dev/null +++ b/src/pyramid/scripts/common.py @@ -0,0 +1,23 @@ +import plaster + +def parse_vars(args): + """ + Given variables like ``['a=b', 'c=d']`` turns it into ``{'a': + 'b', 'c': 'd'}`` + """ + result = {} + for arg in args: + if '=' not in arg: + raise ValueError( + 'Variable assignment %r invalid (no "=")' + % arg) + name, value = arg.split('=', 1) + result[name] = value + return result + +def get_config_loader(config_uri): + """ + Find a ``plaster.ILoader`` object supporting the "wsgi" protocol. + + """ + return plaster.get_loader(config_uri, protocols=['wsgi']) diff --git a/src/pyramid/scripts/pcreate.py b/src/pyramid/scripts/pcreate.py new file mode 100644 index 000000000..a6db520ce --- /dev/null +++ b/src/pyramid/scripts/pcreate.py @@ -0,0 +1,251 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste +# (http://pythonpaste.org) Licensed under the MIT license: +# http://www.opensource.org/licenses/mit-license.php + +import argparse +import os +import os.path +import pkg_resources +import re +import sys +from pyramid.compat import input_ + +_bad_chars_re = re.compile('[^a-zA-Z0-9_]') + + +def main(argv=sys.argv, quiet=False): + command = PCreateCommand(argv, quiet) + try: + return command.run() + except KeyboardInterrupt: # pragma: no cover + return 1 + + +class PCreateCommand(object): + verbosity = 1 # required + parser = argparse.ArgumentParser( + 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 +""", + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument('-s', '--scaffold', + dest='scaffold_name', + action='append', + help=("Add a scaffold to the create process " + "(multiple -s args accepted)")) + parser.add_argument('-t', '--template', + dest='scaffold_name', + action='append', + help=('A backwards compatibility alias for ' + '-s/--scaffold. Add a scaffold to the ' + 'create process (multiple -t args accepted)')) + parser.add_argument('-l', '--list', + dest='list', + action='store_true', + help="List all available scaffold names") + parser.add_argument('--list-templates', + dest='list', + action='store_true', + help=("A backwards compatibility alias for -l/--list. " + "List all available scaffold names.")) + parser.add_argument('--package-name', + dest='package_name', + action='store', + help='Package name to use. The name provided is ' + 'assumed to be a valid Python package name, and ' + 'will not be validated. By default the package ' + 'name is derived from the value of ' + 'output_directory.') + parser.add_argument('--simulate', + dest='simulate', + action='store_true', + help='Simulate but do no work') + parser.add_argument('--overwrite', + dest='overwrite', + action='store_true', + help='Always overwrite') + parser.add_argument('--interactive', + dest='interactive', + action='store_true', + help='When a file would be overwritten, interrogate ' + '(this is the default, but you may specify it to ' + 'override --overwrite)') + parser.add_argument('--ignore-conflicting-name', + dest='force_bad_name', + action='store_true', + default=False, + help='Do create a project even if the chosen name ' + 'is the name of an already existing / importable ' + 'package.') + parser.add_argument('output_directory', + nargs='?', + default=None, + help='The directory where the project will be ' + 'created.') + + pyramid_dist = pkg_resources.get_distribution("pyramid") + + def __init__(self, argv, quiet=False): + self.quiet = quiet + self.args = self.parser.parse_args(argv[1:]) + if not self.args.interactive and not self.args.overwrite: + self.args.interactive = True + self.scaffolds = self.all_scaffolds() + + def run(self): + if self.args.list: + return self.show_scaffolds() + if not self.args.scaffold_name and not self.args.output_directory: + if not self.quiet: # pragma: no cover + self.parser.print_help() + self.out('') + self.show_scaffolds() + return 2 + + if not self.validate_input(): + return 2 + self._warn_pcreate_deprecated() + + return self.render_scaffolds() + + @property + def output_path(self): + return os.path.abspath(os.path.normpath(self.args.output_directory)) + + @property + def project_vars(self): + output_dir = self.output_path + project_name = os.path.basename(os.path.split(output_dir)[1]) + if self.args.package_name is None: + pkg_name = _bad_chars_re.sub( + '', project_name.lower().replace('-', '_')) + safe_name = pkg_resources.safe_name(project_name) + else: + pkg_name = self.args.package_name + safe_name = pkg_name + egg_name = pkg_resources.to_filename(safe_name) + + # get pyramid package version + pyramid_version = self.pyramid_dist.version + + # map pyramid package version of the documentation branch ## + # if version ends with 'dev' then docs version is 'master' + if self.pyramid_dist.version[-3:] == 'dev': + pyramid_docs_branch = 'master' + else: + # if not version is not 'dev' find the version.major_version string + # and combine it with '-branch' + version_match = re.match(r'(\d+\.\d+)', self.pyramid_dist.version) + if version_match is not None: + pyramid_docs_branch = "%s-branch" % version_match.group() + # if can not parse the version then default to 'latest' + else: + pyramid_docs_branch = 'latest' + + return { + 'project': project_name, + 'package': pkg_name, + 'egg': egg_name, + 'pyramid_version': pyramid_version, + 'pyramid_docs_branch': pyramid_docs_branch, + } + + def render_scaffolds(self): + props = self.project_vars + output_dir = self.output_path + for scaffold_name in self.args.scaffold_name: + for scaffold in self.scaffolds: + if scaffold.name == scaffold_name: + scaffold.run(self, output_dir, props) + return 0 + + def show_scaffolds(self): + scaffolds = sorted(self.scaffolds, key=lambda x: x.name) + if scaffolds: + max_name = max([len(t.name) for t in scaffolds]) + self.out('Available scaffolds:') + for scaffold in scaffolds: + self.out(' %s:%s %s' % ( + scaffold.name, + ' ' * (max_name - len(scaffold.name)), scaffold.summary)) + else: + self.out('No scaffolds available') + return 0 + + def all_scaffolds(self): + scaffolds = [] + eps = list(pkg_resources.iter_entry_points('pyramid.scaffold')) + for entry in eps: + try: + scaffold_class = entry.load() + scaffold = scaffold_class(entry.name) + scaffolds.append(scaffold) + except Exception as e: # pragma: no cover + self.out('Warning: could not load entry point %s (%s: %s)' % ( + entry.name, e.__class__.__name__, e)) + return scaffolds + + def out(self, msg): # pragma: no cover + if not self.quiet: + print(msg) + + def validate_input(self): + if not self.args.scaffold_name: + self.out('You must provide at least one scaffold name: ' + '-s ') + self.out('') + self.show_scaffolds() + return False + if not self.args.output_directory: + self.out('You must provide a project name') + return False + available = [x.name for x in self.scaffolds] + diff = set(self.args.scaffold_name).difference(available) + if diff: + self.out('Unavailable scaffolds: %s' % ", ".join(sorted(diff))) + return False + + pkg_name = self.project_vars['package'] + + if pkg_name == 'site' and not self.args.force_bad_name: + self.out('The package name "site" has a special meaning in ' + 'Python. Are you sure you want to use it as your ' + 'project\'s name?') + return self.confirm_bad_name('Really use "{0}"?: '.format( + pkg_name)) + + # check if pkg_name can be imported (i.e. already exists in current + # $PYTHON_PATH, if so - let the user confirm + pkg_exists = True + try: + # use absolute imports + __import__(pkg_name, globals(), locals(), [], 0) + except ImportError as error: + pkg_exists = False + if not pkg_exists: + return True + + if self.args.force_bad_name: + return True + self.out('A package named "{0}" already exists, are you sure you want ' + 'to use it as your project\'s name?'.format(pkg_name)) + return self.confirm_bad_name('Really use "{0}"?: '.format(pkg_name)) + + def confirm_bad_name(self, prompt): # pragma: no cover + 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/src/pyramid/scripts/pdistreport.py b/src/pyramid/scripts/pdistreport.py new file mode 100644 index 000000000..1952e5d39 --- /dev/null +++ b/src/pyramid/scripts/pdistreport.py @@ -0,0 +1,43 @@ +import sys +import platform +import pkg_resources +import argparse +from operator import itemgetter + +def out(*args): # pragma: no cover + for arg in args: + sys.stdout.write(arg) + sys.stdout.write(' ') + sys.stdout.write('\n') + +def get_parser(): + parser = argparse.ArgumentParser( + description="Show Python distribution versions and locations in use") + return parser + +def main(argv=sys.argv, pkg_resources=pkg_resources, platform=platform.platform, + out=out): + # all args except argv are for unit testing purposes only + parser = get_parser() + parser.parse_args(argv[1:]) + packages = [] + for distribution in pkg_resources.working_set: + name = distribution.project_name + packages.append( + {'version': distribution.version, + 'lowername': name.lower(), + 'name': name, + 'location':distribution.location} + ) + packages = sorted(packages, key=itemgetter('lowername')) + pyramid_version = pkg_resources.get_distribution('pyramid').version + plat = platform() + out('Pyramid version:', pyramid_version) + out('Platform:', plat) + out('Packages:') + for package in packages: + out(' ', package['name'], package['version']) + out(' ', package['location']) + +if __name__ == '__main__': # pragma: no cover + sys.exit(main() or 0) diff --git a/src/pyramid/scripts/prequest.py b/src/pyramid/scripts/prequest.py new file mode 100644 index 000000000..f0681afd7 --- /dev/null +++ b/src/pyramid/scripts/prequest.py @@ -0,0 +1,207 @@ +import base64 +import argparse +import sys +import textwrap + +from pyramid.compat import url_unquote +from pyramid.request import Request +from pyramid.scripts.common import get_config_loader +from pyramid.scripts.common import parse_vars + +def main(argv=sys.argv, quiet=False): + command = PRequestCommand(argv, quiet) + return command.run() + +class PRequestCommand(object): + description = """\ + Submit a HTTP request to a web application. + + This command makes an artifical request to a web application that uses a + PasteDeploy (.ini) configuration file for the server and application. + + Use "prequest config.ini /path" to request "/path". + + Use "prequest --method=POST config.ini /path < data" to do a POST with + the given request body. + + Use "prequest --method=PUT config.ini /path < data" to do a + PUT with the given request body. + + Use "prequest --method=PATCH config.ini /path < data" to do a + PATCH with the given request body. + + Use "prequest --method=OPTIONS config.ini /path" to do an + OPTIONS request. + + Use "prequest --method=PROPFIND config.ini /path" to do a + PROPFIND request. + + If the path is relative (doesn't begin with "/") it is interpreted as + relative to "/". The path passed to this script should be URL-quoted. + The path can be succeeded with a query string (e.g. '/path?a=1&=b2'). + + The variable "environ['paste.command_request']" will be set to "True" in + the request's WSGI environment, so your application can distinguish these + calls from normal requests. + """ + + parser = argparse.ArgumentParser( + description=textwrap.dedent(description), + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument( + '-n', '--app-name', + dest='app_name', + metavar='NAME', + help=( + "Load the named application from the config file (default 'main')" + ), + ) + parser.add_argument( + '--header', + dest='headers', + metavar='NAME:VALUE', + action='append', + help=( + "Header to add to request (you can use this option multiple times)" + ), + ) + parser.add_argument( + '-d', '--display-headers', + dest='display_headers', + action='store_true', + help='Display status and headers before the response body' + ) + parser.add_argument( + '-m', '--method', + dest='method', + choices=['GET', 'HEAD', 'POST', 'PUT', 'PATCH','DELETE', + 'PROPFIND', 'OPTIONS'], + help='Request method type (GET, POST, PUT, PATCH, DELETE, ' + 'PROPFIND, OPTIONS)', + ) + parser.add_argument( + '-l', '--login', + dest='login', + help='HTTP basic auth username:password pair', + ) + + parser.add_argument( + 'config_uri', + nargs='?', + default=None, + help='The URI to the configuration file.', + ) + + parser.add_argument( + 'path_info', + nargs='?', + default=None, + help='The path of the request.', + ) + + parser.add_argument( + 'config_vars', + nargs='*', + default=(), + help="Variables required by the config file. For example, " + "`http_port=%%(http_port)s` would expect `http_port=8080` to be " + "passed here.", + ) + + _get_config_loader = staticmethod(get_config_loader) + stdin = sys.stdin + + def __init__(self, argv, quiet=False): + self.quiet = quiet + self.args = self.parser.parse_args(argv[1:]) + + def out(self, msg): # pragma: no cover + if not self.quiet: + print(msg) + + def run(self): + if not self.args.config_uri or not self.args.path_info: + self.out('You must provide at least two arguments') + return 2 + config_uri = self.args.config_uri + config_vars = parse_vars(self.args.config_vars) + path = self.args.path_info + + loader = self._get_config_loader(config_uri) + loader.setup_logging(config_vars) + + app = loader.get_wsgi_app(self.args.app_name, config_vars) + + if not path.startswith('/'): + path = '/' + path + + try: + path, qs = path.split('?', 1) + except ValueError: + qs = '' + + path = url_unquote(path) + + headers = {} + if self.args.login: + enc = base64.b64encode(self.args.login.encode('ascii')) + headers['Authorization'] = 'Basic ' + enc.decode('ascii') + + if self.args.headers: + for item in self.args.headers: + if ':' not in item: + self.out( + "Bad --header=%s option, value must be in the form " + "'name:value'" % item) + return 2 + name, value = item.split(':', 1) + headers[name] = value.strip() + + request_method = (self.args.method or 'GET').upper() + + environ = { + 'REQUEST_METHOD': request_method, + 'SCRIPT_NAME': '', # may be empty if app is at the root + 'PATH_INFO': path, + 'SERVER_NAME': 'localhost', # always mandatory + 'SERVER_PORT': '80', # always mandatory + 'SERVER_PROTOCOL': 'HTTP/1.0', + 'CONTENT_TYPE': 'text/plain', + 'REMOTE_ADDR':'127.0.0.1', + 'wsgi.run_once': True, + 'wsgi.multithread': False, + 'wsgi.multiprocess': False, + 'wsgi.errors': sys.stderr, + 'wsgi.url_scheme': 'http', + 'wsgi.version': (1, 0), + 'QUERY_STRING': qs, + 'HTTP_ACCEPT': 'text/plain;q=1.0, */*;q=0.1', + 'paste.command_request': True, + } + + if request_method in ('POST', 'PUT', 'PATCH'): + environ['wsgi.input'] = self.stdin + environ['CONTENT_LENGTH'] = '-1' + + for name, value in headers.items(): + if name.lower() == 'content-type': + name = 'CONTENT_TYPE' + else: + name = 'HTTP_' + name.upper().replace('-', '_') + environ[name] = value + + request = Request.blank(path, environ=environ) + response = request.get_response(app) + if self.args.display_headers: + self.out(response.status) + for name, value in response.headerlist: + self.out('%s: %s' % (name, value)) + if response.charset: + self.out(response.ubody) + else: + self.out(response.body) + return 0 + +if __name__ == '__main__': # pragma: no cover + sys.exit(main() or 0) diff --git a/src/pyramid/scripts/proutes.py b/src/pyramid/scripts/proutes.py new file mode 100644 index 000000000..69d61ae8f --- /dev/null +++ b/src/pyramid/scripts/proutes.py @@ -0,0 +1,416 @@ +import fnmatch +import argparse +import sys +import textwrap +import re + +from zope.interface import Interface + +from pyramid.paster import bootstrap +from pyramid.compat import string_types +from pyramid.interfaces import IRouteRequest +from pyramid.config import not_ + +from pyramid.scripts.common import get_config_loader +from pyramid.scripts.common import parse_vars +from pyramid.static import static_view +from pyramid.view import _find_views + + +PAD = 3 +ANY_KEY = '*' +UNKNOWN_KEY = '' + + +def main(argv=sys.argv, quiet=False): + command = PRoutesCommand(argv, quiet) + return command.run() + + +def _get_pattern(route): + pattern = route.pattern + + if not pattern.startswith('/'): + pattern = '/%s' % pattern + return pattern + + +def _get_print_format(fmt, max_name, max_pattern, max_view, max_method): + print_fmt = '' + max_map = { + 'name': max_name, + 'pattern': max_pattern, + 'view': max_view, + 'method': max_method, + } + sizes = [] + + for index, col in enumerate(fmt): + size = max_map[col] + PAD + print_fmt += '{{%s: <{%s}}} ' % (col, index) + sizes.append(size) + + return print_fmt.format(*sizes) + + +def _get_request_methods(route_request_methods, view_request_methods): + excludes = set() + + if route_request_methods: + route_request_methods = set(route_request_methods) + + if view_request_methods: + view_request_methods = set(view_request_methods) + + for method in view_request_methods.copy(): + if method.startswith('!'): + view_request_methods.remove(method) + excludes.add(method[1:]) + + has_route_methods = route_request_methods is not None + has_view_methods = len(view_request_methods) > 0 + has_methods = has_route_methods or has_view_methods + + if has_route_methods is False and has_view_methods is False: + request_methods = [ANY_KEY] + elif has_route_methods is False and has_view_methods is True: + request_methods = view_request_methods + elif has_route_methods is True and has_view_methods is False: + request_methods = route_request_methods + else: + request_methods = route_request_methods.intersection( + view_request_methods + ) + + request_methods = set(request_methods).difference(excludes) + + if has_methods and not request_methods: + request_methods = '' + elif request_methods: + if excludes and request_methods == set([ANY_KEY]): + for exclude in excludes: + request_methods.add('!%s' % exclude) + + request_methods = ','.join(sorted(request_methods)) + + return request_methods + + +def _get_view_module(view_callable): + if view_callable is None: + return UNKNOWN_KEY + + if hasattr(view_callable, '__name__'): + if hasattr(view_callable, '__original_view__'): + original_view = view_callable.__original_view__ + else: + original_view = None + + if isinstance(original_view, static_view): + if original_view.package_name is not None: + return '%s:%s' % ( + original_view.package_name, + original_view.docroot + ) + else: + return original_view.docroot + else: + view_name = view_callable.__name__ + else: + # Currently only MultiView hits this, + # we could just not run _get_view_module + # for them and remove this logic + view_name = str(view_callable) + + view_module = '%s.%s' % ( + view_callable.__module__, + view_name, + ) + + # If pyramid wraps something in wsgiapp or wsgiapp2 decorators + # that is currently returned as pyramid.router.decorator, lets + # hack a nice name in: + if view_module == 'pyramid.router.decorator': + view_module = '' + + return view_module + + +def get_route_data(route, registry): + pattern = _get_pattern(route) + + request_iface = registry.queryUtility( + IRouteRequest, + name=route.name + ) + + route_request_methods = None + view_request_methods_order = [] + view_request_methods = {} + view_callable = None + + route_intr = registry.introspector.get( + 'routes', route.name + ) + + if request_iface is None: + return [ + (route.name, _get_pattern(route), UNKNOWN_KEY, ANY_KEY) + ] + + view_callables = _find_views(registry, request_iface, Interface, '') + if view_callables: + view_callable = view_callables[0] + else: + view_callable = None + view_module = _get_view_module(view_callable) + + # Introspectables can be turned off, so there could be a chance + # that we have no `route_intr` but we do have a route + callable + if route_intr is None: + view_request_methods[view_module] = [] + view_request_methods_order.append(view_module) + else: + if route_intr.get('static', False) is True: + return [ + (route.name, route_intr['external_url'], UNKNOWN_KEY, ANY_KEY) + ] + + route_request_methods = route_intr['request_methods'] + view_intr = registry.introspector.related(route_intr) + + if view_intr: + for view in view_intr: + request_method = view.get('request_methods') + + if request_method is not None: + if view.get('attr') is not None: + view_callable = getattr(view['callable'], view['attr']) + view_module = '%s.%s' % ( + _get_view_module(view['callable']), + view['attr'] + ) + else: + view_callable = view['callable'] + view_module = _get_view_module(view_callable) + + if view_module not in view_request_methods: + view_request_methods[view_module] = [] + view_request_methods_order.append(view_module) + + if isinstance(request_method, string_types): + request_method = (request_method,) + elif isinstance(request_method, not_): + request_method = ('!%s' % request_method.value,) + + view_request_methods[view_module].extend(request_method) + else: + if view_module not in view_request_methods: + view_request_methods[view_module] = [] + view_request_methods_order.append(view_module) + + else: + view_request_methods[view_module] = [] + view_request_methods_order.append(view_module) + + final_routes = [] + + for view_module in view_request_methods_order: + methods = view_request_methods[view_module] + request_methods = _get_request_methods( + route_request_methods, + methods + ) + + final_routes.append(( + route.name, + pattern, + view_module, + request_methods, + )) + + return final_routes + + +class PRoutesCommand(object): + description = """\ + Print all URL dispatch routes used by a Pyramid application in the + order in which they are evaluated. Each route includes the name of the + route, the pattern of the route, and the view callable which will be + invoked when the route is matched. + + This command accepts one positional argument named 'config_uri'. It + specifies the PasteDeploy config file to use for the interactive + shell. The format is 'inifile#name'. If the name is left off, 'main' + will be assumed. Example: 'proutes myapp.ini'. + + """ + bootstrap = staticmethod(bootstrap) # testing + get_config_loader = staticmethod(get_config_loader) # testing + stdout = sys.stdout + parser = argparse.ArgumentParser( + description=textwrap.dedent(description), + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument('-g', '--glob', + action='store', + dest='glob', + default='', + help='Display routes matching glob pattern') + + parser.add_argument('-f', '--format', + action='store', + dest='format', + default='', + help=('Choose which columns to display, this will ' + 'override the format key in the [proutes] ini ' + 'section')) + + parser.add_argument( + 'config_uri', + nargs='?', + default=None, + help='The URI to the configuration file.', + ) + + parser.add_argument( + 'config_vars', + nargs='*', + default=(), + help="Variables required by the config file. For example, " + "`http_port=%%(http_port)s` would expect `http_port=8080` to be " + "passed here.", + ) + + def __init__(self, argv, quiet=False): + self.args = self.parser.parse_args(argv[1:]) + self.quiet = quiet + self.available_formats = [ + 'name', 'pattern', 'view', 'method' + ] + self.column_format = self.available_formats + + def validate_formats(self, formats): + invalid_formats = [] + for fmt in formats: + if fmt not in self.available_formats: + invalid_formats.append(fmt) + + msg = ( + 'You provided invalid formats %s, ' + 'Available formats are %s' + ) + + if invalid_formats: + msg = msg % (invalid_formats, self.available_formats) + self.out(msg) + return False + + return True + + def proutes_file_config(self, loader, global_conf=None): + settings = loader.get_settings('proutes', global_conf) + format = settings.get('format') + if format: + cols = re.split(r'[,|\s\n]+', format) + self.column_format = [x.strip() for x in cols] + + def out(self, msg): # pragma: no cover + if not self.quiet: + print(msg) + + def _get_mapper(self, registry): + from pyramid.config import Configurator + config = Configurator(registry=registry) + return config.get_routes_mapper() + + def run(self, quiet=False): + if not self.args.config_uri: + self.out('requires a config file argument') + return 2 + + config_uri = self.args.config_uri + config_vars = parse_vars(self.args.config_vars) + loader = self.get_config_loader(config_uri) + loader.setup_logging(config_vars) + self.proutes_file_config(loader, config_vars) + + env = self.bootstrap(config_uri, options=config_vars) + registry = env['registry'] + mapper = self._get_mapper(registry) + + if self.args.format: + columns = self.args.format.split(',') + self.column_format = [x.strip() for x in columns] + + is_valid = self.validate_formats(self.column_format) + + if is_valid is False: + return 2 + + if mapper is None: + return 0 + + max_name = len('Name') + max_pattern = len('Pattern') + max_view = len('View') + max_method = len('Method') + + routes = mapper.get_routes(include_static=True) + + if len(routes) == 0: + return 0 + + mapped_routes = [{ + 'name': 'Name', + 'pattern': 'Pattern', + 'view': 'View', + 'method': 'Method' + },{ + 'name': '----', + 'pattern': '-------', + 'view': '----', + 'method': '------' + }] + + for route in routes: + route_data = get_route_data(route, registry) + + for name, pattern, view, method in route_data: + if self.args.glob: + match = (fnmatch.fnmatch(name, self.args.glob) or + fnmatch.fnmatch(pattern, self.args.glob)) + if not match: + continue + + if len(name) > max_name: + max_name = len(name) + + if len(pattern) > max_pattern: + max_pattern = len(pattern) + + if len(view) > max_view: + max_view = len(view) + + if len(method) > max_method: + max_method = len(method) + + mapped_routes.append({ + 'name': name, + 'pattern': pattern, + 'view': view, + 'method': method + }) + + fmt = _get_print_format( + self.column_format, max_name, max_pattern, max_view, max_method + ) + + for route in mapped_routes: + self.out(fmt.format(**route)) + + return 0 + + +if __name__ == '__main__': # pragma: no cover + sys.exit(main() or 0) diff --git a/src/pyramid/scripts/pserve.py b/src/pyramid/scripts/pserve.py new file mode 100644 index 000000000..8ee6e1467 --- /dev/null +++ b/src/pyramid/scripts/pserve.py @@ -0,0 +1,383 @@ +# (c) 2005 Ian Bicking and contributors; written for Paste +# (http://pythonpaste.org) Licensed under the MIT license: +# http://www.opensource.org/licenses/mit-license.php +# +# For discussion of daemonizing: +# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/278731 +# +# Code taken also from QP: http://www.mems-exchange.org/software/qp/ From +# lib/site.py + +import argparse +import os +import re +import sys +import textwrap +import threading +import time +import webbrowser + +import hupper + +from pyramid.compat import PY2 + +from pyramid.scripts.common import get_config_loader +from pyramid.scripts.common import parse_vars +from pyramid.path import AssetResolver +from pyramid.settings import aslist + + +def main(argv=sys.argv, quiet=False): + command = PServeCommand(argv, quiet=quiet) + return command.run() + + +class PServeCommand(object): + + description = """\ + This command serves a web application that uses a PasteDeploy + configuration file for the server and application. + + You can also include variable assignments like 'http_port=8080' + and then use %(http_port)s in your config files. + """ + default_verbosity = 1 + + parser = argparse.ArgumentParser( + description=textwrap.dedent(description), + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument( + '-n', '--app-name', + dest='app_name', + metavar='NAME', + help="Load the named application (default main)") + parser.add_argument( + '-s', '--server', + dest='server', + metavar='SERVER_TYPE', + help="Use the named server.") + parser.add_argument( + '--server-name', + dest='server_name', + metavar='SECTION_NAME', + help=("Use the named server as defined in the configuration file " + "(default: main)")) + parser.add_argument( + '--reload', + dest='reload', + action='store_true', + help="Use auto-restart file monitor") + parser.add_argument( + '--reload-interval', + dest='reload_interval', + default=1, + help=("Seconds between checking files (low number can cause " + "significant CPU usage)")) + parser.add_argument( + '-b', '--browser', + dest='browser', + action='store_true', + help=("Open a web browser to the server url. The server url is " + "determined from the 'open_url' setting in the 'pserve' " + "section of the configuration file.")) + parser.add_argument( + '-v', '--verbose', + default=default_verbosity, + dest='verbose', + action='count', + help="Set verbose level (default " + str(default_verbosity) + ")") + parser.add_argument( + '-q', '--quiet', + action='store_const', + const=0, + dest='verbose', + help="Suppress verbose output") + parser.add_argument( + 'config_uri', + nargs='?', + default=None, + help='The URI to the configuration file.', + ) + parser.add_argument( + 'config_vars', + nargs='*', + default=(), + help="Variables required by the config file. For example, " + "`http_port=%%(http_port)s` would expect `http_port=8080` to be " + "passed here.", + ) + + _get_config_loader = staticmethod(get_config_loader) # for testing + + open_url = None + + _scheme_re = re.compile(r'^[a-z][a-z]+:', re.I) + + def __init__(self, argv, quiet=False): + self.args = self.parser.parse_args(argv[1:]) + if quiet: + self.args.verbose = 0 + if self.args.reload: + self.worker_kwargs = {'argv': argv, "quiet": quiet} + self.watch_files = set() + + def out(self, msg): # pragma: no cover + if self.args.verbose > 0: + print(msg) + + def get_config_path(self, loader): + return os.path.abspath(loader.uri.path) + + def pserve_file_config(self, loader, global_conf=None): + settings = loader.get_settings('pserve', global_conf) + config_path = self.get_config_path(loader) + here = os.path.dirname(config_path) + watch_files = aslist(settings.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.add(os.path.abspath(file)) + + # attempt to determine the url of the server + open_url = settings.get('open_url') + if open_url: + self.open_url = open_url + + def guess_server_url(self, loader, server_name, global_conf=None): + server_name = server_name or 'main' + settings = loader.get_settings('server:' + server_name, global_conf) + if 'port' in settings: + return 'http://127.0.0.1:{port}'.format(**settings) + + def run(self): # pragma: no cover + if not self.args.config_uri: + self.out('You must give a config file') + return 2 + config_uri = self.args.config_uri + config_vars = parse_vars(self.args.config_vars) + app_spec = self.args.config_uri + app_name = self.args.app_name + + loader = self._get_config_loader(config_uri) + loader.setup_logging(config_vars) + + self.pserve_file_config(loader, global_conf=config_vars) + + server_name = self.args.server_name + if self.args.server: + server_spec = 'egg:pyramid' + assert server_name is None + server_name = self.args.server + else: + server_spec = app_spec + + server_loader = loader + if server_spec != app_spec: + server_loader = self.get_config_loader(server_spec) + + # do not open the browser on each reload so check hupper first + if self.args.browser and not hupper.is_active(): + url = self.open_url + + if not url: + url = self.guess_server_url( + server_loader, server_name, config_vars) + + if not url: + self.out('WARNING: could not determine the server\'s url to ' + 'open the browser. To fix this set the "open_url" ' + 'setting in the [pserve] section of the ' + 'configuration file.') + + else: + def open_browser(): + time.sleep(1) + webbrowser.open(url) + t = threading.Thread(target=open_browser) + t.setDaemon(True) + t.start() + + if self.args.reload and not hupper.is_active(): + if self.args.verbose > 1: + self.out('Running reloading file monitor') + hupper.start_reloader( + 'pyramid.scripts.pserve.main', + reload_interval=int(self.args.reload_interval), + verbose=self.args.verbose, + worker_kwargs=self.worker_kwargs + ) + return 0 + + config_path = self.get_config_path(loader) + self.watch_files.add(config_path) + + server_path = self.get_config_path(server_loader) + self.watch_files.add(server_path) + + if hupper.is_active(): + reloader = hupper.get_reloader() + reloader.watch_files(list(self.watch_files)) + + server = server_loader.get_wsgi_server(server_name, config_vars) + + app = loader.get_wsgi_app(app_name, config_vars) + + if self.args.verbose > 0: + if hasattr(os, 'getpid'): + msg = 'Starting server in PID %i.' % os.getpid() + else: + msg = 'Starting server.' + self.out(msg) + + try: + server(app) + except (SystemExit, KeyboardInterrupt) as e: + if self.args.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 + from wsgiref.simple_server import make_server + host = kw.get('host', '0.0.0.0') + port = int(kw.get('port', 8080)) + server = make_server(host, port, wsgi_app) + print('Starting HTTP server on http://%s:%s' % (host, port)) + server.serve_forever() + + +# For paste.deploy server instantiation (egg:pyramid#cherrypy) +def cherrypy_server_runner( + app, global_conf=None, host='127.0.0.1', port=None, + ssl_pem=None, protocol_version=None, numthreads=None, + server_name=None, max=None, request_queue_size=None, + timeout=None + ): # pragma: no cover + """ + Entry point for CherryPy's WSGI server + + Serves the specified WSGI app via CherryPyWSGIServer. + + ``app`` + + The WSGI 'application callable'; multiple WSGI applications + may be passed as (script_name, callable) pairs. + + ``host`` + + This is the ipaddress to bind to (or a hostname if your + nameserver is properly configured). This defaults to + 127.0.0.1, which is not a public interface. + + ``port`` + + The port to run on, defaults to 8080 for HTTP, or 4443 for + HTTPS. This can be a string or an integer value. + + ``ssl_pem`` + + This an optional SSL certificate file (via OpenSSL) You can + generate a self-signed test PEM certificate file as follows: + + $ openssl genrsa 1024 > host.key + $ chmod 400 host.key + $ openssl req -new -x509 -nodes -sha1 -days 365 \\ + -key host.key > host.cert + $ cat host.cert host.key > host.pem + $ chmod 400 host.pem + + ``protocol_version`` + + The protocol used by the server, by default ``HTTP/1.1``. + + ``numthreads`` + + The number of worker threads to create. + + ``server_name`` + + The string to set for WSGI's SERVER_NAME environ entry. + + ``max`` + + The maximum number of queued requests. (defaults to -1 = no + limit). + + ``request_queue_size`` + + The 'backlog' argument to socket.listen(); specifies the + maximum number of queued connections. + + ``timeout`` + + The timeout in seconds for accepted connections. + """ + is_ssl = False + if ssl_pem: + port = port or 4443 + is_ssl = True + + if not port: + if ':' in host: + host, port = host.split(':', 1) + else: + port = 8080 + bind_addr = (host, int(port)) + + kwargs = {} + for var_name in ('numthreads', 'max', 'request_queue_size', 'timeout'): + var = locals()[var_name] + if var is not None: + kwargs[var_name] = int(var) + + try: + from cheroot.wsgi import Server as WSGIServer + except ImportError: + from cherrypy.wsgiserver import CherryPyWSGIServer as WSGIServer + + server = WSGIServer(bind_addr, app, + server_name=server_name, **kwargs) + if ssl_pem is not None: + if PY2: + server.ssl_certificate = server.ssl_private_key = ssl_pem + else: + # creates wsgiserver.ssl_builtin as side-effect + try: + from cheroot.server import get_ssl_adapter_class + from cheroot.ssl.builtin import BuiltinSSLAdapter + except ImportError: + from cherrypy.wsgiserver import get_ssl_adapter_class + from cherrypy.wsgiserver.ssl_builtin import BuiltinSSLAdapter + get_ssl_adapter_class() + server.ssl_adapter = BuiltinSSLAdapter(ssl_pem, ssl_pem) + + if protocol_version: + server.protocol = protocol_version + + try: + protocol = is_ssl and 'https' or 'http' + if host == '0.0.0.0': + print('serving on 0.0.0.0:%s view at %s://127.0.0.1:%s' % + (port, protocol, port)) + else: + print('serving on %s://%s:%s' % (protocol, host, port)) + server.start() + except (KeyboardInterrupt, SystemExit): + server.stop() + + return server + + +if __name__ == '__main__': # pragma: no cover + sys.exit(main() or 0) diff --git a/src/pyramid/scripts/pshell.py b/src/pyramid/scripts/pshell.py new file mode 100644 index 000000000..4898eb39f --- /dev/null +++ b/src/pyramid/scripts/pshell.py @@ -0,0 +1,270 @@ +from code import interact +from contextlib import contextmanager +import argparse +import os +import sys +import textwrap +import pkg_resources + +from pyramid.compat import exec_ +from pyramid.util import DottedNameResolver +from pyramid.util import make_contextmanager +from pyramid.paster import bootstrap + +from pyramid.settings import aslist + +from pyramid.scripts.common import get_config_loader +from pyramid.scripts.common import parse_vars + +def main(argv=sys.argv, quiet=False): + command = PShellCommand(argv, quiet) + return command.run() + + +def python_shell_runner(env, help, interact=interact): + cprt = 'Type "help" for more information.' + banner = "Python %s on %s\n%s" % (sys.version, sys.platform, cprt) + banner += '\n\n' + help + '\n' + interact(banner, local=env) + + +class PShellCommand(object): + description = """\ + Open an interactive shell with a Pyramid app loaded. This command + accepts one positional argument named "config_uri" which specifies the + PasteDeploy config file to use for the interactive shell. The format is + "inifile#name". If the name is left off, the Pyramid default application + will be assumed. Example: "pshell myapp.ini#main". + + If you do not point the loader directly at the section of the ini file + containing your Pyramid application, the command will attempt to + find the app for you. If you are loading a pipeline that contains more + than one Pyramid application within it, the loader will use the + last one. + """ + bootstrap = staticmethod(bootstrap) # for testing + get_config_loader = staticmethod(get_config_loader) # for testing + pkg_resources = pkg_resources # for testing + + parser = argparse.ArgumentParser( + description=textwrap.dedent(description), + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument('-p', '--python-shell', + action='store', + dest='python_shell', + default='', + help=('Select the shell to use. A list of possible ' + 'shells is available using the --list-shells ' + 'option.')) + parser.add_argument('-l', '--list-shells', + dest='list', + action='store_true', + help='List all available shells.') + parser.add_argument('--setup', + dest='setup', + help=("A callable that will be passed the environment " + "before it is made available to the shell. This " + "option will override the 'setup' key in the " + "[pshell] ini section.")) + parser.add_argument('config_uri', + nargs='?', + default=None, + help='The URI to the configuration file.') + parser.add_argument( + 'config_vars', + nargs='*', + default=(), + help="Variables required by the config file. For example, " + "`http_port=%%(http_port)s` would expect `http_port=8080` to be " + "passed here.", + ) + + default_runner = python_shell_runner # testing + + loaded_objects = {} + object_help = {} + preferred_shells = [] + setup = None + pystartup = os.environ.get('PYTHONSTARTUP') + resolver = DottedNameResolver(None) + + def __init__(self, argv, quiet=False): + self.quiet = quiet + self.args = self.parser.parse_args(argv[1:]) + + def pshell_file_config(self, loader, defaults): + settings = loader.get_settings('pshell', defaults) + self.loaded_objects = {} + self.object_help = {} + self.setup = None + for k, v in settings.items(): + if k == 'setup': + self.setup = v + elif k == 'default_shell': + self.preferred_shells = [x.lower() for x in aslist(v)] + else: + self.loaded_objects[k] = self.resolver.maybe_resolve(v) + self.object_help[k] = v + + def out(self, msg): # pragma: no cover + if not self.quiet: + print(msg) + + def run(self, shell=None): + if self.args.list: + return self.show_shells() + if not self.args.config_uri: + self.out('Requires a config file argument') + return 2 + + config_uri = self.args.config_uri + config_vars = parse_vars(self.args.config_vars) + loader = self.get_config_loader(config_uri) + loader.setup_logging(config_vars) + self.pshell_file_config(loader, config_vars) + + self.env = self.bootstrap(config_uri, options=config_vars) + + # remove the closer from the env + self.closer = self.env.pop('closer') + + try: + if shell is None: + try: + shell = self.make_shell() + except ValueError as e: + self.out(str(e)) + return 1 + + with self.setup_env(): + shell(self.env, self.help) + + finally: + self.closer() + + @contextmanager + def setup_env(self): + # setup help text for default environment + env = self.env + env_help = dict(env) + env_help['app'] = 'The WSGI application.' + env_help['root'] = 'Root of the default resource tree.' + env_help['registry'] = 'Active Pyramid registry.' + env_help['request'] = 'Active request object.' + env_help['root_factory'] = ( + 'Default root factory used to create `root`.') + + # load the pshell section of the ini file + env.update(self.loaded_objects) + + # eliminate duplicates from env, allowing custom vars to override + for k in self.loaded_objects: + if k in env_help: + del env_help[k] + + # override use_script with command-line options + if self.args.setup: + self.setup = self.args.setup + + if self.setup: + # call the setup callable + self.setup = self.resolver.maybe_resolve(self.setup) + + # store the env before muddling it with the script + orig_env = env.copy() + setup_manager = make_contextmanager(self.setup) + with setup_manager(env): + # remove any objects from default help that were overidden + for k, v in env.items(): + if k not in orig_env or v != orig_env[k]: + if getattr(v, '__doc__', False): + env_help[k] = v.__doc__.replace("\n", " ") + else: + env_help[k] = v + del orig_env + + # generate help text + help = '' + if env_help: + help += 'Environment:' + for var in sorted(env_help.keys()): + help += '\n %-12s %s' % (var, env_help[var]) + + if self.object_help: + help += '\n\nCustom Variables:' + for var in sorted(self.object_help.keys()): + help += '\n %-12s %s' % (var, self.object_help[var]) + + if self.pystartup and os.path.isfile(self.pystartup): + with open(self.pystartup, 'rb') as fp: + exec_(fp.read().decode('utf-8'), env) + if '__builtins__' in env: + del env['__builtins__'] + + self.help = help.strip() + yield + + def show_shells(self): + shells = self.find_all_shells() + sorted_names = sorted(shells.keys(), key=lambda x: x.lower()) + + self.out('Available shells:') + for name in sorted_names: + self.out(' %s' % (name,)) + return 0 + + def find_all_shells(self): + pkg_resources = self.pkg_resources + + shells = {} + for ep in pkg_resources.iter_entry_points('pyramid.pshell_runner'): + name = ep.name + shell_factory = ep.load() + shells[name] = shell_factory + return shells + + def make_shell(self): + shells = self.find_all_shells() + + shell = None + user_shell = self.args.python_shell.lower() + + if not user_shell: + preferred_shells = self.preferred_shells + if not preferred_shells: + # by default prioritize all shells above python + preferred_shells = [k for k in shells.keys() if k != 'python'] + max_weight = len(preferred_shells) + def order(x): + # invert weight to reverse sort the list + # (closer to the front is higher priority) + try: + return preferred_shells.index(x[0].lower()) - max_weight + except ValueError: + return 1 + sorted_shells = sorted(shells.items(), key=order) + + if len(sorted_shells) > 0: + shell = sorted_shells[0][1] + + else: + runner = shells.get(user_shell) + + if runner is not None: + shell = runner + + if shell is None: + raise ValueError( + 'could not find a shell named "%s"' % user_shell + ) + + if shell is None: + # should never happen, but just incase entry points are borked + shell = self.default_runner + + return shell + + +if __name__ == '__main__': # pragma: no cover + sys.exit(main() or 0) diff --git a/src/pyramid/scripts/ptweens.py b/src/pyramid/scripts/ptweens.py new file mode 100644 index 000000000..d5cbebe12 --- /dev/null +++ b/src/pyramid/scripts/ptweens.py @@ -0,0 +1,109 @@ +import argparse +import sys +import textwrap + +from pyramid.interfaces import ITweens + +from pyramid.tweens import MAIN +from pyramid.tweens import INGRESS +from pyramid.paster import bootstrap +from pyramid.paster import setup_logging +from pyramid.scripts.common import parse_vars + +def main(argv=sys.argv, quiet=False): + command = PTweensCommand(argv, quiet) + return command.run() + +class PTweensCommand(object): + description = """\ + Print all implicit and explicit tween objects used by a Pyramid + application. The handler output includes whether the system is using an + explicit tweens ordering (will be true when the "pyramid.tweens" + deployment setting is used) or an implicit tweens ordering (will be true + when the "pyramid.tweens" deployment setting is *not* used). + + This command accepts one positional argument named "config_uri" which + specifies the PasteDeploy config file to use for the interactive + shell. The format is "inifile#name". If the name is left off, "main" + will be assumed. Example: "ptweens myapp.ini#main". + + """ + parser = argparse.ArgumentParser( + description=textwrap.dedent(description), + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + + parser.add_argument('config_uri', + nargs='?', + default=None, + help='The URI to the configuration file.') + + parser.add_argument( + 'config_vars', + nargs='*', + default=(), + help="Variables required by the config file. For example, " + "`http_port=%%(http_port)s` would expect `http_port=8080` to be " + "passed here.", + ) + + stdout = sys.stdout + bootstrap = staticmethod(bootstrap) # testing + setup_logging = staticmethod(setup_logging) # testing + + def __init__(self, argv, quiet=False): + self.quiet = quiet + self.args = self.parser.parse_args(argv[1:]) + + def _get_tweens(self, registry): + from pyramid.config import Configurator + config = Configurator(registry=registry) + return config.registry.queryUtility(ITweens) + + def out(self, msg): # pragma: no cover + if not self.quiet: + print(msg) + + def show_chain(self, chain): + fmt = '%-10s %-65s' + self.out(fmt % ('Position', 'Name')) + self.out(fmt % ('-' * len('Position'), '-' * len('Name'))) + self.out(fmt % ('-', INGRESS)) + for pos, (name, _) in enumerate(chain): + self.out(fmt % (pos, name)) + self.out(fmt % ('-', MAIN)) + + def run(self): + if not self.args.config_uri: + self.out('Requires a config file argument') + return 2 + config_uri = self.args.config_uri + config_vars = parse_vars(self.args.config_vars) + self.setup_logging(config_uri, global_conf=config_vars) + env = self.bootstrap(config_uri, options=config_vars) + registry = env['registry'] + tweens = self._get_tweens(registry) + if tweens is not None: + explicit = tweens.explicit + if explicit: + self.out('"pyramid.tweens" config value set ' + '(explicitly ordered tweens used)') + self.out('') + self.out('Explicit Tween Chain (used)') + self.out('') + self.show_chain(tweens.explicit) + self.out('') + self.out('Implicit Tween Chain (not used)') + self.out('') + self.show_chain(tweens.implicit()) + else: + self.out('"pyramid.tweens" config value NOT set ' + '(implicitly ordered tweens used)') + self.out('') + self.out('Implicit Tween Chain') + self.out('') + self.show_chain(tweens.implicit()) + return 0 + +if __name__ == '__main__': # pragma: no cover + sys.exit(main() or 0) diff --git a/src/pyramid/scripts/pviews.py b/src/pyramid/scripts/pviews.py new file mode 100644 index 000000000..c0df2f078 --- /dev/null +++ b/src/pyramid/scripts/pviews.py @@ -0,0 +1,289 @@ +import argparse +import sys +import textwrap + +from pyramid.interfaces import IMultiView +from pyramid.paster import bootstrap +from pyramid.paster import setup_logging +from pyramid.request import Request +from pyramid.scripts.common import parse_vars +from pyramid.view import _find_views + +def main(argv=sys.argv, quiet=False): + command = PViewsCommand(argv, quiet) + return command.run() + +class PViewsCommand(object): + description = """\ + Print, for a given URL, the views that might match. Underneath each + potentially matching route, list the predicates required. Underneath + each route+predicate set, print each view that might match and its + predicates. + + This command accepts two positional arguments: 'config_uri' specifies the + PasteDeploy config file to use for the interactive shell. The format is + 'inifile#name'. If the name is left off, 'main' will be assumed. 'url' + specifies the path info portion of a URL that will be used to find + matching views. Example: 'proutes myapp.ini#main /url' + """ + stdout = sys.stdout + + parser = argparse.ArgumentParser( + description=textwrap.dedent(description), + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + + parser.add_argument('config_uri', + nargs='?', + default=None, + help='The URI to the configuration file.') + + parser.add_argument('url', + nargs='?', + default=None, + help='The path info portion of the URL.') + parser.add_argument( + 'config_vars', + nargs='*', + default=(), + help="Variables required by the config file. For example, " + "`http_port=%%(http_port)s` would expect `http_port=8080` to be " + "passed here.", + ) + + + bootstrap = staticmethod(bootstrap) # testing + setup_logging = staticmethod(setup_logging) # testing + + def __init__(self, argv, quiet=False): + self.quiet = quiet + self.args = self.parser.parse_args(argv[1:]) + + def out(self, msg): # pragma: no cover + if not self.quiet: + print(msg) + + def _find_multi_routes(self, mapper, request): + infos = [] + path = request.environ['PATH_INFO'] + # find all routes that match path, regardless of predicates + for route in mapper.get_routes(): + match = route.match(path) + if match is not None: + info = {'match':match, 'route':route} + infos.append(info) + return infos + + def _find_view(self, request): + """ + Accept ``url`` and ``registry``; create a :term:`request` and + find a :app:`Pyramid` view based on introspection of :term:`view + configuration` within the application registry; return the view. + """ + from zope.interface import providedBy + from zope.interface import implementer + from pyramid.interfaces import IRequest + from pyramid.interfaces import IRootFactory + from pyramid.interfaces import IRouteRequest + from pyramid.interfaces import IRoutesMapper + from pyramid.interfaces import ITraverser + from pyramid.traversal import DefaultRootFactory + from pyramid.traversal import ResourceTreeTraverser + + registry = request.registry + q = registry.queryUtility + root_factory = q(IRootFactory, default=DefaultRootFactory) + routes_mapper = q(IRoutesMapper) + + adapters = registry.adapters + + @implementer(IMultiView) + class RoutesMultiView(object): + + def __init__(self, infos, context_iface, root_factory, request): + self.views = [] + for info in infos: + match, route = info['match'], info['route'] + if route is not None: + request_iface = registry.queryUtility( + IRouteRequest, + name=route.name, + default=IRequest) + views = _find_views( + request.registry, + request_iface, + context_iface, + '' + ) + if not views: + continue + view = views[0] + view.__request_attrs__ = {} + view.__request_attrs__['matchdict'] = match + view.__request_attrs__['matched_route'] = route + root_factory = route.factory or root_factory + root = root_factory(request) + traverser = adapters.queryAdapter(root, ITraverser) + if traverser is None: + traverser = ResourceTreeTraverser(root) + tdict = traverser(request) + view.__request_attrs__.update(tdict) + if not hasattr(view, '__view_attr__'): + view.__view_attr__ = '' + self.views.append((None, view, None)) + + context = None + routes_multiview = None + attrs = request.__dict__ + request_iface = IRequest + + # find the root object + if routes_mapper is not None: + infos = self._find_multi_routes(routes_mapper, request) + if len(infos) == 1: + info = infos[0] + match, route = info['match'], info['route'] + if route is not None: + attrs['matchdict'] = match + attrs['matched_route'] = route + request.environ['bfg.routes.matchdict'] = match + request_iface = registry.queryUtility( + IRouteRequest, + name=route.name, + default=IRequest) + root_factory = route.factory or root_factory + if len(infos) > 1: + routes_multiview = infos + + root = root_factory(request) + attrs['root'] = root + + # find a context + traverser = adapters.queryAdapter(root, ITraverser) + if traverser is None: + traverser = ResourceTreeTraverser(root) + tdict = traverser(request) + context, view_name = (tdict['context'], tdict['view_name']) + + attrs.update(tdict) + + # find a view callable + context_iface = providedBy(context) + if routes_multiview is None: + views = _find_views( + request.registry, + request_iface, + context_iface, + view_name, + ) + if views: + view = views[0] + else: + view = None + else: + view = RoutesMultiView(infos, context_iface, root_factory, request) + + # routes are not registered with a view name + if view is None: + views = _find_views( + request.registry, + request_iface, + context_iface, + '', + ) + if views: + view = views[0] + else: + view = None + # we don't want a multiview here + if IMultiView.providedBy(view): + view = None + + if view is not None: + view.__request_attrs__ = attrs + + return view + + def output_route_attrs(self, attrs, indent): + route = attrs['matched_route'] + self.out("%sroute name: %s" % (indent, route.name)) + self.out("%sroute pattern: %s" % (indent, route.pattern)) + self.out("%sroute path: %s" % (indent, route.path)) + self.out("%ssubpath: %s" % (indent, '/'.join(attrs['subpath']))) + predicates = ', '.join([p.text() for p in route.predicates]) + if predicates != '': + self.out("%sroute predicates (%s)" % (indent, predicates)) + + def output_view_info(self, view_wrapper, level=1): + indent = " " * level + name = getattr(view_wrapper, '__name__', '') + module = getattr(view_wrapper, '__module__', '') + attr = getattr(view_wrapper, '__view_attr__', None) + request_attrs = getattr(view_wrapper, '__request_attrs__', {}) + if attr is not None: + view_callable = "%s.%s.%s" % (module, name, attr) + else: + attr = view_wrapper.__class__.__name__ + if attr == 'function': + attr = name + view_callable = "%s.%s" % (module, attr) + self.out('') + if 'matched_route' in request_attrs: + self.out("%sRoute:" % indent) + self.out("%s------" % indent) + self.output_route_attrs(request_attrs, indent) + permission = getattr(view_wrapper, '__permission__', None) + if not IMultiView.providedBy(view_wrapper): + # single view for this route, so repeat call without route data + del request_attrs['matched_route'] + self.output_view_info(view_wrapper, level + 1) + else: + self.out("%sView:" % indent) + self.out("%s-----" % indent) + self.out("%s%s" % (indent, view_callable)) + permission = getattr(view_wrapper, '__permission__', None) + if permission is not None: + self.out("%srequired permission = %s" % (indent, permission)) + predicates = getattr(view_wrapper, '__predicates__', None) + if predicates is not None: + predicate_text = ', '.join([p.text() for p in predicates]) + self.out("%sview predicates (%s)" % (indent, predicate_text)) + + def run(self): + if not self.args.config_uri or not self.args.url: + self.out('Command requires a config file arg and a url arg') + return 2 + config_uri = self.args.config_uri + config_vars = parse_vars(self.args.config_vars) + url = self.args.url + + self.setup_logging(config_uri, global_conf=config_vars) + + if not url.startswith('/'): + url = '/%s' % url + request = Request.blank(url) + env = self.bootstrap(config_uri, options=config_vars, request=request) + view = self._find_view(request) + self.out('') + self.out("URL = %s" % url) + self.out('') + if view is not None: + self.out(" context: %s" % view.__request_attrs__['context']) + self.out(" view name: %s" % view.__request_attrs__['view_name']) + if IMultiView.providedBy(view): + for dummy, view_wrapper, dummy in view.views: + self.output_view_info(view_wrapper) + if IMultiView.providedBy(view_wrapper): + for dummy, mv_view_wrapper, dummy in view_wrapper.views: + self.output_view_info(mv_view_wrapper, level=2) + else: + if view is not None: + self.output_view_info(view) + else: + self.out(" Not found.") + self.out('') + env['closer']() + return 0 + +if __name__ == '__main__': # pragma: no cover + sys.exit(main() or 0) diff --git a/src/pyramid/security.py b/src/pyramid/security.py new file mode 100644 index 000000000..0bdca090b --- /dev/null +++ b/src/pyramid/security.py @@ -0,0 +1,427 @@ +from zope.deprecation import deprecated +from zope.interface import providedBy + +from pyramid.interfaces import ( + IAuthenticationPolicy, + IAuthorizationPolicy, + ISecuredView, + IView, + IViewClassifier, + ) + +from pyramid.compat import map_ +from pyramid.threadlocal import get_current_registry + +Everyone = 'system.Everyone' +Authenticated = 'system.Authenticated' +Allow = 'Allow' +Deny = 'Deny' + +class AllPermissionsList(object): + """ Stand in 'permission list' to represent all permissions """ + + def __iter__(self): + return iter(()) + + def __contains__(self, other): + return True + + def __eq__(self, other): + return isinstance(other, self.__class__) + +ALL_PERMISSIONS = AllPermissionsList() +DENY_ALL = (Deny, Everyone, ALL_PERMISSIONS) + +NO_PERMISSION_REQUIRED = '__no_permission_required__' + +def _get_registry(request): + try: + reg = request.registry + except AttributeError: + reg = get_current_registry() # b/c + return reg + +def _get_authentication_policy(request): + registry = _get_registry(request) + return registry.queryUtility(IAuthenticationPolicy) + +def has_permission(permission, context, request): + """ + A function that calls :meth:`pyramid.request.Request.has_permission` + and returns its result. + + .. deprecated:: 1.5 + Use :meth:`pyramid.request.Request.has_permission` instead. + + .. versionchanged:: 1.5a3 + If context is None, then attempt to use the context attribute of self; + if not set, then the AttributeError is propagated. + """ + return request.has_permission(permission, context) + +deprecated( + 'has_permission', + 'As of Pyramid 1.5 the "pyramid.security.has_permission" API is now ' + 'deprecated. It will be removed in Pyramid 1.8. Use the ' + '"has_permission" method of the Pyramid request instead.' + ) + + +def authenticated_userid(request): + """ + A function that returns the value of the property + :attr:`pyramid.request.Request.authenticated_userid`. + + .. deprecated:: 1.5 + Use :attr:`pyramid.request.Request.authenticated_userid` instead. + """ + return request.authenticated_userid + +deprecated( + 'authenticated_userid', + 'As of Pyramid 1.5 the "pyramid.security.authenticated_userid" API is now ' + 'deprecated. It will be removed in Pyramid 1.8. Use the ' + '"authenticated_userid" attribute of the Pyramid request instead.' + ) + +def unauthenticated_userid(request): + """ + A function that returns the value of the property + :attr:`pyramid.request.Request.unauthenticated_userid`. + + .. deprecated:: 1.5 + Use :attr:`pyramid.request.Request.unauthenticated_userid` instead. + """ + return request.unauthenticated_userid + +deprecated( + 'unauthenticated_userid', + 'As of Pyramid 1.5 the "pyramid.security.unauthenticated_userid" API is ' + 'now deprecated. It will be removed in Pyramid 1.8. Use the ' + '"unauthenticated_userid" attribute of the Pyramid request instead.' + ) + +def effective_principals(request): + """ + A function that returns the value of the property + :attr:`pyramid.request.Request.effective_principals`. + + .. deprecated:: 1.5 + Use :attr:`pyramid.request.Request.effective_principals` instead. + """ + return request.effective_principals + +deprecated( + 'effective_principals', + 'As of Pyramid 1.5 the "pyramid.security.effective_principals" API is ' + 'now deprecated. It will be removed in Pyramid 1.8. Use the ' + '"effective_principals" attribute of the Pyramid request instead.' + ) + +def remember(request, userid, **kw): + """ + Returns a sequence of header tuples (e.g. ``[('Set-Cookie', 'foo=abc')]``) + on this request's response. + These headers are suitable for 'remembering' a set of credentials + implied by the data passed as ``userid`` and ``*kw`` using the + current :term:`authentication policy`. Common usage might look + like so within the body of a view function (``response`` is + assumed to be a :term:`WebOb` -style :term:`response` object + computed previously by the view code): + + .. code-block:: python + + from pyramid.security import remember + headers = remember(request, 'chrism', password='123', max_age='86400') + response = request.response + response.headerlist.extend(headers) + return response + + If no :term:`authentication policy` is in use, this function will + always return an empty sequence. If used, the composition and + meaning of ``**kw`` must be agreed upon by the calling code and + the effective authentication policy. + + .. versionchanged:: 1.6 + Deprecated the ``principal`` argument in favor of ``userid`` to clarify + its relationship to the authentication policy. + + .. versionchanged:: 1.10 + Removed the deprecated ``principal`` argument. + """ + policy = _get_authentication_policy(request) + if policy is None: + return [] + return policy.remember(request, userid, **kw) + +def forget(request): + """ + Return a sequence of header tuples (e.g. ``[('Set-Cookie', + 'foo=abc')]``) suitable for 'forgetting' the set of credentials + possessed by the currently authenticated user. A common usage + might look like so within the body of a view function + (``response`` is assumed to be an :term:`WebOb` -style + :term:`response` object computed previously by the view code): + + .. code-block:: python + + from pyramid.security import forget + headers = forget(request) + response.headerlist.extend(headers) + return response + + If no :term:`authentication policy` is in use, this function will + always return an empty sequence. + """ + policy = _get_authentication_policy(request) + if policy is None: + return [] + return policy.forget(request) + +def principals_allowed_by_permission(context, permission): + """ Provided a ``context`` (a resource object), and a ``permission`` + (a string or unicode object), if an :term:`authorization policy` is + in effect, return a sequence of :term:`principal` ids that possess + the permission in the ``context``. If no authorization policy is + in effect, this will return a sequence with the single value + :mod:`pyramid.security.Everyone` (the special principal + identifier representing all principals). + + .. note:: + + Even if an :term:`authorization policy` is in effect, + some (exotic) authorization policies may not implement the + required machinery for this function; those will cause a + :exc:`NotImplementedError` exception to be raised when this + function is invoked. + """ + reg = get_current_registry() + policy = reg.queryUtility(IAuthorizationPolicy) + if policy is None: + return [Everyone] + return policy.principals_allowed_by_permission(context, permission) + +def view_execution_permitted(context, request, name=''): + """ If the view specified by ``context`` and ``name`` is protected + by a :term:`permission`, check the permission associated with the + view using the effective authentication/authorization policies and + the ``request``. Return a boolean result. If no + :term:`authorization policy` is in effect, or if the view is not + protected by a permission, return ``True``. If no view can view found, + an exception will be raised. + + .. versionchanged:: 1.4a4 + An exception is raised if no view is found. + + """ + reg = _get_registry(request) + provides = [IViewClassifier] + map_(providedBy, (request, context)) + # XXX not sure what to do here about using _find_views or analogue; + # for now let's just keep it as-is + view = reg.adapters.lookup(provides, ISecuredView, name=name) + if view is None: + view = reg.adapters.lookup(provides, IView, name=name) + if view is None: + raise TypeError('No registered view satisfies the constraints. ' + 'It would not make sense to claim that this view ' + '"is" or "is not" permitted.') + return Allowed( + 'Allowed: view name %r in context %r (no permission defined)' % + (name, context)) + return view.__permitted__(context, request) + + +class PermitsResult(int): + def __new__(cls, s, *args): + """ + Create a new instance. + + :param fmt: A format string explaining the reason for denial. + :param args: Arguments are stored and used with the format string + to generate the ``msg``. + + """ + inst = int.__new__(cls, cls.boolval) + inst.s = s + inst.args = args + return inst + + @property + def msg(self): + """ A string indicating why the result was generated.""" + return self.s % self.args + + def __str__(self): + return self.msg + + def __repr__(self): + return '<%s instance at %s with msg %r>' % (self.__class__.__name__, + id(self), + self.msg) + +class Denied(PermitsResult): + """ + An instance of ``Denied`` is returned when a security-related + API or other :app:`Pyramid` code denies an action unrelated to + an ACL check. It evaluates equal to all boolean false types. It + has an attribute named ``msg`` describing the circumstances for + the deny. + + """ + boolval = 0 + +class Allowed(PermitsResult): + """ + An instance of ``Allowed`` is returned when a security-related + API or other :app:`Pyramid` code allows an action unrelated to + an ACL check. It evaluates equal to all boolean true types. It + has an attribute named ``msg`` describing the circumstances for + the allow. + + """ + boolval = 1 + +class ACLPermitsResult(PermitsResult): + def __new__(cls, ace, acl, permission, principals, context): + """ + Create a new instance. + + :param ace: The :term:`ACE` that matched, triggering the result. + :param acl: The :term:`ACL` containing ``ace``. + :param permission: The required :term:`permission`. + :param principals: The list of :term:`principals ` provided. + :param context: The :term:`context` providing the :term:`lineage` + searched. + + """ + fmt = ('%s permission %r via ACE %r in ACL %r on context %r for ' + 'principals %r') + inst = PermitsResult.__new__( + cls, + fmt, + cls.__name__, + permission, + ace, + acl, + context, + principals, + ) + inst.permission = permission + inst.ace = ace + inst.acl = acl + inst.principals = principals + inst.context = context + return inst + +class ACLDenied(ACLPermitsResult, Denied): + """ + An instance of ``ACLDenied`` is a specialization of + :class:`pyramid.security.Denied` that represents that a security check + made explicitly against ACL was denied. It evaluates equal to all + boolean false types. It also has the following attributes: ``acl``, + ``ace``, ``permission``, ``principals``, and ``context``. These + attributes indicate the security values involved in the request. Its + ``__str__`` method prints a summary of these attributes for debugging + purposes. The same summary is available as the ``msg`` attribute. + + """ + +class ACLAllowed(ACLPermitsResult, Allowed): + """ + An instance of ``ACLAllowed`` is a specialization of + :class:`pyramid.security.Allowed` that represents that a security check + made explicitly against ACL was allowed. It evaluates equal to all + boolean true types. It also has the following attributes: ``acl``, + ``ace``, ``permission``, ``principals``, and ``context``. These + attributes indicate the security values involved in the request. Its + ``__str__`` method prints a summary of these attributes for debugging + purposes. The same summary is available as the ``msg`` attribute. + + """ + +class AuthenticationAPIMixin(object): + + def _get_authentication_policy(self): + reg = _get_registry(self) + return reg.queryUtility(IAuthenticationPolicy) + + @property + def authenticated_userid(self): + """ Return the userid of the currently authenticated user or + ``None`` if there is no :term:`authentication policy` in effect or + there is no currently authenticated user. + + .. versionadded:: 1.5 + """ + policy = self._get_authentication_policy() + if policy is None: + return None + return policy.authenticated_userid(self) + + @property + def unauthenticated_userid(self): + """ Return an object which represents the *claimed* (not verified) user + id of the credentials present in the request. ``None`` if there is no + :term:`authentication policy` in effect or there is no user data + associated with the current request. This differs from + :attr:`~pyramid.request.Request.authenticated_userid`, because the + effective authentication policy will not ensure that a record + associated with the userid exists in persistent storage. + + .. versionadded:: 1.5 + """ + policy = self._get_authentication_policy() + if policy is None: + return None + return policy.unauthenticated_userid(self) + + @property + def effective_principals(self): + """ Return the list of 'effective' :term:`principal` identifiers + for the ``request``. If no :term:`authentication policy` is in effect, + this will return a one-element list containing the + :data:`pyramid.security.Everyone` principal. + + .. versionadded:: 1.5 + """ + policy = self._get_authentication_policy() + if policy is None: + return [Everyone] + return policy.effective_principals(self) + +class AuthorizationAPIMixin(object): + + def has_permission(self, permission, context=None): + """ Given a permission and an optional context, returns an instance of + :data:`pyramid.security.Allowed` if the permission is granted to this + request with the provided context, or the context already associated + with the request. Otherwise, returns an instance of + :data:`pyramid.security.Denied`. This method delegates to the current + authentication and authorization policies. Returns + :data:`pyramid.security.Allowed` unconditionally if no authentication + policy has been registered for this request. If ``context`` is not + supplied or is supplied as ``None``, the context used is the + ``request.context`` attribute. + + :param permission: Does this request have the given permission? + :type permission: unicode, str + :param context: A resource object or ``None`` + :type context: object + :returns: Either :class:`pyramid.security.Allowed` or + :class:`pyramid.security.Denied`. + + .. versionadded:: 1.5 + + """ + if context is None: + context = self.context + reg = _get_registry(self) + authn_policy = reg.queryUtility(IAuthenticationPolicy) + if authn_policy is None: + return Allowed('No authentication policy in use.') + authz_policy = reg.queryUtility(IAuthorizationPolicy) + if authz_policy is None: + raise ValueError('Authentication policy registered without ' + 'authorization policy') # should never happen + principals = authn_policy.effective_principals(self) + return authz_policy.permits(context, principals, permission) diff --git a/src/pyramid/session.py b/src/pyramid/session.py new file mode 100644 index 000000000..b953fa184 --- /dev/null +++ b/src/pyramid/session.py @@ -0,0 +1,712 @@ +import base64 +import binascii +import hashlib +import hmac +import os +import time +import warnings + +from zope.deprecation import deprecated +from zope.interface import implementer + +from webob.cookies import ( + JSONSerializer, + SignedSerializer, +) + +from pyramid.compat import ( + pickle, + PY2, + text_, + bytes_, + native_, + ) +from pyramid.csrf import ( + check_csrf_origin, + check_csrf_token, +) + +from pyramid.interfaces import ISession +from pyramid.util import strings_differ + + +def manage_accessed(wrapped): + """ Decorator which causes a cookie to be renewed when an accessor + method is called.""" + def accessed(session, *arg, **kw): + session.accessed = now = int(time.time()) + if session._reissue_time is not None: + if now - session.renewed > session._reissue_time: + session.changed() + return wrapped(session, *arg, **kw) + accessed.__doc__ = wrapped.__doc__ + return accessed + +def manage_changed(wrapped): + """ Decorator which causes a cookie to be set when a setter method + is called.""" + def changed(session, *arg, **kw): + session.accessed = int(time.time()) + session.changed() + return wrapped(session, *arg, **kw) + changed.__doc__ = wrapped.__doc__ + return changed + +def signed_serialize(data, secret): + """ Serialize any pickleable structure (``data``) and sign it + using the ``secret`` (must be a string). Return the + serialization, which includes the signature as its first 40 bytes. + The ``signed_deserialize`` method will deserialize such a value. + + This function is useful for creating signed cookies. For example: + + .. code-block:: python + + cookieval = signed_serialize({'a':1}, 'secret') + response.set_cookie('signed_cookie', cookieval) + + .. deprecated:: 1.10 + + This function will be removed in :app:`Pyramid` 2.0. It is using + pickle-based serialization, which is considered vulnerable to remote + code execution attacks and will no longer be used by the default + session factories at that time. + + """ + pickled = pickle.dumps(data, pickle.HIGHEST_PROTOCOL) + try: + # bw-compat with pyramid <= 1.5b1 where latin1 is the default + secret = bytes_(secret) + except UnicodeEncodeError: + secret = bytes_(secret, 'utf-8') + sig = hmac.new(secret, pickled, hashlib.sha1).hexdigest() + return sig + native_(base64.b64encode(pickled)) + +deprecated( + 'signed_serialize', + 'This function will be removed in Pyramid 2.0. It is using pickle-based ' + 'serialization, which is considered vulnerable to remote code execution ' + 'attacks.', +) + +def signed_deserialize(serialized, secret, hmac=hmac): + """ Deserialize the value returned from ``signed_serialize``. If + the value cannot be deserialized for any reason, a + :exc:`ValueError` exception will be raised. + + This function is useful for deserializing a signed cookie value + created by ``signed_serialize``. For example: + + .. code-block:: python + + cookieval = request.cookies['signed_cookie'] + data = signed_deserialize(cookieval, 'secret') + + .. deprecated:: 1.10 + + This function will be removed in :app:`Pyramid` 2.0. It is using + pickle-based serialization, which is considered vulnerable to remote + code execution attacks and will no longer be used by the default + session factories at that time. + """ + # hmac parameterized only for unit tests + try: + input_sig, pickled = (bytes_(serialized[:40]), + base64.b64decode(bytes_(serialized[40:]))) + except (binascii.Error, TypeError) as e: + # Badly formed data can make base64 die + raise ValueError('Badly formed base64 data: %s' % e) + + try: + # bw-compat with pyramid <= 1.5b1 where latin1 is the default + secret = bytes_(secret) + except UnicodeEncodeError: + secret = bytes_(secret, 'utf-8') + sig = bytes_(hmac.new(secret, pickled, hashlib.sha1).hexdigest()) + + # Avoid timing attacks (see + # http://seb.dbzteam.org/crypto/python-oauth-timing-hmac.pdf) + if strings_differ(sig, input_sig): + raise ValueError('Invalid signature') + + return pickle.loads(pickled) + +deprecated( + 'signed_deserialize', + 'This function will be removed in Pyramid 2.0. It is using pickle-based ' + 'serialization, which is considered vulnerable to remote code execution ' + 'attacks.', +) + + +class PickleSerializer(object): + """ A serializer that uses the pickle protocol to dump Python + data to bytes. + + This is the default serializer used by Pyramid. + + ``protocol`` may be specified to control the version of pickle used. + Defaults to :attr:`pickle.HIGHEST_PROTOCOL`. + + """ + def __init__(self, protocol=pickle.HIGHEST_PROTOCOL): + self.protocol = protocol + + def loads(self, bstruct): + """Accept bytes and return a Python object.""" + try: + return pickle.loads(bstruct) + # at least ValueError, AttributeError, ImportError but more to be safe + except Exception: + raise ValueError + + def dumps(self, appstruct): + """Accept a Python object and return bytes.""" + return pickle.dumps(appstruct, self.protocol) + + +JSONSerializer = JSONSerializer # api + + +def BaseCookieSessionFactory( + serializer, + cookie_name='session', + max_age=None, + path='/', + domain=None, + secure=False, + httponly=False, + samesite='Lax', + timeout=1200, + reissue_time=0, + set_on_exception=True, + ): + """ + Configure a :term:`session factory` which will provide cookie-based + sessions. The return value of this function is a :term:`session factory`, + which may be provided as the ``session_factory`` argument of a + :class:`pyramid.config.Configurator` constructor, or used as the + ``session_factory`` argument of the + :meth:`pyramid.config.Configurator.set_session_factory` method. + + The session factory returned by this function will create sessions + which are limited to storing fewer than 4000 bytes of data (as the + payload must fit into a single cookie). + + .. warning: + + This class provides no protection from tampering and is only intended + to be used by framework authors to create their own cookie-based + session factories. + + Parameters: + + ``serializer`` + An object with two methods: ``loads`` and ``dumps``. The ``loads`` + method should accept bytes and return a Python object. The ``dumps`` + method should accept a Python object and return bytes. A ``ValueError`` + should be raised for malformed inputs. + + ``cookie_name`` + The name of the cookie used for sessioning. Default: ``'session'``. + + ``max_age`` + The maximum age of the cookie used for sessioning (in seconds). + Default: ``None`` (browser scope). + + ``path`` + The path used for the session cookie. Default: ``'/'``. + + ``domain`` + The domain used for the session cookie. Default: ``None`` (no domain). + + ``secure`` + The 'secure' flag of the session cookie. Default: ``False``. + + ``httponly`` + Hide the cookie from Javascript by setting the 'HttpOnly' flag of the + session cookie. Default: ``False``. + + ``samesite`` + The 'samesite' option of the session cookie. Set the value to ``None`` + to turn off the samesite option. Default: ``'Lax'``. + + ``timeout`` + A number of seconds of inactivity before a session times out. If + ``None`` then the cookie never expires. This lifetime only applies + to the *value* within the cookie. Meaning that if the cookie expires + due to a lower ``max_age``, then this setting has no effect. + Default: ``1200``. + + ``reissue_time`` + The number of seconds that must pass before the cookie is automatically + reissued as the result of a request which accesses the session. The + duration is measured as the number of seconds since the last session + cookie was issued and 'now'. If this value is ``0``, a new cookie + will be reissued on every request accessing the session. If ``None`` + then the cookie's lifetime will never be extended. + + A good rule of thumb: if you want auto-expired cookies based on + inactivity: set the ``timeout`` value to 1200 (20 mins) and set the + ``reissue_time`` value to perhaps a tenth of the ``timeout`` value + (120 or 2 mins). It's nonsensical to set the ``timeout`` value lower + than the ``reissue_time`` value, as the ticket will never be reissued. + However, such a configuration is not explicitly prevented. + + Default: ``0``. + + ``set_on_exception`` + If ``True``, set a session cookie even if an exception occurs + while rendering a view. Default: ``True``. + + .. versionadded: 1.5a3 + + .. versionchanged: 1.10 + + Added the ``samesite`` option and made the default ``'Lax'``. + """ + + @implementer(ISession) + class CookieSession(dict): + """ Dictionary-like session object """ + + # configuration parameters + _cookie_name = cookie_name + _cookie_max_age = max_age if max_age is None else int(max_age) + _cookie_path = path + _cookie_domain = domain + _cookie_secure = secure + _cookie_httponly = httponly + _cookie_samesite = samesite + _cookie_on_exception = set_on_exception + _timeout = timeout if timeout is None else int(timeout) + _reissue_time = reissue_time if reissue_time is None else int(reissue_time) + + # dirty flag + _dirty = False + + def __init__(self, request): + self.request = request + now = time.time() + created = renewed = now + new = True + value = None + state = {} + cookieval = request.cookies.get(self._cookie_name) + if cookieval is not None: + try: + value = serializer.loads(bytes_(cookieval)) + except ValueError: + # the cookie failed to deserialize, dropped + value = None + + if value is not None: + try: + # since the value is not necessarily signed, we have + # to unpack it a little carefully + rval, cval, sval = value + renewed = float(rval) + created = float(cval) + state = sval + new = False + except (TypeError, ValueError): + # value failed to unpack properly or renewed was not + # a numeric type so we'll fail deserialization here + state = {} + + if self._timeout is not None: + if now - renewed > self._timeout: + # expire the session because it was not renewed + # before the timeout threshold + state = {} + + self.created = created + self.accessed = renewed + self.renewed = renewed + self.new = new + dict.__init__(self, state) + + # ISession methods + def changed(self): + if not self._dirty: + self._dirty = True + def set_cookie_callback(request, response): + self._set_cookie(response) + self.request = None # explicitly break cycle for gc + self.request.add_response_callback(set_cookie_callback) + + def invalidate(self): + self.clear() # XXX probably needs to unset cookie + + # non-modifying dictionary methods + get = manage_accessed(dict.get) + __getitem__ = manage_accessed(dict.__getitem__) + items = manage_accessed(dict.items) + values = manage_accessed(dict.values) + keys = manage_accessed(dict.keys) + __contains__ = manage_accessed(dict.__contains__) + __len__ = manage_accessed(dict.__len__) + __iter__ = manage_accessed(dict.__iter__) + + if PY2: + iteritems = manage_accessed(dict.iteritems) + itervalues = manage_accessed(dict.itervalues) + iterkeys = manage_accessed(dict.iterkeys) + has_key = manage_accessed(dict.has_key) + + # modifying dictionary methods + clear = manage_changed(dict.clear) + update = manage_changed(dict.update) + setdefault = manage_changed(dict.setdefault) + pop = manage_changed(dict.pop) + popitem = manage_changed(dict.popitem) + __setitem__ = manage_changed(dict.__setitem__) + __delitem__ = manage_changed(dict.__delitem__) + + # flash API methods + @manage_changed + def flash(self, msg, queue='', allow_duplicate=True): + storage = self.setdefault('_f_' + queue, []) + if allow_duplicate or (msg not in storage): + storage.append(msg) + + @manage_changed + def pop_flash(self, queue=''): + storage = self.pop('_f_' + queue, []) + return storage + + @manage_accessed + def peek_flash(self, queue=''): + storage = self.get('_f_' + queue, []) + return storage + + # CSRF API methods + @manage_changed + def new_csrf_token(self): + token = text_(binascii.hexlify(os.urandom(20))) + self['_csrft_'] = token + return token + + @manage_accessed + def get_csrf_token(self): + token = self.get('_csrft_', None) + if token is None: + token = self.new_csrf_token() + return token + + # non-API methods + def _set_cookie(self, response): + if not self._cookie_on_exception: + exception = getattr(self.request, 'exception', None) + if exception is not None: # dont set a cookie during exceptions + return False + cookieval = native_(serializer.dumps( + (self.accessed, self.created, dict(self)) + )) + if len(cookieval) > 4064: + raise ValueError( + 'Cookie value is too long to store (%s bytes)' % + len(cookieval) + ) + response.set_cookie( + self._cookie_name, + value=cookieval, + max_age=self._cookie_max_age, + path=self._cookie_path, + domain=self._cookie_domain, + secure=self._cookie_secure, + httponly=self._cookie_httponly, + samesite=self._cookie_samesite, + ) + return True + + return CookieSession + + +def UnencryptedCookieSessionFactoryConfig( + secret, + timeout=1200, + cookie_name='session', + cookie_max_age=None, + cookie_path='/', + cookie_domain=None, + cookie_secure=False, + cookie_httponly=False, + cookie_samesite='Lax', + cookie_on_exception=True, + signed_serialize=signed_serialize, + signed_deserialize=signed_deserialize, + ): + """ + .. deprecated:: 1.5 + Use :func:`pyramid.session.SignedCookieSessionFactory` instead. + Caveat: Cookies generated using ``SignedCookieSessionFactory`` are not + compatible with cookies generated using + ``UnencryptedCookieSessionFactory``, so existing user session data + will be destroyed if you switch to it. + + Configure a :term:`session factory` which will provide unencrypted + (but signed) cookie-based sessions. The return value of this + function is a :term:`session factory`, which may be provided as + the ``session_factory`` argument of a + :class:`pyramid.config.Configurator` constructor, or used + as the ``session_factory`` argument of the + :meth:`pyramid.config.Configurator.set_session_factory` + method. + + The session factory returned by this function will create sessions + which are limited to storing fewer than 4000 bytes of data (as the + payload must fit into a single cookie). + + Parameters: + + ``secret`` + A string which is used to sign the cookie. + + ``timeout`` + A number of seconds of inactivity before a session times out. + + ``cookie_name`` + The name of the cookie used for sessioning. + + ``cookie_max_age`` + The maximum age of the cookie used for sessioning (in seconds). + Default: ``None`` (browser scope). + + ``cookie_path`` + The path used for the session cookie. + + ``cookie_domain`` + The domain used for the session cookie. Default: ``None`` (no domain). + + ``cookie_secure`` + The 'secure' flag of the session cookie. + + ``cookie_httponly`` + The 'httpOnly' flag of the session cookie. + + ``cookie_samesite`` + The 'samesite' option of the session cookie. Set the value to ``None`` + to turn off the samesite option. Default: ``'Lax'``. + + ``cookie_on_exception`` + If ``True``, set a session cookie even if an exception occurs + while rendering a view. + + ``signed_serialize`` + A callable which takes more or less arbitrary Python data structure and + a secret and returns a signed serialization in bytes. + Default: ``signed_serialize`` (using pickle). + + ``signed_deserialize`` + A callable which takes a signed and serialized data structure in bytes + and a secret and returns the original data structure if the signature + is valid. Default: ``signed_deserialize`` (using pickle). + + .. versionchanged: 1.10 + + Added the ``samesite`` option and made the default ``'Lax'``. + """ + + class SerializerWrapper(object): + def __init__(self, secret): + self.secret = secret + + def loads(self, bstruct): + return signed_deserialize(bstruct, secret) + + def dumps(self, appstruct): + return signed_serialize(appstruct, secret) + + serializer = SerializerWrapper(secret) + + return BaseCookieSessionFactory( + serializer, + cookie_name=cookie_name, + max_age=cookie_max_age, + path=cookie_path, + domain=cookie_domain, + secure=cookie_secure, + httponly=cookie_httponly, + samesite=cookie_samesite, + timeout=timeout, + reissue_time=0, # to keep session.accessed == session.renewed + set_on_exception=cookie_on_exception, + ) + +deprecated( + 'UnencryptedCookieSessionFactoryConfig', + 'The UnencryptedCookieSessionFactoryConfig callable is deprecated as of ' + 'Pyramid 1.5. Use ``pyramid.session.SignedCookieSessionFactory`` instead.' + ' Caveat: Cookies generated using SignedCookieSessionFactory are not ' + 'compatible with cookies generated using UnencryptedCookieSessionFactory, ' + 'so existing user session data will be destroyed if you switch to it.' + ) + + +def SignedCookieSessionFactory( + secret, + cookie_name='session', + max_age=None, + path='/', + domain=None, + secure=False, + httponly=False, + samesite='Lax', + set_on_exception=True, + timeout=1200, + reissue_time=0, + hashalg='sha512', + salt='pyramid.session.', + serializer=None, + ): + """ + .. versionadded:: 1.5 + + Configure a :term:`session factory` which will provide signed + cookie-based sessions. The return value of this + function is a :term:`session factory`, which may be provided as + the ``session_factory`` argument of a + :class:`pyramid.config.Configurator` constructor, or used + as the ``session_factory`` argument of the + :meth:`pyramid.config.Configurator.set_session_factory` + method. + + The session factory returned by this function will create sessions + which are limited to storing fewer than 4000 bytes of data (as the + payload must fit into a single cookie). + + Parameters: + + ``secret`` + A string which is used to sign the cookie. The secret should be at + least as long as the block size of the selected hash algorithm. For + ``sha512`` this would mean a 512 bit (64 character) secret. It should + be unique within the set of secret values provided to Pyramid for + its various subsystems (see :ref:`admonishment_against_secret_sharing`). + + ``hashalg`` + The HMAC digest algorithm to use for signing. The algorithm must be + supported by the :mod:`hashlib` library. Default: ``'sha512'``. + + ``salt`` + A namespace to avoid collisions between different uses of a shared + secret. Reusing a secret for different parts of an application is + strongly discouraged (see :ref:`admonishment_against_secret_sharing`). + Default: ``'pyramid.session.'``. + + ``cookie_name`` + The name of the cookie used for sessioning. Default: ``'session'``. + + ``max_age`` + The maximum age of the cookie used for sessioning (in seconds). + Default: ``None`` (browser scope). + + ``path`` + The path used for the session cookie. Default: ``'/'``. + + ``domain`` + The domain used for the session cookie. Default: ``None`` (no domain). + + ``secure`` + The 'secure' flag of the session cookie. Default: ``False``. + + ``httponly`` + Hide the cookie from Javascript by setting the 'HttpOnly' flag of the + session cookie. Default: ``False``. + + ``samesite`` + The 'samesite' option of the session cookie. Set the value to ``None`` + to turn off the samesite option. Default: ``'Lax'``. + + ``timeout`` + A number of seconds of inactivity before a session times out. If + ``None`` then the cookie never expires. This lifetime only applies + to the *value* within the cookie. Meaning that if the cookie expires + due to a lower ``max_age``, then this setting has no effect. + Default: ``1200``. + + ``reissue_time`` + The number of seconds that must pass before the cookie is automatically + reissued as the result of accessing the session. The + duration is measured as the number of seconds since the last session + cookie was issued and 'now'. If this value is ``0``, a new cookie + will be reissued on every request accessing the session. If ``None`` + then the cookie's lifetime will never be extended. + + A good rule of thumb: if you want auto-expired cookies based on + inactivity: set the ``timeout`` value to 1200 (20 mins) and set the + ``reissue_time`` value to perhaps a tenth of the ``timeout`` value + (120 or 2 mins). It's nonsensical to set the ``timeout`` value lower + than the ``reissue_time`` value, as the ticket will never be reissued. + However, such a configuration is not explicitly prevented. + + Default: ``0``. + + ``set_on_exception`` + If ``True``, set a session cookie even if an exception occurs + while rendering a view. Default: ``True``. + + ``serializer`` + An object with two methods: ``loads`` and ``dumps``. The ``loads`` + method should accept bytes and return a Python object. The ``dumps`` + method should accept a Python object and return bytes. A ``ValueError`` + should be raised for malformed inputs. If a serializer is not passed, + the :class:`pyramid.session.PickleSerializer` serializer will be used. + + .. warning:: + + In :app:`Pyramid` 2.0 the default ``serializer`` option will change to + use :class:`pyramid.session.JSONSerializer`. See + :ref:`pickle_session_deprecation` for more information about why this + change is being made. + + .. versionadded: 1.5a3 + + .. versionchanged: 1.10 + + Added the ``samesite`` option and made the default ``Lax``. + + """ + if serializer is None: + serializer = PickleSerializer() + warnings.warn( + 'The default pickle serializer is deprecated as of Pyramid 1.9 ' + 'and it will be changed to use pyramid.session.JSONSerializer in ' + 'version 2.0. Explicitly set the serializer to avoid future ' + 'incompatibilities. See "Upcoming Changes to ISession in ' + 'Pyramid 2.0" for more information about this change.', + DeprecationWarning, + stacklevel=1, + ) + + signed_serializer = SignedSerializer( + secret, + salt, + hashalg, + serializer=serializer, + ) + + return BaseCookieSessionFactory( + signed_serializer, + cookie_name=cookie_name, + max_age=max_age, + path=path, + domain=domain, + secure=secure, + httponly=httponly, + samesite=samesite, + timeout=timeout, + reissue_time=reissue_time, + set_on_exception=set_on_exception, + ) + +check_csrf_origin = check_csrf_origin # api +deprecated('check_csrf_origin', + 'pyramid.session.check_csrf_origin is deprecated as of Pyramid ' + '1.9. Use pyramid.csrf.check_csrf_origin instead.') + +check_csrf_token = check_csrf_token # api +deprecated('check_csrf_token', + 'pyramid.session.check_csrf_token is deprecated as of Pyramid ' + '1.9. Use pyramid.csrf.check_csrf_token instead.') diff --git a/src/pyramid/settings.py b/src/pyramid/settings.py new file mode 100644 index 000000000..8a498d572 --- /dev/null +++ b/src/pyramid/settings.py @@ -0,0 +1,33 @@ +from pyramid.compat import string_types + +truthy = frozenset(('t', 'true', 'y', 'yes', 'on', '1')) +falsey = frozenset(('f', 'false', 'n', 'no', 'off', '0')) + +def asbool(s): + """ Return the boolean value ``True`` if the case-lowered value of string + input ``s`` is a :term:`truthy string`. If ``s`` is already one of the + boolean values ``True`` or ``False``, return it.""" + if s is None: + return False + if isinstance(s, bool): + return s + s = str(s).strip() + return s.lower() in truthy + +def aslist_cronly(value): + if isinstance(value, string_types): + value = filter(None, [x.strip() for x in value.splitlines()]) + return list(value) + +def aslist(value, flatten=True): + """ Return a list of strings, separating the input based on newlines + and, if flatten=True (the default), also split on spaces within + each line.""" + values = aslist_cronly(value) + if not flatten: + return values + result = [] + for value in values: + subvalues = value.split() + result.extend(subvalues) + return result diff --git a/src/pyramid/static.py b/src/pyramid/static.py new file mode 100644 index 000000000..70fdf877b --- /dev/null +++ b/src/pyramid/static.py @@ -0,0 +1,301 @@ +# -*- coding: utf-8 -*- +import json +import os + +from os.path import ( + getmtime, + normcase, + normpath, + join, + isdir, + exists, + ) + +from pkg_resources import ( + resource_exists, + resource_filename, + resource_isdir, + ) + +from pyramid.asset import ( + abspath_from_asset_spec, + resolve_asset_spec, +) + +from pyramid.compat import ( + lru_cache, + text_, +) + +from pyramid.httpexceptions import ( + HTTPNotFound, + HTTPMovedPermanently, + ) + +from pyramid.path import caller_package + +from pyramid.response import ( + _guess_type, + FileResponse, +) + +from pyramid.traversal import traversal_path_info + +slash = text_('/') + +class static_view(object): + """ An instance of this class is a callable which can act as a + :app:`Pyramid` :term:`view callable`; this view will serve + static files from a directory on disk based on the ``root_dir`` + you provide to its constructor. + + The directory may contain subdirectories (recursively); the static + view implementation will descend into these directories as + necessary based on the components of the URL in order to resolve a + path into a response. + + You may pass an absolute or relative filesystem path or a + :term:`asset specification` representing the directory + containing static files as the ``root_dir`` argument to this + class' constructor. + + If the ``root_dir`` path is relative, and the ``package_name`` + argument is ``None``, ``root_dir`` will be considered relative to + the directory in which the Python file which *calls* ``static`` + resides. If the ``package_name`` name argument is provided, and a + relative ``root_dir`` is provided, the ``root_dir`` will be + considered relative to the Python :term:`package` specified by + ``package_name`` (a dotted path to a Python package). + + ``cache_max_age`` influences the ``Expires`` and ``Max-Age`` + response headers returned by the view (default is 3600 seconds or + one hour). + + ``use_subpath`` influences whether ``request.subpath`` will be used as + ``PATH_INFO`` when calling the underlying WSGI application which actually + serves the static files. If it is ``True``, the static application will + consider ``request.subpath`` as ``PATH_INFO`` input. If it is ``False``, + the static application will consider request.environ[``PATH_INFO``] as + ``PATH_INFO`` input. By default, this is ``False``. + + .. note:: + + If the ``root_dir`` is relative to a :term:`package`, or is a + :term:`asset specification` the :app:`Pyramid` + :class:`pyramid.config.Configurator` method can be used to override + assets within the named ``root_dir`` package-relative directory. + However, if the ``root_dir`` is absolute, configuration will not be able + to override the assets it contains. + """ + + def __init__(self, root_dir, cache_max_age=3600, package_name=None, + use_subpath=False, index='index.html'): + # package_name is for bw compat; it is preferred to pass in a + # package-relative path as root_dir + # (e.g. ``anotherpackage:foo/static``). + self.cache_max_age = cache_max_age + if package_name is None: + package_name = caller_package().__name__ + package_name, docroot = resolve_asset_spec(root_dir, package_name) + self.use_subpath = use_subpath + self.package_name = package_name + self.docroot = docroot + self.norm_docroot = normcase(normpath(docroot)) + self.index = index + + def __call__(self, context, request): + if self.use_subpath: + path_tuple = request.subpath + else: + path_tuple = traversal_path_info(request.environ['PATH_INFO']) + path = _secure_path(path_tuple) + + if path is None: + raise HTTPNotFound('Out of bounds: %s' % request.url) + + if self.package_name: # package resource + resource_path = '%s/%s' % (self.docroot.rstrip('/'), path) + if resource_isdir(self.package_name, resource_path): + if not request.path_url.endswith('/'): + self.add_slash_redirect(request) + resource_path = '%s/%s' % ( + resource_path.rstrip('/'), self.index + ) + + if not resource_exists(self.package_name, resource_path): + raise HTTPNotFound(request.url) + filepath = resource_filename(self.package_name, resource_path) + + else: # filesystem file + + # os.path.normpath converts / to \ on windows + filepath = normcase(normpath(join(self.norm_docroot, path))) + if isdir(filepath): + if not request.path_url.endswith('/'): + self.add_slash_redirect(request) + filepath = join(filepath, self.index) + if not exists(filepath): + raise HTTPNotFound(request.url) + + content_type, content_encoding = _guess_type(filepath) + return FileResponse( + filepath, request, self.cache_max_age, + content_type, content_encoding=None) + + def add_slash_redirect(self, request): + url = request.path_url + '/' + qs = request.query_string + if qs: + url = url + '?' + qs + raise HTTPMovedPermanently(url) + +_seps = set(['/', os.sep]) +def _contains_slash(item): + for sep in _seps: + if sep in item: + return True + +_has_insecure_pathelement = set(['..', '.', '']).intersection + +@lru_cache(1000) +def _secure_path(path_tuple): + if _has_insecure_pathelement(path_tuple): + # belt-and-suspenders security; this should never be true + # unless someone screws up the traversal_path code + # (request.subpath is computed via traversal_path too) + return None + if any([_contains_slash(item) for item in path_tuple]): + return None + encoded = slash.join(path_tuple) # will be unicode + return encoded + +class QueryStringCacheBuster(object): + """ + An implementation of :class:`~pyramid.interfaces.ICacheBuster` which adds + a token for cache busting in the query string of an asset URL. + + The optional ``param`` argument determines the name of the parameter added + to the query string and defaults to ``'x'``. + + To use this class, subclass it and provide a ``tokenize`` method which + accepts ``request, pathspec, kw`` and returns a token. + + .. versionadded:: 1.6 + """ + def __init__(self, param='x'): + self.param = param + + def __call__(self, request, subpath, kw): + token = self.tokenize(request, subpath, kw) + query = kw.setdefault('_query', {}) + if isinstance(query, dict): + query[self.param] = token + else: + kw['_query'] = tuple(query) + ((self.param, token),) + return subpath, kw + +class QueryStringConstantCacheBuster(QueryStringCacheBuster): + """ + An implementation of :class:`~pyramid.interfaces.ICacheBuster` which adds + an arbitrary token for cache busting in the query string of an asset URL. + + The ``token`` parameter is the token string to use for cache busting and + will be the same for every request. + + The optional ``param`` argument determines the name of the parameter added + to the query string and defaults to ``'x'``. + + .. versionadded:: 1.6 + """ + def __init__(self, token, param='x'): + super(QueryStringConstantCacheBuster, self).__init__(param=param) + self._token = token + + def tokenize(self, request, subpath, kw): + return self._token + +class ManifestCacheBuster(object): + """ + An implementation of :class:`~pyramid.interfaces.ICacheBuster` which + uses a supplied manifest file to map an asset path to a cache-busted + version of the path. + + The ``manifest_spec`` can be an absolute path or a :term:`asset + specification` pointing to a package-relative file. + + The manifest file is expected to conform to the following simple JSON + format: + + .. code-block:: json + + { + "css/main.css": "css/main-678b7c80.css", + "images/background.png": "images/background-a8169106.png", + } + + By default, it is a JSON-serialized dictionary where the keys are the + source asset paths used in calls to + :meth:`~pyramid.request.Request.static_url`. For example: + + .. code-block:: pycon + + >>> request.static_url('myapp:static/css/main.css') + "http://www.example.com/static/css/main-678b7c80.css" + + The file format and location can be changed by subclassing and overriding + :meth:`.parse_manifest`. + + If a path is not found in the manifest it will pass through unchanged. + + If ``reload`` is ``True`` then the manifest file will be reloaded when + changed. It is not recommended to leave this enabled in production. + + If the manifest file cannot be found on disk it will be treated as + an empty mapping unless ``reload`` is ``False``. + + .. versionadded:: 1.6 + """ + exists = staticmethod(exists) # testing + getmtime = staticmethod(getmtime) # testing + + def __init__(self, manifest_spec, reload=False): + package_name = caller_package().__name__ + self.manifest_path = abspath_from_asset_spec( + manifest_spec, package_name) + self.reload = reload + + self._mtime = None + if not reload: + self._manifest = self.get_manifest() + + def get_manifest(self): + with open(self.manifest_path, 'rb') as fp: + return self.parse_manifest(fp.read()) + + def parse_manifest(self, content): + """ + Parse the ``content`` read from the ``manifest_path`` into a + dictionary mapping. + + Subclasses may override this method to use something other than + ``json.loads`` to load any type of file format and return a conforming + dictionary. + + """ + return json.loads(content.decode('utf-8')) + + @property + def manifest(self): + """ The current manifest dictionary.""" + if self.reload: + if not self.exists(self.manifest_path): + return {} + mtime = self.getmtime(self.manifest_path) + if self._mtime is None or mtime > self._mtime: + self._manifest = self.get_manifest() + self._mtime = mtime + return self._manifest + + def __call__(self, request, subpath, kw): + subpath = self.manifest.get(subpath, subpath) + return (subpath, kw) diff --git a/src/pyramid/testing.py b/src/pyramid/testing.py new file mode 100644 index 000000000..4986c0e27 --- /dev/null +++ b/src/pyramid/testing.py @@ -0,0 +1,641 @@ +import copy +import os +from contextlib import contextmanager + +from zope.interface import ( + implementer, + alsoProvides, + ) + +from pyramid.interfaces import ( + IRequest, + ISession, + ) + +from pyramid.compat import ( + PY3, + PYPY, + class_types, + text_, + ) + +from pyramid.config import Configurator +from pyramid.decorator import reify +from pyramid.path import caller_package +from pyramid.response import _get_response_factory +from pyramid.registry import Registry + +from pyramid.security import ( + Authenticated, + Everyone, + AuthenticationAPIMixin, + AuthorizationAPIMixin, + ) + +from pyramid.threadlocal import ( + get_current_registry, + manager, + ) + +from pyramid.i18n import LocalizerRequestMixin +from pyramid.request import CallbackMethodsMixin +from pyramid.url import URLMethodsMixin +from pyramid.util import InstancePropertyMixin +from pyramid.view import ViewMethodsMixin + + +_marker = object() + +class DummyRootFactory(object): + __parent__ = None + __name__ = None + def __init__(self, request): + if 'bfg.routes.matchdict' in request: + self.__dict__.update(request['bfg.routes.matchdict']) + +class DummySecurityPolicy(object): + """ A standin for both an IAuthentication and IAuthorization policy """ + def __init__(self, userid=None, groupids=(), permissive=True, + remember_result=None, forget_result=None): + self.userid = userid + self.groupids = groupids + self.permissive = permissive + if remember_result is None: + remember_result = [] + if forget_result is None: + forget_result = [] + self.remember_result = remember_result + self.forget_result = forget_result + + def authenticated_userid(self, request): + return self.userid + + def unauthenticated_userid(self, request): + return self.userid + + def effective_principals(self, request): + effective_principals = [Everyone] + if self.userid: + effective_principals.append(Authenticated) + effective_principals.append(self.userid) + effective_principals.extend(self.groupids) + return effective_principals + + def remember(self, request, userid, **kw): + self.remembered = userid + return self.remember_result + + def forget(self, request): + self.forgotten = True + return self.forget_result + + def permits(self, context, principals, permission): + return self.permissive + + def principals_allowed_by_permission(self, context, permission): + return self.effective_principals(None) + +class DummyTemplateRenderer(object): + """ + An instance of this class is returned from + :meth:`pyramid.config.Configurator.testing_add_renderer`. It has a + helper function (``assert_``) that makes it possible to make an + assertion which compares data passed to the renderer by the view + function against expected key/value pairs. + """ + def __init__(self, string_response=''): + self._received = {} + self._string_response = string_response + self._implementation = MockTemplate(string_response) + + # For in-the-wild test code that doesn't create its own renderer, + # but mutates our internals instead. When all you read is the + # source code, *everything* is an API! + def _get_string_response(self): + return self._string_response + def _set_string_response(self, response): + self._string_response = response + self._implementation.response = response + string_response = property(_get_string_response, _set_string_response) + + def implementation(self): + return self._implementation + + def __call__(self, kw, system=None): + if system: + self._received.update(system) + self._received.update(kw) + return self.string_response + + def __getattr__(self, k): + """ Backwards compatibility """ + val = self._received.get(k, _marker) + if val is _marker: + val = self._implementation._received.get(k, _marker) + if val is _marker: + raise AttributeError(k) + return val + + def assert_(self, **kw): + """ Accept an arbitrary set of assertion key/value pairs. For + each assertion key/value pair assert that the renderer + (eg. :func:`pyramid.renderers.render_to_response`) + received the key with a value that equals the asserted + value. If the renderer did not receive the key at all, or the + value received by the renderer doesn't match the assertion + value, raise an :exc:`AssertionError`.""" + for k, v in kw.items(): + myval = self._received.get(k, _marker) + if myval is _marker: + myval = self._implementation._received.get(k, _marker) + if myval is _marker: + raise AssertionError( + 'A value for key "%s" was not passed to the renderer' + % k) + + if myval != v: + raise AssertionError( + '\nasserted value for %s: %r\nactual value: %r' % ( + k, v, myval)) + return True + + +class DummyResource: + """ A dummy :app:`Pyramid` :term:`resource` object.""" + def __init__(self, __name__=None, __parent__=None, __provides__=None, + **kw): + """ The resource's ``__name__`` attribute will be set to the + value of the ``__name__`` argument, and the resource's + ``__parent__`` attribute will be set to the value of the + ``__parent__`` argument. If ``__provides__`` is specified, it + should be an interface object or tuple of interface objects + that will be attached to the resulting resource via + :func:`zope.interface.alsoProvides`. Any extra keywords passed + in the ``kw`` argumnent will be set as direct attributes of + the resource object. + + .. note:: For backwards compatibility purposes, this class can also + be imported as :class:`pyramid.testing.DummyModel`. + + """ + self.__name__ = __name__ + self.__parent__ = __parent__ + if __provides__ is not None: + alsoProvides(self, __provides__) + self.kw = kw + self.__dict__.update(**kw) + self.subs = {} + + def __setitem__(self, name, val): + """ When the ``__setitem__`` method is called, the object + passed in as ``val`` will be decorated with a ``__parent__`` + attribute pointing at the dummy resource and a ``__name__`` + attribute that is the value of ``name``. The value will then + be returned when dummy resource's ``__getitem__`` is called with + the name ``name```.""" + val.__name__ = name + val.__parent__ = self + self.subs[name] = val + + def __getitem__(self, name): + """ Return a named subobject (see ``__setitem__``)""" + ob = self.subs[name] + return ob + + def __delitem__(self, name): + del self.subs[name] + + def get(self, name, default=None): + return self.subs.get(name, default) + + def values(self): + """ Return the values set by __setitem__ """ + return self.subs.values() + + def items(self): + """ Return the items set by __setitem__ """ + return self.subs.items() + + def keys(self): + """ Return the keys set by __setitem__ """ + return self.subs.keys() + + __iter__ = keys + + def __nonzero__(self): + return True + + __bool__ = __nonzero__ + + def __len__(self): + return len(self.subs) + + def __contains__(self, name): + return name in self.subs + + def clone(self, __name__=_marker, __parent__=_marker, **kw): + """ Create a clone of the resource object. If ``__name__`` or + ``__parent__`` arguments are passed, use these values to + override the existing ``__name__`` or ``__parent__`` of the + resource. If any extra keyword args are passed in via the ``kw`` + argument, use these keywords to add to or override existing + resource keywords (attributes).""" + oldkw = self.kw.copy() + oldkw.update(kw) + inst = self.__class__(self.__name__, self.__parent__, **oldkw) + inst.subs = copy.deepcopy(self.subs) + if __name__ is not _marker: + inst.__name__ = __name__ + if __parent__ is not _marker: + inst.__parent__ = __parent__ + return inst + +DummyModel = DummyResource # b/w compat (forever) + +@implementer(ISession) +class DummySession(dict): + created = None + new = True + def changed(self): + pass + + def invalidate(self): + self.clear() + + def flash(self, msg, queue='', allow_duplicate=True): + storage = self.setdefault('_f_' + queue, []) + if allow_duplicate or (msg not in storage): + storage.append(msg) + + def pop_flash(self, queue=''): + storage = self.pop('_f_' + queue, []) + return storage + + def peek_flash(self, queue=''): + storage = self.get('_f_' + queue, []) + return storage + + def new_csrf_token(self): + token = text_('0123456789012345678901234567890123456789') + self['_csrft_'] = token + return token + + def get_csrf_token(self): + token = self.get('_csrft_', None) + if token is None: + token = self.new_csrf_token() + return token + +@implementer(IRequest) +class DummyRequest( + URLMethodsMixin, + CallbackMethodsMixin, + InstancePropertyMixin, + LocalizerRequestMixin, + AuthenticationAPIMixin, + AuthorizationAPIMixin, + ViewMethodsMixin, + ): + """ A DummyRequest object (incompletely) imitates a :term:`request` object. + + The ``params``, ``environ``, ``headers``, ``path``, and + ``cookies`` arguments correspond to their :term:`WebOb` + equivalents. + + The ``post`` argument, if passed, populates the request's + ``POST`` attribute, but *not* ``params``, in order to allow testing + that the app accepts data for a given view only from POST requests. + This argument also sets ``self.method`` to "POST". + + Extra keyword arguments are assigned as attributes of the request + itself. + + Note that DummyRequest does not have complete fidelity with a "real" + request. For example, by default, the DummyRequest ``GET`` and ``POST`` + attributes are of type ``dict``, unlike a normal Request's GET and POST, + which are of type ``MultiDict``. If your code uses the features of + MultiDict, you should either use a real :class:`pyramid.request.Request` + or adapt your DummyRequest by replacing the attributes with ``MultiDict`` + instances. + + Other similar incompatibilities exist. If you need all the features of + a Request, use the :class:`pyramid.request.Request` class itself rather + than this class while writing tests. + """ + method = 'GET' + application_url = 'http://example.com' + host = 'example.com:80' + domain = 'example.com' + content_length = 0 + query_string = '' + charset = 'UTF-8' + script_name = '' + _registry = None + request_iface = IRequest + + def __init__(self, params=None, environ=None, headers=None, path='/', + cookies=None, post=None, **kw): + if environ is None: + environ = {} + if params is None: + params = {} + if headers is None: + headers = {} + if cookies is None: + cookies = {} + self.environ = environ + self.headers = headers + self.params = params + self.cookies = cookies + self.matchdict = {} + self.GET = params + if post is not None: + self.method = 'POST' + self.POST = post + else: + self.POST = params + self.host_url = self.application_url + self.path_url = self.application_url + self.url = self.application_url + self.path = path + self.path_info = path + self.script_name = '' + self.path_qs = '' + self.body = '' + self.view_name = '' + self.subpath = () + self.traversed = () + self.virtual_root_path = () + self.context = None + self.root = None + self.virtual_root = None + self.marshalled = params # repoze.monty + self.session = DummySession() + self.__dict__.update(kw) + + def _get_registry(self): + if self._registry is None: + return get_current_registry() + return self._registry + + def _set_registry(self, registry): + self._registry = registry + + def _del_registry(self): + self._registry = None + + registry = property(_get_registry, _set_registry, _del_registry) + + @reify + def response(self): + f = _get_response_factory(self.registry) + return f(self) + +have_zca = True + + +def setUp(registry=None, request=None, hook_zca=True, autocommit=True, + settings=None, package=None): + """ + Set :app:`Pyramid` registry and request thread locals for the + duration of a single unit test. + + Use this function in the ``setUp`` method of a unittest test case + which directly or indirectly uses: + + - any method of the :class:`pyramid.config.Configurator` + object returned by this function. + + - the :func:`pyramid.threadlocal.get_current_registry` or + :func:`pyramid.threadlocal.get_current_request` functions. + + If you use the ``get_current_*`` functions (or call :app:`Pyramid` code + that uses these functions) without calling ``setUp``, + :func:`pyramid.threadlocal.get_current_registry` will return a *global* + :term:`application registry`, which may cause unit tests to not be + isolated with respect to registrations they perform. + + If the ``registry`` argument is ``None``, a new empty + :term:`application registry` will be created (an instance of the + :class:`pyramid.registry.Registry` class). If the ``registry`` + argument is not ``None``, the value passed in should be an + instance of the :class:`pyramid.registry.Registry` class or a + suitable testing analogue. + + After ``setUp`` is finished, the registry returned by the + :func:`pyramid.threadlocal.get_current_registry` function will + be the passed (or constructed) registry until + :func:`pyramid.testing.tearDown` is called (or + :func:`pyramid.testing.setUp` is called again) . + + If the ``hook_zca`` argument is ``True``, ``setUp`` will attempt + to perform the operation ``zope.component.getSiteManager.sethook( + pyramid.threadlocal.get_current_registry)``, which will cause + the :term:`Zope Component Architecture` global API + (e.g. :func:`zope.component.getSiteManager`, + :func:`zope.component.getAdapter`, and so on) to use the registry + constructed by ``setUp`` as the value it returns from + :func:`zope.component.getSiteManager`. If the + :mod:`zope.component` package cannot be imported, or if + ``hook_zca`` is ``False``, the hook will not be set. + + If ``settings`` is not ``None``, it must be a dictionary representing the + values passed to a Configurator as its ``settings=`` argument. + + If ``package`` is ``None`` it will be set to the caller's package. The + ``package`` setting in the :class:`pyramid.config.Configurator` will + affect any relative imports made via + :meth:`pyramid.config.Configurator.include` or + :meth:`pyramid.config.Configurator.maybe_dotted`. + + This function returns an instance of the + :class:`pyramid.config.Configurator` class, which can be + used for further configuration to set up an environment suitable + for a unit or integration test. The ``registry`` attribute + attached to the Configurator instance represents the 'current' + :term:`application registry`; the same registry will be returned + by :func:`pyramid.threadlocal.get_current_registry` during the + execution of the test. + """ + manager.clear() + if registry is None: + registry = Registry('testing') + if package is None: + package = caller_package() + config = Configurator(registry=registry, autocommit=autocommit, + package=package) + if settings is None: + settings = {} + if getattr(registry, 'settings', None) is None: + config._set_settings(settings) + if hasattr(registry, 'registerUtility'): + # Sometimes nose calls us with a non-registry object because + # it thinks this function is module test setup. Likewise, + # someone may be passing us an esoteric "dummy" registry, and + # the below won't succeed if it doesn't have a registerUtility + # method. + config.add_default_response_adapters() + config.add_default_renderers() + config.add_default_accept_view_order() + config.add_default_view_predicates() + config.add_default_view_derivers() + config.add_default_route_predicates() + config.add_default_tweens() + config.add_default_security() + config.commit() + global have_zca + try: + have_zca and hook_zca and config.hook_zca() + except ImportError: # pragma: no cover + # (dont choke on not being able to import z.component) + have_zca = False + config.begin(request=request) + return config + +def tearDown(unhook_zca=True): + """Undo the effects of :func:`pyramid.testing.setUp`. Use this + function in the ``tearDown`` method of a unit test that uses + :func:`pyramid.testing.setUp` in its ``setUp`` method. + + If the ``unhook_zca`` argument is ``True`` (the default), call + :func:`zope.component.getSiteManager.reset`. This undoes the + action of :func:`pyramid.testing.setUp` when called with the + argument ``hook_zca=True``. If :mod:`zope.component` cannot be + imported, ``unhook_zca`` is set to ``False``. + """ + global have_zca + if unhook_zca and have_zca: + try: + from zope.component import getSiteManager + getSiteManager.reset() + except ImportError: # pragma: no cover + have_zca = False + info = manager.pop() + manager.clear() + if info is not None: + registry = info['registry'] + if hasattr(registry, '__init__') and hasattr(registry, '__name__'): + try: + registry.__init__(registry.__name__) + except TypeError: + # calling __init__ is largely for the benefit of + # people who want to use the global ZCA registry; + # however maybe somebody's using a registry we don't + # understand, let's not blow up + pass + +def cleanUp(*arg, **kw): + """ An alias for :func:`pyramid.testing.setUp`. """ + package = kw.get('package', None) + if package is None: + package = caller_package() + kw['package'] = package + return setUp(*arg, **kw) + +class DummyRendererFactory(object): + """ Registered by + :meth:`pyramid.config.Configurator.testing_add_renderer` as + a dummy renderer factory. The indecision about what to use as a + key (a spec vs. a relative name) is caused by test suites in the + wild believing they can register either. The ``factory`` argument + passed to this constructor is usually the *real* template renderer + factory, found when ``testing_add_renderer`` is called.""" + def __init__(self, name, factory): + self.name = name + self.factory = factory # the "real" renderer factory reg'd previously + self.renderers = {} + + def add(self, spec, renderer): + self.renderers[spec] = renderer + if ':' in spec: + package, relative = spec.split(':', 1) + self.renderers[relative] = renderer + + def __call__(self, info): + spec = info.name + renderer = self.renderers.get(spec) + if renderer is None: + if ':' in spec: + package, relative = spec.split(':', 1) + renderer = self.renderers.get(relative) + if renderer is None: + if self.factory: + renderer = self.factory(info) + else: + raise KeyError('No testing renderer registered for %r' % + spec) + return renderer + + +class MockTemplate(object): + def __init__(self, response): + self._received = {} + self.response = response + def __getattr__(self, attrname): + return self + def __getitem__(self, attrname): + return self + def __call__(self, *arg, **kw): + self._received.update(kw) + return self.response + +def skip_on(*platforms): # pragma: no cover + skip = False + for platform in platforms: + if skip_on.os_name.startswith(platform): + skip = True + if platform == 'pypy' and PYPY: + skip = True + if platform == 'py3' and PY3: + skip = True + + def decorator(func): + if isinstance(func, class_types): + if skip: + return None + else: + return func + else: + def wrapper(*args, **kw): + if skip: + return + return func(*args, **kw) + wrapper.__name__ = func.__name__ + wrapper.__doc__ = func.__doc__ + return wrapper + return decorator +skip_on.os_name = os.name # for testing + +@contextmanager +def testConfig(registry=None, + request=None, + hook_zca=True, + autocommit=True, + settings=None): + """Returns a context manager for test set up. + + This context manager calls :func:`pyramid.testing.setUp` when + entering and :func:`pyramid.testing.tearDown` when exiting. + + All arguments are passed directly to :func:`pyramid.testing.setUp`. + If the ZCA is hooked, it will always be un-hooked in tearDown. + + This context manager allows you to write test code like this: + + .. code-block:: python + :linenos: + + with testConfig() as config: + config.add_route('bar', '/bar/{id}') + req = DummyRequest() + resp = myview(req) + """ + config = setUp(registry=registry, + request=request, + hook_zca=hook_zca, + autocommit=autocommit, + settings=settings) + try: + yield config + finally: + tearDown(unhook_zca=hook_zca) diff --git a/src/pyramid/tests/__init__.py b/src/pyramid/tests/__init__.py new file mode 100644 index 000000000..a62c29f47 --- /dev/null +++ b/src/pyramid/tests/__init__.py @@ -0,0 +1,3 @@ + +def dummy_extend(*args): + """used to test Configurator.extend""" diff --git a/src/pyramid/tests/fixtures/dummy.ini b/src/pyramid/tests/fixtures/dummy.ini new file mode 100644 index 000000000..bc2281168 --- /dev/null +++ b/src/pyramid/tests/fixtures/dummy.ini @@ -0,0 +1,4 @@ +[app:myapp] +use = call:pyramid.tests.test_paster:make_dummyapp + +foo = %(bar)s diff --git a/src/pyramid/tests/fixtures/manifest.json b/src/pyramid/tests/fixtures/manifest.json new file mode 100644 index 000000000..0a43bc5e3 --- /dev/null +++ b/src/pyramid/tests/fixtures/manifest.json @@ -0,0 +1,4 @@ +{ + "css/main.css": "css/main-test.css", + "images/background.png": "images/background-a8169106.png" +} diff --git a/src/pyramid/tests/fixtures/manifest2.json b/src/pyramid/tests/fixtures/manifest2.json new file mode 100644 index 000000000..fd6b9a7bb --- /dev/null +++ b/src/pyramid/tests/fixtures/manifest2.json @@ -0,0 +1,4 @@ +{ + "css/main.css": "css/main-678b7c80.css", + "images/background.png": "images/background-a8169106.png" +} diff --git a/src/pyramid/tests/fixtures/minimal.jpg b/src/pyramid/tests/fixtures/minimal.jpg new file mode 100644 index 000000000..1cda9a53d Binary files /dev/null and b/src/pyramid/tests/fixtures/minimal.jpg differ diff --git a/src/pyramid/tests/fixtures/minimal.pdf b/src/pyramid/tests/fixtures/minimal.pdf new file mode 100755 index 000000000..e267be996 Binary files /dev/null and b/src/pyramid/tests/fixtures/minimal.pdf differ diff --git a/src/pyramid/tests/fixtures/minimal.txt b/src/pyramid/tests/fixtures/minimal.txt new file mode 100644 index 000000000..18832d351 --- /dev/null +++ b/src/pyramid/tests/fixtures/minimal.txt @@ -0,0 +1 @@ +Hello. diff --git a/src/pyramid/tests/fixtures/minimal.xml b/src/pyramid/tests/fixtures/minimal.xml new file mode 100644 index 000000000..1972c155d --- /dev/null +++ b/src/pyramid/tests/fixtures/minimal.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/pyramid/tests/fixtures/nonminimal.txt b/src/pyramid/tests/fixtures/nonminimal.txt new file mode 100644 index 000000000..9de95ec92 --- /dev/null +++ b/src/pyramid/tests/fixtures/nonminimal.txt @@ -0,0 +1 @@ +Hello, ${name}! diff --git a/src/pyramid/tests/fixtures/static/.hiddenfile b/src/pyramid/tests/fixtures/static/.hiddenfile new file mode 100644 index 000000000..86d345000 --- /dev/null +++ b/src/pyramid/tests/fixtures/static/.hiddenfile @@ -0,0 +1,2 @@ +I'm hidden + diff --git a/src/pyramid/tests/fixtures/static/arcs.svg.tgz b/src/pyramid/tests/fixtures/static/arcs.svg.tgz new file mode 100644 index 000000000..376c42ac8 --- /dev/null +++ b/src/pyramid/tests/fixtures/static/arcs.svg.tgz @@ -0,0 +1,73 @@ + + + + + + + + diff --git a/src/pyramid/tests/fixtures/static/index.html b/src/pyramid/tests/fixtures/static/index.html new file mode 100644 index 000000000..0470710b2 --- /dev/null +++ b/src/pyramid/tests/fixtures/static/index.html @@ -0,0 +1 @@ +static \ No newline at end of file diff --git a/src/pyramid/tests/fixtures/static/subdir/index.html b/src/pyramid/tests/fixtures/static/subdir/index.html new file mode 100644 index 000000000..bb84fad04 --- /dev/null +++ b/src/pyramid/tests/fixtures/static/subdir/index.html @@ -0,0 +1 @@ +subdir diff --git a/src/pyramid/tests/pkgs/__init__.py b/src/pyramid/tests/pkgs/__init__.py new file mode 100644 index 000000000..5bb534f79 --- /dev/null +++ b/src/pyramid/tests/pkgs/__init__.py @@ -0,0 +1 @@ +# package diff --git a/src/pyramid/tests/pkgs/ccbugapp/__init__.py b/src/pyramid/tests/pkgs/ccbugapp/__init__.py new file mode 100644 index 000000000..afe21d4e0 --- /dev/null +++ b/src/pyramid/tests/pkgs/ccbugapp/__init__.py @@ -0,0 +1,16 @@ +from webob import Response + +def rdf_view(request): + """ """ + return Response('rdf') + +def juri_view(request): + """ """ + return Response('juri') + +def includeme(config): + config.add_route('rdf', 'licenses/:license_code/:license_version/rdf') + config.add_route('juri', + 'licenses/:license_code/:license_version/:jurisdiction') + config.add_view(rdf_view, route_name='rdf') + config.add_view(juri_view, route_name='juri') diff --git a/src/pyramid/tests/pkgs/conflictapp/__init__.py b/src/pyramid/tests/pkgs/conflictapp/__init__.py new file mode 100644 index 000000000..38116ab2f --- /dev/null +++ b/src/pyramid/tests/pkgs/conflictapp/__init__.py @@ -0,0 +1,24 @@ +from pyramid.response import Response +from pyramid.authentication import AuthTktAuthenticationPolicy +from pyramid.authorization import ACLAuthorizationPolicy + +def aview(request): + return Response('a view') + +def routeview(request): + return Response('route view') + +def protectedview(request): + return Response('protected view') + +def includeme(config): + # purposely sorta-randomly ordered (route comes after view naming it, + # authz comes after views) + config.add_view(aview) + config.add_view(protectedview, name='protected', permission='view') + config.add_view(routeview, route_name='aroute') + config.add_route('aroute', '/route') + config.set_authentication_policy(AuthTktAuthenticationPolicy( + 'seekri1t', hashalg='sha512')) + config.set_authorization_policy(ACLAuthorizationPolicy()) + config.include('pyramid.tests.pkgs.conflictapp.included') diff --git a/src/pyramid/tests/pkgs/conflictapp/included.py b/src/pyramid/tests/pkgs/conflictapp/included.py new file mode 100644 index 000000000..0b76fb2bc --- /dev/null +++ b/src/pyramid/tests/pkgs/conflictapp/included.py @@ -0,0 +1,6 @@ +from webob import Response + +def bview(request): return Response('b view') + +def includeme(config): + config.add_view(bview) diff --git a/src/pyramid/tests/pkgs/defpermbugapp/__init__.py b/src/pyramid/tests/pkgs/defpermbugapp/__init__.py new file mode 100644 index 000000000..032e8c626 --- /dev/null +++ b/src/pyramid/tests/pkgs/defpermbugapp/__init__.py @@ -0,0 +1,26 @@ +from webob import Response +from pyramid.security import NO_PERMISSION_REQUIRED +from pyramid.view import view_config + +@view_config(name='x') +def x_view(request): # pragma: no cover + return Response('this is private!') + +@view_config(name='y', permission='private2') +def y_view(request): # pragma: no cover + return Response('this is private too!') + +@view_config(name='z', permission=NO_PERMISSION_REQUIRED) +def z_view(request): + return Response('this is public') + +def includeme(config): + from pyramid.authorization import ACLAuthorizationPolicy + from pyramid.authentication import AuthTktAuthenticationPolicy + authn_policy = AuthTktAuthenticationPolicy('seekt1t', hashalg='sha512') + authz_policy = ACLAuthorizationPolicy() + config.scan('pyramid.tests.pkgs.defpermbugapp') + config._set_authentication_policy(authn_policy) + config._set_authorization_policy(authz_policy) + config.set_default_permission('private') + diff --git a/src/pyramid/tests/pkgs/eventonly/__init__.py b/src/pyramid/tests/pkgs/eventonly/__init__.py new file mode 100644 index 000000000..7ae93ada6 --- /dev/null +++ b/src/pyramid/tests/pkgs/eventonly/__init__.py @@ -0,0 +1,64 @@ +from pyramid.view import view_config +from pyramid.events import subscriber + +class Yup(object): + def __init__(self, val, config): + self.val = val + + def text(self): + return 'path_startswith = %s' % (self.val,) + + phash = text + + def __call__(self, event): + return getattr(event.response, 'yup', False) + +class Foo(object): + def __init__(self, response): + self.response = response + +class Bar(object): + pass + +@subscriber(Foo) +def foo(event): + event.response.text += 'foo ' + +@subscriber(Foo, yup=True) +def fooyup(event): + event.response.text += 'fooyup ' + +@subscriber([Foo, Bar]) +def foobar(event): + event.response.text += 'foobar ' + +@subscriber([Foo, Bar]) +def foobar2(event, context): + event.response.text += 'foobar2 ' + +@subscriber([Foo, Bar], yup=True) +def foobaryup(event): + event.response.text += 'foobaryup ' + +@subscriber([Foo, Bar], yup=True) +def foobaryup2(event, context): + event.response.text += 'foobaryup2 ' + +@view_config(name='sendfoo') +def sendfoo(request): + response = request.response + response.yup = True + request.registry.notify(Foo(response)) + return response + +@view_config(name='sendfoobar') +def sendfoobar(request): + response = request.response + response.yup = True + request.registry.notify(Foo(response), Bar()) + return response + +def includeme(config): + config.add_subscriber_predicate('yup', Yup) + config.scan('pyramid.tests.pkgs.eventonly') + diff --git a/src/pyramid/tests/pkgs/exceptionviewapp/__init__.py b/src/pyramid/tests/pkgs/exceptionviewapp/__init__.py new file mode 100644 index 000000000..ffc1b47c6 --- /dev/null +++ b/src/pyramid/tests/pkgs/exceptionviewapp/__init__.py @@ -0,0 +1,31 @@ +from pyramid.httpexceptions import HTTPException + +def includeme(config): + config.add_route('route_raise_exception', 'route_raise_exception') + config.add_route('route_raise_httpexception', 'route_raise_httpexception') + config.add_route('route_raise_exception2', 'route_raise_exception2', + factory='.models.route_factory') + config.add_route('route_raise_exception3', 'route_raise_exception3', + factory='.models.route_factory2') + config.add_route('route_raise_exception4', 'route_raise_exception4') + config.add_view('.views.maybe') + config.add_view('.views.no', context='.models.NotAnException') + config.add_view('.views.yes', context=".models.AnException") + config.add_view('.views.raise_exception', name='raise_exception') + config.add_view('.views.raise_exception', + route_name='route_raise_exception') + config.add_view('.views.raise_exception', + route_name='route_raise_exception2') + config.add_view('.views.raise_exception', + route_name='route_raise_exception3') + config.add_view('.views.whoa', context='.models.AnException', + route_name='route_raise_exception3') + config.add_view('.views.raise_exception', + route_name='route_raise_exception4') + config.add_view('.views.whoa', context='.models.AnException', + route_name='route_raise_exception4') + config.add_view('.views.raise_httpexception', + route_name='route_raise_httpexception') + config.add_view('.views.catch_httpexception', context=HTTPException) + + diff --git a/src/pyramid/tests/pkgs/exceptionviewapp/models.py b/src/pyramid/tests/pkgs/exceptionviewapp/models.py new file mode 100644 index 000000000..fe407badc --- /dev/null +++ b/src/pyramid/tests/pkgs/exceptionviewapp/models.py @@ -0,0 +1,18 @@ + +class NotAnException(object): + pass + +class AnException(Exception): + pass + +class RouteContext(object): + pass + +class RouteContext2(object): + pass + +def route_factory(*arg): + return RouteContext() + +def route_factory2(*arg): + return RouteContext2() diff --git a/src/pyramid/tests/pkgs/exceptionviewapp/views.py b/src/pyramid/tests/pkgs/exceptionviewapp/views.py new file mode 100644 index 000000000..4953056bc --- /dev/null +++ b/src/pyramid/tests/pkgs/exceptionviewapp/views.py @@ -0,0 +1,24 @@ +from webob import Response +from .models import AnException +from pyramid.httpexceptions import HTTPBadRequest + +def no(request): + return Response('no') + +def yes(request): + return Response('yes') + +def maybe(request): + return Response('maybe') + +def whoa(request): + return Response('whoa') + +def raise_exception(request): + raise AnException() + +def raise_httpexception(request): + raise HTTPBadRequest + +def catch_httpexception(request): + return Response('caught') diff --git a/src/pyramid/tests/pkgs/fixtureapp/__init__.py b/src/pyramid/tests/pkgs/fixtureapp/__init__.py new file mode 100644 index 000000000..27063aae2 --- /dev/null +++ b/src/pyramid/tests/pkgs/fixtureapp/__init__.py @@ -0,0 +1,12 @@ +def includeme(config): + config.add_view('.views.fixture_view') + config.add_view('.views.exception_view', context=RuntimeError) + config.add_view('.views.protected_view', name='protected.html') + config.add_view('.views.erroneous_view', name='error.html') + config.add_view('.views.fixture_view', name='dummyskin.html', + request_type='.views.IDummy') + from .models import fixture, IFixture + config.registry.registerUtility(fixture, IFixture) + config.add_view('.views.fixture_view', name='another.html') + + diff --git a/src/pyramid/tests/pkgs/fixtureapp/models.py b/src/pyramid/tests/pkgs/fixtureapp/models.py new file mode 100644 index 000000000..d80d14bb3 --- /dev/null +++ b/src/pyramid/tests/pkgs/fixtureapp/models.py @@ -0,0 +1,8 @@ +from zope.interface import Interface + +class IFixture(Interface): + pass + +def fixture(): + """ """ + diff --git a/src/pyramid/tests/pkgs/fixtureapp/subpackage/__init__.py b/src/pyramid/tests/pkgs/fixtureapp/subpackage/__init__.py new file mode 100644 index 000000000..d3173e636 --- /dev/null +++ b/src/pyramid/tests/pkgs/fixtureapp/subpackage/__init__.py @@ -0,0 +1 @@ +#package diff --git a/src/pyramid/tests/pkgs/fixtureapp/views.py b/src/pyramid/tests/pkgs/fixtureapp/views.py new file mode 100644 index 000000000..cbfc5a574 --- /dev/null +++ b/src/pyramid/tests/pkgs/fixtureapp/views.py @@ -0,0 +1,22 @@ +from zope.interface import Interface +from webob import Response +from pyramid.httpexceptions import HTTPForbidden + +def fixture_view(context, request): + """ """ + return Response('fixture') + +def erroneous_view(context, request): + """ """ + raise RuntimeError() + +def exception_view(context, request): + """ """ + return Response('supressed') + +def protected_view(context, request): + """ """ + raise HTTPForbidden() + +class IDummy(Interface): + pass diff --git a/src/pyramid/tests/pkgs/forbiddenapp/__init__.py b/src/pyramid/tests/pkgs/forbiddenapp/__init__.py new file mode 100644 index 000000000..c378126fc --- /dev/null +++ b/src/pyramid/tests/pkgs/forbiddenapp/__init__.py @@ -0,0 +1,24 @@ +from webob import Response +from pyramid.httpexceptions import HTTPForbidden +from pyramid.compat import bytes_ + +def x_view(request): # pragma: no cover + return Response('this is private!') + +def forbidden_view(context, request): + msg = context.message + result = context.result + message = msg + '\n' + str(result) + resp = HTTPForbidden() + resp.body = bytes_(message) + return resp + +def includeme(config): + from pyramid.authentication import AuthTktAuthenticationPolicy + from pyramid.authorization import ACLAuthorizationPolicy + authn_policy = AuthTktAuthenticationPolicy('seekr1t', hashalg='sha512') + authz_policy = ACLAuthorizationPolicy() + config._set_authentication_policy(authn_policy) + config._set_authorization_policy(authz_policy) + config.add_view(x_view, name='x', permission='private') + config.add_view(forbidden_view, context=HTTPForbidden) diff --git a/src/pyramid/tests/pkgs/forbiddenview/__init__.py b/src/pyramid/tests/pkgs/forbiddenview/__init__.py new file mode 100644 index 000000000..45fb8380b --- /dev/null +++ b/src/pyramid/tests/pkgs/forbiddenview/__init__.py @@ -0,0 +1,31 @@ +from pyramid.view import forbidden_view_config, view_config +from pyramid.response import Response +from pyramid.authentication import AuthTktAuthenticationPolicy +from pyramid.authorization import ACLAuthorizationPolicy + +@forbidden_view_config(route_name='foo') +def foo_forbidden(request): # pragma: no cover + return Response('foo_forbidden') + +@forbidden_view_config() +def forbidden(request): + return Response('generic_forbidden') + +@view_config(route_name='foo') +def foo(request): # pragma: no cover + return Response('OK foo') + +@view_config(route_name='bar') +def bar(request): # pragma: no cover + return Response('OK bar') + +def includeme(config): + authn_policy = AuthTktAuthenticationPolicy('seekri1', hashalg='sha512') + authz_policy = ACLAuthorizationPolicy() + config.set_authentication_policy(authn_policy) + config.set_authorization_policy(authz_policy) + config.set_default_permission('a') + config.add_route('foo', '/foo') + config.add_route('bar', '/bar') + config.scan('pyramid.tests.pkgs.forbiddenview') + diff --git a/src/pyramid/tests/pkgs/hybridapp/__init__.py b/src/pyramid/tests/pkgs/hybridapp/__init__.py new file mode 100644 index 000000000..1cc2dde83 --- /dev/null +++ b/src/pyramid/tests/pkgs/hybridapp/__init__.py @@ -0,0 +1,39 @@ +def includeme(config): + # + config.add_route('route', 'abc') + config.add_view('.views.route_view', route_name='route') + # + config.add_view('.views.global_view', + context='pyramid.traversal.DefaultRootFactory') + config.add_view('.views.global2_view', + context='pyramid.traversal.DefaultRootFactory', + name='global2') + config.add_route('route2', 'def') + # + config.add_view('.views.route2_view', route_name='route2') + + # + config.add_route('route3', 'ghi', use_global_views=True) + # + config.add_route('route4', 'jkl') + # + config.add_route('route5', 'mno/*traverse') + # + config.add_route('route6', 'pqr/*traverse', use_global_views=True) + config.add_route('route7', 'error') + config.add_view('.views.erroneous_view', route_name='route7') + config.add_route('route8', 'error2') + config.add_view('.views.erroneous_view', route_name='route8') + # + config.add_view('.views.exception_view', context=RuntimeError) + # + config.add_view('.views.exception2_view', context=RuntimeError, + route_name='route8') + config.add_route('route9', 'error_sub') + config.add_view('.views.erroneous_sub_view', route_name='route9') + # + config.add_view('.views.exception2_view', context='.views.SuperException', + route_name='route9') + # + config.add_view('.views.exception_view', context='.views.SubException') diff --git a/src/pyramid/tests/pkgs/hybridapp/views.py b/src/pyramid/tests/pkgs/hybridapp/views.py new file mode 100644 index 000000000..135ef8290 --- /dev/null +++ b/src/pyramid/tests/pkgs/hybridapp/views.py @@ -0,0 +1,39 @@ +from webob import Response + +def route_view(request): + """ """ + return Response('route') + +def global_view(request): + """ """ + return Response('global') + +def global2_view(request): + """ """ + return Response('global2') + +def route2_view(request): + """ """ + return Response('route2') + +def exception_view(request): + """ """ + return Response('supressed') + +def exception2_view(request): + """ """ + return Response('supressed2') + +def erroneous_view(request): + """ """ + raise RuntimeError() + +def erroneous_sub_view(request): + """ """ + raise SubException() + +class SuperException(Exception): + """ """ + +class SubException(SuperException): + """ """ diff --git a/src/pyramid/tests/pkgs/includeapp1/__init__.py b/src/pyramid/tests/pkgs/includeapp1/__init__.py new file mode 100644 index 000000000..eaeeb7ef6 --- /dev/null +++ b/src/pyramid/tests/pkgs/includeapp1/__init__.py @@ -0,0 +1 @@ +# include app diff --git a/src/pyramid/tests/pkgs/includeapp1/root.py b/src/pyramid/tests/pkgs/includeapp1/root.py new file mode 100644 index 000000000..f56203cfa --- /dev/null +++ b/src/pyramid/tests/pkgs/includeapp1/root.py @@ -0,0 +1,10 @@ +from pyramid.response import Response + +def aview(request): + return Response('root') + +def configure(config): + config.add_view(aview) + config.include('pyramid.tests.pkgs.includeapp1.two.configure') + config.commit() + diff --git a/src/pyramid/tests/pkgs/includeapp1/three.py b/src/pyramid/tests/pkgs/includeapp1/three.py new file mode 100644 index 000000000..e7131bcf5 --- /dev/null +++ b/src/pyramid/tests/pkgs/includeapp1/three.py @@ -0,0 +1,10 @@ +from pyramid.response import Response + +def aview(request): + return Response('three') + +def configure(config): + config.add_view(aview, name='three') + config.include('pyramid.tests.pkgs.includeapp1.two.configure') # should not cycle + config.add_view(aview) # will be overridden by root when resolved + diff --git a/src/pyramid/tests/pkgs/includeapp1/two.py b/src/pyramid/tests/pkgs/includeapp1/two.py new file mode 100644 index 000000000..99b0f883a --- /dev/null +++ b/src/pyramid/tests/pkgs/includeapp1/two.py @@ -0,0 +1,9 @@ +from pyramid.response import Response + +def aview(request): + return Response('two') + +def configure(config): + config.add_view(aview, name='two') + config.include('pyramid.tests.pkgs.includeapp1.three.configure') + config.add_view(aview) # will be overridden by root when resolved diff --git a/src/pyramid/tests/pkgs/localeapp/__init__.py b/src/pyramid/tests/pkgs/localeapp/__init__.py new file mode 100644 index 000000000..1a35cdb4a --- /dev/null +++ b/src/pyramid/tests/pkgs/localeapp/__init__.py @@ -0,0 +1 @@ +# a file diff --git a/src/pyramid/tests/pkgs/localeapp/locale/GARBAGE b/src/pyramid/tests/pkgs/localeapp/locale/GARBAGE new file mode 100644 index 000000000..032c55584 --- /dev/null +++ b/src/pyramid/tests/pkgs/localeapp/locale/GARBAGE @@ -0,0 +1 @@ +Garbage file. diff --git a/src/pyramid/tests/pkgs/localeapp/locale/be/LC_MESSAGES b/src/pyramid/tests/pkgs/localeapp/locale/be/LC_MESSAGES new file mode 100644 index 000000000..909cf6a3b --- /dev/null +++ b/src/pyramid/tests/pkgs/localeapp/locale/be/LC_MESSAGES @@ -0,0 +1 @@ +busted. diff --git a/src/pyramid/tests/pkgs/localeapp/locale/de/LC_MESSAGES/deformsite.mo b/src/pyramid/tests/pkgs/localeapp/locale/de/LC_MESSAGES/deformsite.mo new file mode 100644 index 000000000..2924a5eb5 Binary files /dev/null and b/src/pyramid/tests/pkgs/localeapp/locale/de/LC_MESSAGES/deformsite.mo differ diff --git a/src/pyramid/tests/pkgs/localeapp/locale/de/LC_MESSAGES/deformsite.po b/src/pyramid/tests/pkgs/localeapp/locale/de/LC_MESSAGES/deformsite.po new file mode 100644 index 000000000..17f87bc19 --- /dev/null +++ b/src/pyramid/tests/pkgs/localeapp/locale/de/LC_MESSAGES/deformsite.po @@ -0,0 +1,31 @@ +# German translations for deformsite. +# Copyright (C) 2010 ORGANIZATION +# This file is distributed under the same license as the deformsite project. +# FIRST AUTHOR , 2010. +# +msgid "" +msgstr "" +"Project-Id-Version: deformsite 0.0\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2010-04-22 14:17+0400\n" +"PO-Revision-Date: 2010-04-22 14:17-0400\n" +"Last-Translator: FULL NAME \n" +"Language-Team: de \n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 0.9.5\n" + +#: deformsite/__init__.py:458 +msgid "Approve" +msgstr "Genehmigen" + +#: deformsite/__init__.py:459 +msgid "Show approval" +msgstr "Zeigen Genehmigung" + +#: deformsite/__init__.py:466 +msgid "Submit" +msgstr "Beugen" + diff --git a/src/pyramid/tests/pkgs/localeapp/locale/de_DE/LC_MESSAGES/deformsite.mo b/src/pyramid/tests/pkgs/localeapp/locale/de_DE/LC_MESSAGES/deformsite.mo new file mode 100644 index 000000000..e3b2b0881 Binary files /dev/null and b/src/pyramid/tests/pkgs/localeapp/locale/de_DE/LC_MESSAGES/deformsite.mo differ diff --git a/src/pyramid/tests/pkgs/localeapp/locale/de_DE/LC_MESSAGES/deformsite.po b/src/pyramid/tests/pkgs/localeapp/locale/de_DE/LC_MESSAGES/deformsite.po new file mode 100644 index 000000000..be055bed9 --- /dev/null +++ b/src/pyramid/tests/pkgs/localeapp/locale/de_DE/LC_MESSAGES/deformsite.po @@ -0,0 +1,26 @@ +# German translations for deformsite. +# Copyright (C) 2010 ORGANIZATION +# This file is distributed under the same license as the deformsite project. +# FIRST AUTHOR , 2010. +# +msgid "" +msgstr "" +"Project-Id-Version: deformsite 0.0\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2010-04-22 14:17+0400\n" +"PO-Revision-Date: 2010-04-22 14:17-0400\n" +"Last-Translator: FULL NAME \n" +"Language-Team: de \n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 0.9.5\n" + +#: deformsite/__init__.py:459 +msgid "Show approval" +msgstr "Zeigen Genehmigung" + +#: deformsite/__init__.py:466 +msgid "Submit" +msgstr "different" diff --git a/src/pyramid/tests/pkgs/localeapp/locale/en/LC_MESSAGES/deformsite.mo b/src/pyramid/tests/pkgs/localeapp/locale/en/LC_MESSAGES/deformsite.mo new file mode 100644 index 000000000..2924a5eb5 Binary files /dev/null and b/src/pyramid/tests/pkgs/localeapp/locale/en/LC_MESSAGES/deformsite.mo differ diff --git a/src/pyramid/tests/pkgs/localeapp/locale/en/LC_MESSAGES/deformsite.po b/src/pyramid/tests/pkgs/localeapp/locale/en/LC_MESSAGES/deformsite.po new file mode 100644 index 000000000..17f87bc19 --- /dev/null +++ b/src/pyramid/tests/pkgs/localeapp/locale/en/LC_MESSAGES/deformsite.po @@ -0,0 +1,31 @@ +# German translations for deformsite. +# Copyright (C) 2010 ORGANIZATION +# This file is distributed under the same license as the deformsite project. +# FIRST AUTHOR , 2010. +# +msgid "" +msgstr "" +"Project-Id-Version: deformsite 0.0\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2010-04-22 14:17+0400\n" +"PO-Revision-Date: 2010-04-22 14:17-0400\n" +"Last-Translator: FULL NAME \n" +"Language-Team: de \n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 0.9.5\n" + +#: deformsite/__init__.py:458 +msgid "Approve" +msgstr "Genehmigen" + +#: deformsite/__init__.py:459 +msgid "Show approval" +msgstr "Zeigen Genehmigung" + +#: deformsite/__init__.py:466 +msgid "Submit" +msgstr "Beugen" + diff --git a/src/pyramid/tests/pkgs/localeapp/locale2/GARBAGE b/src/pyramid/tests/pkgs/localeapp/locale2/GARBAGE new file mode 100644 index 000000000..032c55584 --- /dev/null +++ b/src/pyramid/tests/pkgs/localeapp/locale2/GARBAGE @@ -0,0 +1 @@ +Garbage file. diff --git a/src/pyramid/tests/pkgs/localeapp/locale2/be/LC_MESSAGES b/src/pyramid/tests/pkgs/localeapp/locale2/be/LC_MESSAGES new file mode 100644 index 000000000..909cf6a3b --- /dev/null +++ b/src/pyramid/tests/pkgs/localeapp/locale2/be/LC_MESSAGES @@ -0,0 +1 @@ +busted. diff --git a/src/pyramid/tests/pkgs/localeapp/locale2/de/LC_MESSAGES/deformsite.mo b/src/pyramid/tests/pkgs/localeapp/locale2/de/LC_MESSAGES/deformsite.mo new file mode 100644 index 000000000..2924a5eb5 Binary files /dev/null and b/src/pyramid/tests/pkgs/localeapp/locale2/de/LC_MESSAGES/deformsite.mo differ diff --git a/src/pyramid/tests/pkgs/localeapp/locale2/de/LC_MESSAGES/deformsite.po b/src/pyramid/tests/pkgs/localeapp/locale2/de/LC_MESSAGES/deformsite.po new file mode 100644 index 000000000..17f87bc19 --- /dev/null +++ b/src/pyramid/tests/pkgs/localeapp/locale2/de/LC_MESSAGES/deformsite.po @@ -0,0 +1,31 @@ +# German translations for deformsite. +# Copyright (C) 2010 ORGANIZATION +# This file is distributed under the same license as the deformsite project. +# FIRST AUTHOR , 2010. +# +msgid "" +msgstr "" +"Project-Id-Version: deformsite 0.0\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2010-04-22 14:17+0400\n" +"PO-Revision-Date: 2010-04-22 14:17-0400\n" +"Last-Translator: FULL NAME \n" +"Language-Team: de \n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 0.9.5\n" + +#: deformsite/__init__.py:458 +msgid "Approve" +msgstr "Genehmigen" + +#: deformsite/__init__.py:459 +msgid "Show approval" +msgstr "Zeigen Genehmigung" + +#: deformsite/__init__.py:466 +msgid "Submit" +msgstr "Beugen" + diff --git a/src/pyramid/tests/pkgs/localeapp/locale2/en/LC_MESSAGES/deformsite.mo b/src/pyramid/tests/pkgs/localeapp/locale2/en/LC_MESSAGES/deformsite.mo new file mode 100644 index 000000000..2924a5eb5 Binary files /dev/null and b/src/pyramid/tests/pkgs/localeapp/locale2/en/LC_MESSAGES/deformsite.mo differ diff --git a/src/pyramid/tests/pkgs/localeapp/locale2/en/LC_MESSAGES/deformsite.po b/src/pyramid/tests/pkgs/localeapp/locale2/en/LC_MESSAGES/deformsite.po new file mode 100644 index 000000000..17f87bc19 --- /dev/null +++ b/src/pyramid/tests/pkgs/localeapp/locale2/en/LC_MESSAGES/deformsite.po @@ -0,0 +1,31 @@ +# German translations for deformsite. +# Copyright (C) 2010 ORGANIZATION +# This file is distributed under the same license as the deformsite project. +# FIRST AUTHOR , 2010. +# +msgid "" +msgstr "" +"Project-Id-Version: deformsite 0.0\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2010-04-22 14:17+0400\n" +"PO-Revision-Date: 2010-04-22 14:17-0400\n" +"Last-Translator: FULL NAME \n" +"Language-Team: de \n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 0.9.5\n" + +#: deformsite/__init__.py:458 +msgid "Approve" +msgstr "Genehmigen" + +#: deformsite/__init__.py:459 +msgid "Show approval" +msgstr "Zeigen Genehmigung" + +#: deformsite/__init__.py:466 +msgid "Submit" +msgstr "Beugen" + diff --git a/src/pyramid/tests/pkgs/localeapp/locale3/GARBAGE b/src/pyramid/tests/pkgs/localeapp/locale3/GARBAGE new file mode 100644 index 000000000..032c55584 --- /dev/null +++ b/src/pyramid/tests/pkgs/localeapp/locale3/GARBAGE @@ -0,0 +1 @@ +Garbage file. diff --git a/src/pyramid/tests/pkgs/localeapp/locale3/be/LC_MESSAGES b/src/pyramid/tests/pkgs/localeapp/locale3/be/LC_MESSAGES new file mode 100644 index 000000000..909cf6a3b --- /dev/null +++ b/src/pyramid/tests/pkgs/localeapp/locale3/be/LC_MESSAGES @@ -0,0 +1 @@ +busted. diff --git a/src/pyramid/tests/pkgs/localeapp/locale3/de/LC_MESSAGES/deformsite.mo b/src/pyramid/tests/pkgs/localeapp/locale3/de/LC_MESSAGES/deformsite.mo new file mode 100644 index 000000000..2924a5eb5 Binary files /dev/null and b/src/pyramid/tests/pkgs/localeapp/locale3/de/LC_MESSAGES/deformsite.mo differ diff --git a/src/pyramid/tests/pkgs/localeapp/locale3/de/LC_MESSAGES/deformsite.po b/src/pyramid/tests/pkgs/localeapp/locale3/de/LC_MESSAGES/deformsite.po new file mode 100644 index 000000000..17f87bc19 --- /dev/null +++ b/src/pyramid/tests/pkgs/localeapp/locale3/de/LC_MESSAGES/deformsite.po @@ -0,0 +1,31 @@ +# German translations for deformsite. +# Copyright (C) 2010 ORGANIZATION +# This file is distributed under the same license as the deformsite project. +# FIRST AUTHOR , 2010. +# +msgid "" +msgstr "" +"Project-Id-Version: deformsite 0.0\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2010-04-22 14:17+0400\n" +"PO-Revision-Date: 2010-04-22 14:17-0400\n" +"Last-Translator: FULL NAME \n" +"Language-Team: de \n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 0.9.5\n" + +#: deformsite/__init__.py:458 +msgid "Approve" +msgstr "Genehmigen" + +#: deformsite/__init__.py:459 +msgid "Show approval" +msgstr "Zeigen Genehmigung" + +#: deformsite/__init__.py:466 +msgid "Submit" +msgstr "Beugen" + diff --git a/src/pyramid/tests/pkgs/localeapp/locale3/en/LC_MESSAGES/deformsite.mo b/src/pyramid/tests/pkgs/localeapp/locale3/en/LC_MESSAGES/deformsite.mo new file mode 100644 index 000000000..2924a5eb5 Binary files /dev/null and b/src/pyramid/tests/pkgs/localeapp/locale3/en/LC_MESSAGES/deformsite.mo differ diff --git a/src/pyramid/tests/pkgs/localeapp/locale3/en/LC_MESSAGES/deformsite.po b/src/pyramid/tests/pkgs/localeapp/locale3/en/LC_MESSAGES/deformsite.po new file mode 100644 index 000000000..17f87bc19 --- /dev/null +++ b/src/pyramid/tests/pkgs/localeapp/locale3/en/LC_MESSAGES/deformsite.po @@ -0,0 +1,31 @@ +# German translations for deformsite. +# Copyright (C) 2010 ORGANIZATION +# This file is distributed under the same license as the deformsite project. +# FIRST AUTHOR , 2010. +# +msgid "" +msgstr "" +"Project-Id-Version: deformsite 0.0\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2010-04-22 14:17+0400\n" +"PO-Revision-Date: 2010-04-22 14:17-0400\n" +"Last-Translator: FULL NAME \n" +"Language-Team: de \n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 0.9.5\n" + +#: deformsite/__init__.py:458 +msgid "Approve" +msgstr "Genehmigen" + +#: deformsite/__init__.py:459 +msgid "Show approval" +msgstr "Zeigen Genehmigung" + +#: deformsite/__init__.py:466 +msgid "Submit" +msgstr "Beugen" + diff --git a/src/pyramid/tests/pkgs/notfoundview/__init__.py b/src/pyramid/tests/pkgs/notfoundview/__init__.py new file mode 100644 index 000000000..ae148ea8c --- /dev/null +++ b/src/pyramid/tests/pkgs/notfoundview/__init__.py @@ -0,0 +1,30 @@ +from pyramid.view import notfound_view_config, view_config +from pyramid.response import Response + +@notfound_view_config(route_name='foo', append_slash=True) +def foo_notfound(request): # pragma: no cover + return Response('foo_notfound') + +@notfound_view_config(route_name='baz') +def baz_notfound(request): + return Response('baz_notfound') + +@notfound_view_config(append_slash=True) +def notfound(request): + return Response('generic_notfound') + +@view_config(route_name='bar') +def bar(request): + return Response('OK bar') + +@view_config(route_name='foo2') +def foo2(request): + return Response('OK foo2') + +def includeme(config): + config.add_route('foo', '/foo') + config.add_route('foo2', '/foo/') + config.add_route('bar', '/bar/') + config.add_route('baz', '/baz') + config.scan('pyramid.tests.pkgs.notfoundview') + diff --git a/src/pyramid/tests/pkgs/permbugapp/__init__.py b/src/pyramid/tests/pkgs/permbugapp/__init__.py new file mode 100644 index 000000000..4868427a5 --- /dev/null +++ b/src/pyramid/tests/pkgs/permbugapp/__init__.py @@ -0,0 +1,22 @@ +from pyramid.compat import escape +from pyramid.security import view_execution_permitted +from pyramid.response import Response + +def x_view(request): # pragma: no cover + return Response('this is private!') + +def test(context, request): + # should return false + msg = 'Allow ./x? %s' % repr(view_execution_permitted( + context, request, 'x')) + return Response(escape(msg)) + +def includeme(config): + from pyramid.authentication import AuthTktAuthenticationPolicy + from pyramid.authorization import ACLAuthorizationPolicy + authn_policy = AuthTktAuthenticationPolicy('seekt1t', hashalg='sha512') + authz_policy = ACLAuthorizationPolicy() + config.set_authentication_policy(authn_policy) + config.set_authorization_policy(authz_policy) + config.add_view(test, name='test') + config.add_view(x_view, name='x', permission='private') diff --git a/src/pyramid/tests/pkgs/rendererscanapp/__init__.py b/src/pyramid/tests/pkgs/rendererscanapp/__init__.py new file mode 100644 index 000000000..f3276a063 --- /dev/null +++ b/src/pyramid/tests/pkgs/rendererscanapp/__init__.py @@ -0,0 +1,9 @@ +from pyramid.view import view_config + +@view_config(name='one', renderer='json') +def one(request): + return {'name':'One!'} + +def includeme(config): + config.scan() + diff --git a/src/pyramid/tests/pkgs/rendererscanapp/two/__init__.py b/src/pyramid/tests/pkgs/rendererscanapp/two/__init__.py new file mode 100644 index 000000000..6f575dd83 --- /dev/null +++ b/src/pyramid/tests/pkgs/rendererscanapp/two/__init__.py @@ -0,0 +1,6 @@ +from pyramid.view import view_config + +@view_config(name='two', renderer='json') +def two(request): + return {'nameagain':'Two!'} + diff --git a/src/pyramid/tests/pkgs/restbugapp/__init__.py b/src/pyramid/tests/pkgs/restbugapp/__init__.py new file mode 100644 index 000000000..9ad79e32e --- /dev/null +++ b/src/pyramid/tests/pkgs/restbugapp/__init__.py @@ -0,0 +1,15 @@ +def includeme(config): + config.add_route('gameactions_pet_get_pets', '/pet', + request_method='GET') + config.add_route('gameactions_pet_care_for_pet', '/pet', + request_method='POST') + config.add_view('.views.PetRESTView', + route_name='gameactions_pet_get_pets', + attr='GET', + permission='view', + renderer='json') + config.add_view('.views.PetRESTView', + route_name='gameactions_pet_care_for_pet', + attr='POST', + permission='view', + renderer='json') diff --git a/src/pyramid/tests/pkgs/restbugapp/views.py b/src/pyramid/tests/pkgs/restbugapp/views.py new file mode 100644 index 000000000..2ace59fa9 --- /dev/null +++ b/src/pyramid/tests/pkgs/restbugapp/views.py @@ -0,0 +1,15 @@ +from pyramid.response import Response + +class BaseRESTView(object): + def __init__(self, context, request): + self.context = context + self.request = request + +class PetRESTView(BaseRESTView): + """ REST Controller to control action of an avatar """ + def __init__(self, context, request): + super(PetRESTView, self).__init__(context, request) + + def GET(self): + return Response('gotten') + diff --git a/src/pyramid/tests/pkgs/static_abspath/__init__.py b/src/pyramid/tests/pkgs/static_abspath/__init__.py new file mode 100644 index 000000000..812cca467 --- /dev/null +++ b/src/pyramid/tests/pkgs/static_abspath/__init__.py @@ -0,0 +1,7 @@ +import os + +def includeme(config): + here = here = os.path.dirname(__file__) + fixtures = os.path.normpath(os.path.join(here, '..', '..', 'fixtures')) + config.add_static_view('/', fixtures) + diff --git a/src/pyramid/tests/pkgs/static_assetspec/__init__.py b/src/pyramid/tests/pkgs/static_assetspec/__init__.py new file mode 100644 index 000000000..cd6195397 --- /dev/null +++ b/src/pyramid/tests/pkgs/static_assetspec/__init__.py @@ -0,0 +1,3 @@ +def includeme(config): + config.add_static_view('/', 'pyramid.tests:fixtures') + diff --git a/src/pyramid/tests/pkgs/static_routeprefix/__init__.py b/src/pyramid/tests/pkgs/static_routeprefix/__init__.py new file mode 100644 index 000000000..9b539380a --- /dev/null +++ b/src/pyramid/tests/pkgs/static_routeprefix/__init__.py @@ -0,0 +1,7 @@ +def includeme(config): + config.add_static_view('/static', 'pyramid.tests:fixtures') + config.include(includeme2, route_prefix='/prefix') + +def includeme2(config): + config.add_static_view('/static', 'pyramid.tests:fixtures/static') + diff --git a/src/pyramid/tests/pkgs/staticpermapp/__init__.py b/src/pyramid/tests/pkgs/staticpermapp/__init__.py new file mode 100644 index 000000000..cc690d937 --- /dev/null +++ b/src/pyramid/tests/pkgs/staticpermapp/__init__.py @@ -0,0 +1,25 @@ +class RootFactory(object): + __acl__ = [('Allow', 'fred', 'view')] + def __init__(self, request): + pass + +class LocalRootFactory(object): + __acl__ = [('Allow', 'bob', 'view')] + def __init__(self, request): + pass + + +def includeme(config): + from pyramid.authentication import RemoteUserAuthenticationPolicy + from pyramid.authorization import ACLAuthorizationPolicy + authn_policy = RemoteUserAuthenticationPolicy() + authz_policy = ACLAuthorizationPolicy() + config._set_authentication_policy(authn_policy) + config._set_authorization_policy(authz_policy) + config.add_static_view('allowed', 'pyramid.tests:fixtures/static/') + config.add_static_view('protected', 'pyramid.tests:fixtures/static/', + permission='view') + config.add_static_view('factory_protected', + 'pyramid.tests:fixtures/static/', + permission='view', + factory=LocalRootFactory) diff --git a/src/pyramid/tests/pkgs/subrequestapp/__init__.py b/src/pyramid/tests/pkgs/subrequestapp/__init__.py new file mode 100644 index 000000000..e4b1d386a --- /dev/null +++ b/src/pyramid/tests/pkgs/subrequestapp/__init__.py @@ -0,0 +1,52 @@ +from pyramid.config import Configurator +from pyramid.request import Request + +def view_one(request): + subreq = Request.blank('/view_two') + response = request.invoke_subrequest(subreq, use_tweens=False) + return response + +def view_two(request): + # check that request.foo is valid for a subrequest + return 'This came from view_two, foo=%s' % (request.foo,) + +def view_three(request): + subreq = Request.blank('/view_four') + try: + return request.invoke_subrequest(subreq, use_tweens=True) + except: # pragma: no cover + request.response.body = b'Value error raised' + return request.response + +def view_four(request): + raise ValueError('foo') + +def view_five(request): + subreq = Request.blank('/view_four') + try: + return request.invoke_subrequest(subreq, use_tweens=False) + except ValueError: + request.response.body = b'Value error raised' + return request.response + +def excview(request): + request.response.status_int = 500 + request.response.body = b'Bad stuff happened' + return request.response + +def main(): + config = Configurator() + config.add_route('one', '/view_one') + config.add_route('two', '/view_two') + config.add_route('three', '/view_three') + config.add_route('four', '/view_four') + config.add_route('five', '/view_five') + config.add_view(excview, context=Exception) + config.add_view(view_one, route_name='one') + config.add_view(view_two, route_name='two', renderer='string') + config.add_view(view_three, route_name='three') + config.add_view(view_four, route_name='four') + config.add_view(view_five, route_name='five') + config.add_request_method(lambda r: 'bar', 'foo', property=True) + return config + diff --git a/src/pyramid/tests/pkgs/viewdecoratorapp/__init__.py b/src/pyramid/tests/pkgs/viewdecoratorapp/__init__.py new file mode 100644 index 000000000..5fa98062a --- /dev/null +++ b/src/pyramid/tests/pkgs/viewdecoratorapp/__init__.py @@ -0,0 +1,3 @@ +def includeme(config): + config.scan('pyramid.tests.pkgs.viewdecoratorapp') + diff --git a/src/pyramid/tests/pkgs/viewdecoratorapp/views/__init__.py b/src/pyramid/tests/pkgs/viewdecoratorapp/views/__init__.py new file mode 100644 index 000000000..5bb534f79 --- /dev/null +++ b/src/pyramid/tests/pkgs/viewdecoratorapp/views/__init__.py @@ -0,0 +1 @@ +# package diff --git a/src/pyramid/tests/pkgs/viewdecoratorapp/views/views.py b/src/pyramid/tests/pkgs/viewdecoratorapp/views/views.py new file mode 100644 index 000000000..18ec78847 --- /dev/null +++ b/src/pyramid/tests/pkgs/viewdecoratorapp/views/views.py @@ -0,0 +1,12 @@ +from pyramid.view import view_config + +@view_config(renderer='json', name='first') +def first(request): + return {'result':'OK1'} + +@view_config( + renderer='json', + name='second') +def second(request): + return {'result':'OK2'} + diff --git a/src/pyramid/tests/pkgs/wsgiapp2app/__init__.py b/src/pyramid/tests/pkgs/wsgiapp2app/__init__.py new file mode 100644 index 000000000..e2024198e --- /dev/null +++ b/src/pyramid/tests/pkgs/wsgiapp2app/__init__.py @@ -0,0 +1,17 @@ +from pyramid.view import view_config +from pyramid.wsgi import wsgiapp2 + +@view_config(name='hello', renderer='string') +@wsgiapp2 +def hello(environ, start_response): + assert environ['PATH_INFO'] == '/' + assert environ['SCRIPT_NAME'] == '/hello' + response_headers = [('Content-Type', 'text/plain')] + start_response('200 OK', response_headers) + return [b'Hello!'] + +def main(): + from pyramid.config import Configurator + c = Configurator() + c.scan() + return c diff --git a/src/pyramid/tests/test_asset.py b/src/pyramid/tests/test_asset.py new file mode 100644 index 000000000..d3ebd5f7d --- /dev/null +++ b/src/pyramid/tests/test_asset.py @@ -0,0 +1,88 @@ +import unittest +import os + +here = os.path.abspath(os.path.dirname(__file__)) + +class Test_resolve_asset_spec(unittest.TestCase): + def _callFUT(self, spec, package_name='__main__'): + from pyramid.resource import resolve_asset_spec + return resolve_asset_spec(spec, package_name) + + def test_abspath(self): + package_name, filename = self._callFUT(here, 'apackage') + self.assertEqual(filename, here) + self.assertEqual(package_name, None) + + def test_rel_spec(self): + pkg = 'pyramid.tests' + path = 'test_asset.py' + package_name, filename = self._callFUT(path, pkg) + self.assertEqual(package_name, 'pyramid.tests') + self.assertEqual(filename, 'test_asset.py') + + def test_abs_spec(self): + pkg = 'pyramid.tests' + path = 'pyramid.nottests:test_asset.py' + package_name, filename = self._callFUT(path, pkg) + self.assertEqual(package_name, 'pyramid.nottests') + self.assertEqual(filename, 'test_asset.py') + + def test_package_name_is_None(self): + pkg = None + path = 'test_asset.py' + package_name, filename = self._callFUT(path, pkg) + self.assertEqual(package_name, None) + self.assertEqual(filename, 'test_asset.py') + + def test_package_name_is_package_object(self): + import pyramid.tests + pkg = pyramid.tests + path = 'test_asset.py' + package_name, filename = self._callFUT(path, pkg) + self.assertEqual(package_name, 'pyramid.tests') + self.assertEqual(filename, 'test_asset.py') + + +class Test_abspath_from_asset_spec(unittest.TestCase): + def _callFUT(self, spec, pname='__main__'): + from pyramid.resource import abspath_from_asset_spec + return abspath_from_asset_spec(spec, pname) + + def test_pname_is_None_before_resolve_asset_spec(self): + result = self._callFUT('abc', None) + self.assertEqual(result, 'abc') + + def test_pname_is_None_after_resolve_asset_spec(self): + result = self._callFUT('/abc', '__main__') + self.assertEqual(result, '/abc') + + def test_pkgrelative(self): + result = self._callFUT('abc', 'pyramid.tests') + self.assertEqual(result, os.path.join(here, 'abc')) + +class Test_asset_spec_from_abspath(unittest.TestCase): + def _callFUT(self, abspath, package): + from pyramid.asset import asset_spec_from_abspath + return asset_spec_from_abspath(abspath, package) + + def test_package_name_is_main(self): + pkg = DummyPackage('__main__') + result = self._callFUT('abspath', pkg) + self.assertEqual(result, 'abspath') + + def test_abspath_startswith_package_path(self): + abspath = os.path.join(here, 'fixtureapp') + pkg = DummyPackage('pyramid.tests') + pkg.__file__ = 'file' + result = self._callFUT(abspath, pkg) + self.assertEqual(result, 'pyramid:fixtureapp') + + def test_abspath_doesnt_startwith_package_path(self): + pkg = DummyPackage('pyramid.tests') + result = self._callFUT(here, pkg) + self.assertEqual(result, here) + +class DummyPackage: + def __init__(self, name): + self.__name__ = name + diff --git a/src/pyramid/tests/test_authentication.py b/src/pyramid/tests/test_authentication.py new file mode 100644 index 000000000..4efd76f2b --- /dev/null +++ b/src/pyramid/tests/test_authentication.py @@ -0,0 +1,1738 @@ +import unittest +import warnings +from pyramid import testing +from pyramid.compat import ( + text_, + bytes_, + ) + +class TestCallbackAuthenticationPolicyDebugging(unittest.TestCase): + def setUp(self): + from pyramid.interfaces import IDebugLogger + self.config = testing.setUp() + self.config.registry.registerUtility(self, IDebugLogger) + self.messages = [] + + def tearDown(self): + del self.config + + def debug(self, msg): + self.messages.append(msg) + + def _makeOne(self, userid=None, callback=None): + from pyramid.authentication import CallbackAuthenticationPolicy + class MyAuthenticationPolicy(CallbackAuthenticationPolicy): + def unauthenticated_userid(self, request): + return userid + policy = MyAuthenticationPolicy() + policy.debug = True + policy.callback = callback + return policy + + def test_authenticated_userid_no_unauthenticated_userid(self): + request = DummyRequest(registry=self.config.registry) + policy = self._makeOne() + self.assertEqual(policy.authenticated_userid(request), None) + self.assertEqual(len(self.messages), 1) + self.assertEqual( + self.messages[0], + 'pyramid.tests.test_authentication.MyAuthenticationPolicy.' + 'authenticated_userid: call to unauthenticated_userid returned ' + 'None; returning None') + + def test_authenticated_userid_no_callback(self): + request = DummyRequest(registry=self.config.registry) + policy = self._makeOne(userid='fred') + self.assertEqual(policy.authenticated_userid(request), 'fred') + self.assertEqual(len(self.messages), 1) + self.assertEqual( + self.messages[0], + "pyramid.tests.test_authentication.MyAuthenticationPolicy." + "authenticated_userid: there was no groupfinder callback; " + "returning 'fred'") + + def test_authenticated_userid_with_callback_fail(self): + request = DummyRequest(registry=self.config.registry) + def callback(userid, request): + return None + policy = self._makeOne(userid='fred', callback=callback) + self.assertEqual(policy.authenticated_userid(request), None) + self.assertEqual(len(self.messages), 1) + self.assertEqual( + self.messages[0], + 'pyramid.tests.test_authentication.MyAuthenticationPolicy.' + 'authenticated_userid: groupfinder callback returned None; ' + 'returning None') + + def test_authenticated_userid_with_callback_success(self): + request = DummyRequest(registry=self.config.registry) + def callback(userid, request): + return [] + policy = self._makeOne(userid='fred', callback=callback) + self.assertEqual(policy.authenticated_userid(request), 'fred') + self.assertEqual(len(self.messages), 1) + self.assertEqual( + self.messages[0], + "pyramid.tests.test_authentication.MyAuthenticationPolicy." + "authenticated_userid: groupfinder callback returned []; " + "returning 'fred'") + + def test_authenticated_userid_fails_cleaning_as_Authenticated(self): + request = DummyRequest(registry=self.config.registry) + policy = self._makeOne(userid='system.Authenticated') + self.assertEqual(policy.authenticated_userid(request), None) + self.assertEqual(len(self.messages), 1) + self.assertEqual( + self.messages[0], + "pyramid.tests.test_authentication.MyAuthenticationPolicy." + "authenticated_userid: use of userid 'system.Authenticated' is " + "disallowed by any built-in Pyramid security policy, returning " + "None") + + def test_authenticated_userid_fails_cleaning_as_Everyone(self): + request = DummyRequest(registry=self.config.registry) + policy = self._makeOne(userid='system.Everyone') + self.assertEqual(policy.authenticated_userid(request), None) + self.assertEqual(len(self.messages), 1) + self.assertEqual( + self.messages[0], + "pyramid.tests.test_authentication.MyAuthenticationPolicy." + "authenticated_userid: use of userid 'system.Everyone' is " + "disallowed by any built-in Pyramid security policy, returning " + "None") + + def test_effective_principals_no_unauthenticated_userid(self): + request = DummyRequest(registry=self.config.registry) + policy = self._makeOne() + self.assertEqual(policy.effective_principals(request), + ['system.Everyone']) + self.assertEqual(len(self.messages), 1) + self.assertEqual( + self.messages[0], + "pyramid.tests.test_authentication.MyAuthenticationPolicy." + "effective_principals: unauthenticated_userid returned None; " + "returning ['system.Everyone']") + + def test_effective_principals_no_callback(self): + request = DummyRequest(registry=self.config.registry) + policy = self._makeOne(userid='fred') + self.assertEqual( + policy.effective_principals(request), + ['system.Everyone', 'system.Authenticated', 'fred']) + self.assertEqual(len(self.messages), 2) + self.assertEqual( + self.messages[0], + 'pyramid.tests.test_authentication.MyAuthenticationPolicy.' + 'effective_principals: groupfinder callback is None, so groups ' + 'is []') + self.assertEqual( + self.messages[1], + "pyramid.tests.test_authentication.MyAuthenticationPolicy." + "effective_principals: returning effective principals: " + "['system.Everyone', 'system.Authenticated', 'fred']") + + def test_effective_principals_with_callback_fail(self): + request = DummyRequest(registry=self.config.registry) + def callback(userid, request): + return None + policy = self._makeOne(userid='fred', callback=callback) + self.assertEqual( + policy.effective_principals(request), ['system.Everyone']) + self.assertEqual(len(self.messages), 2) + self.assertEqual( + self.messages[0], + 'pyramid.tests.test_authentication.MyAuthenticationPolicy.' + 'effective_principals: groupfinder callback returned None as ' + 'groups') + self.assertEqual( + self.messages[1], + "pyramid.tests.test_authentication.MyAuthenticationPolicy." + "effective_principals: returning effective principals: " + "['system.Everyone']") + + def test_effective_principals_with_callback_success(self): + request = DummyRequest(registry=self.config.registry) + def callback(userid, request): + return [] + policy = self._makeOne(userid='fred', callback=callback) + self.assertEqual( + policy.effective_principals(request), + ['system.Everyone', 'system.Authenticated', 'fred']) + self.assertEqual(len(self.messages), 2) + self.assertEqual( + self.messages[0], + 'pyramid.tests.test_authentication.MyAuthenticationPolicy.' + 'effective_principals: groupfinder callback returned [] as groups') + self.assertEqual( + self.messages[1], + "pyramid.tests.test_authentication.MyAuthenticationPolicy." + "effective_principals: returning effective principals: " + "['system.Everyone', 'system.Authenticated', 'fred']") + + def test_effective_principals_with_unclean_principal_Authenticated(self): + request = DummyRequest(registry=self.config.registry) + policy = self._makeOne(userid='system.Authenticated') + self.assertEqual( + policy.effective_principals(request), + ['system.Everyone']) + self.assertEqual(len(self.messages), 1) + self.assertEqual( + self.messages[0], + "pyramid.tests.test_authentication.MyAuthenticationPolicy." + "effective_principals: unauthenticated_userid returned disallowed " + "'system.Authenticated'; returning ['system.Everyone'] as if it " + "was None") + + def test_effective_principals_with_unclean_principal_Everyone(self): + request = DummyRequest(registry=self.config.registry) + policy = self._makeOne(userid='system.Everyone') + self.assertEqual( + policy.effective_principals(request), + ['system.Everyone']) + self.assertEqual(len(self.messages), 1) + self.assertEqual( + self.messages[0], + "pyramid.tests.test_authentication.MyAuthenticationPolicy." + "effective_principals: unauthenticated_userid returned disallowed " + "'system.Everyone'; returning ['system.Everyone'] as if it " + "was None") + +class TestRepozeWho1AuthenticationPolicy(unittest.TestCase): + def _getTargetClass(self): + from pyramid.authentication import RepozeWho1AuthenticationPolicy + return RepozeWho1AuthenticationPolicy + + def _makeOne(self, identifier_name='auth_tkt', callback=None): + return self._getTargetClass()(identifier_name, callback) + + def test_class_implements_IAuthenticationPolicy(self): + from zope.interface.verify import verifyClass + from pyramid.interfaces import IAuthenticationPolicy + verifyClass(IAuthenticationPolicy, self._getTargetClass()) + + def test_instance_implements_IAuthenticationPolicy(self): + from zope.interface.verify import verifyObject + from pyramid.interfaces import IAuthenticationPolicy + verifyObject(IAuthenticationPolicy, self._makeOne()) + + def test_unauthenticated_userid_returns_None(self): + request = DummyRequest({}) + policy = self._makeOne() + self.assertEqual(policy.unauthenticated_userid(request), None) + + def test_unauthenticated_userid(self): + request = DummyRequest( + {'repoze.who.identity':{'repoze.who.userid':'fred'}}) + policy = self._makeOne() + self.assertEqual(policy.unauthenticated_userid(request), 'fred') + + def test_authenticated_userid_None(self): + request = DummyRequest({}) + policy = self._makeOne() + self.assertEqual(policy.authenticated_userid(request), None) + + def test_authenticated_userid(self): + request = DummyRequest( + {'repoze.who.identity':{'repoze.who.userid':'fred'}}) + policy = self._makeOne() + self.assertEqual(policy.authenticated_userid(request), 'fred') + + def test_authenticated_userid_repoze_who_userid_is_None(self): + request = DummyRequest( + {'repoze.who.identity':{'repoze.who.userid':None}}) + policy = self._makeOne() + self.assertEqual(policy.authenticated_userid(request), None) + + def test_authenticated_userid_with_callback_returns_None(self): + request = DummyRequest( + {'repoze.who.identity':{'repoze.who.userid':'fred'}}) + def callback(identity, request): + return None + policy = self._makeOne(callback=callback) + self.assertEqual(policy.authenticated_userid(request), None) + + def test_authenticated_userid_with_callback_returns_something(self): + request = DummyRequest( + {'repoze.who.identity':{'repoze.who.userid':'fred'}}) + def callback(identity, request): + return ['agroup'] + policy = self._makeOne(callback=callback) + self.assertEqual(policy.authenticated_userid(request), 'fred') + + def test_authenticated_userid_unclean_principal_Authenticated(self): + request = DummyRequest( + {'repoze.who.identity':{'repoze.who.userid':'system.Authenticated'}} + ) + policy = self._makeOne() + self.assertEqual(policy.authenticated_userid(request), None) + + def test_authenticated_userid_unclean_principal_Everyone(self): + request = DummyRequest( + {'repoze.who.identity':{'repoze.who.userid':'system.Everyone'}} + ) + policy = self._makeOne() + self.assertEqual(policy.authenticated_userid(request), None) + + def test_effective_principals_None(self): + from pyramid.security import Everyone + request = DummyRequest({}) + policy = self._makeOne() + self.assertEqual(policy.effective_principals(request), [Everyone]) + + def test_effective_principals_userid_only(self): + from pyramid.security import Everyone + from pyramid.security import Authenticated + request = DummyRequest( + {'repoze.who.identity':{'repoze.who.userid':'fred'}}) + policy = self._makeOne() + self.assertEqual(policy.effective_principals(request), + [Everyone, Authenticated, 'fred']) + + def test_effective_principals_userid_and_groups(self): + from pyramid.security import Everyone + from pyramid.security import Authenticated + request = DummyRequest( + {'repoze.who.identity':{'repoze.who.userid':'fred', + 'groups':['quux', 'biz']}}) + def callback(identity, request): + return identity['groups'] + policy = self._makeOne(callback=callback) + self.assertEqual(policy.effective_principals(request), + [Everyone, Authenticated, 'fred', 'quux', 'biz']) + + def test_effective_principals_userid_callback_returns_None(self): + from pyramid.security import Everyone + request = DummyRequest( + {'repoze.who.identity':{'repoze.who.userid':'fred', + 'groups':['quux', 'biz']}}) + def callback(identity, request): + return None + policy = self._makeOne(callback=callback) + self.assertEqual(policy.effective_principals(request), [Everyone]) + + def test_effective_principals_repoze_who_userid_is_None(self): + from pyramid.security import Everyone + request = DummyRequest( + {'repoze.who.identity':{'repoze.who.userid':None}} + ) + policy = self._makeOne() + self.assertEqual(policy.effective_principals(request), [Everyone]) + + def test_effective_principals_repoze_who_userid_is_unclean_Everyone(self): + from pyramid.security import Everyone + request = DummyRequest( + {'repoze.who.identity':{'repoze.who.userid':'system.Everyone'}} + ) + policy = self._makeOne() + self.assertEqual(policy.effective_principals(request), [Everyone]) + + def test_effective_principals_repoze_who_userid_is_unclean_Authenticated( + self): + from pyramid.security import Everyone + request = DummyRequest( + {'repoze.who.identity':{'repoze.who.userid':'system.Authenticated'}} + ) + policy = self._makeOne() + self.assertEqual(policy.effective_principals(request), [Everyone]) + + def test_remember_no_plugins(self): + request = DummyRequest({}) + policy = self._makeOne() + result = policy.remember(request, 'fred') + self.assertEqual(result, []) + + def test_remember(self): + authtkt = DummyWhoPlugin() + request = DummyRequest( + {'repoze.who.plugins':{'auth_tkt':authtkt}}) + policy = self._makeOne() + result = policy.remember(request, 'fred') + self.assertEqual(result[0], request.environ) + self.assertEqual(result[1], {'repoze.who.userid':'fred'}) + + def test_remember_kwargs(self): + authtkt = DummyWhoPlugin() + request = DummyRequest( + {'repoze.who.plugins':{'auth_tkt':authtkt}}) + policy = self._makeOne() + result = policy.remember(request, 'fred', max_age=23) + self.assertEqual(result[1], {'repoze.who.userid':'fred', 'max_age': 23}) + + def test_forget_no_plugins(self): + request = DummyRequest({}) + policy = self._makeOne() + result = policy.forget(request) + self.assertEqual(result, []) + + def test_forget(self): + authtkt = DummyWhoPlugin() + request = DummyRequest( + {'repoze.who.plugins':{'auth_tkt':authtkt}, + 'repoze.who.identity':{'repoze.who.userid':'fred'}, + }) + policy = self._makeOne() + result = policy.forget(request) + self.assertEqual(result[0], request.environ) + self.assertEqual(result[1], request.environ['repoze.who.identity']) + +class TestRemoteUserAuthenticationPolicy(unittest.TestCase): + def _getTargetClass(self): + from pyramid.authentication import RemoteUserAuthenticationPolicy + return RemoteUserAuthenticationPolicy + + def _makeOne(self, environ_key='REMOTE_USER', callback=None): + return self._getTargetClass()(environ_key, callback) + + def test_class_implements_IAuthenticationPolicy(self): + from zope.interface.verify import verifyClass + from pyramid.interfaces import IAuthenticationPolicy + verifyClass(IAuthenticationPolicy, self._getTargetClass()) + + def test_instance_implements_IAuthenticationPolicy(self): + from zope.interface.verify import verifyObject + from pyramid.interfaces import IAuthenticationPolicy + verifyObject(IAuthenticationPolicy, self._makeOne()) + + def test_unauthenticated_userid_returns_None(self): + request = DummyRequest({}) + policy = self._makeOne() + self.assertEqual(policy.unauthenticated_userid(request), None) + + def test_unauthenticated_userid(self): + request = DummyRequest({'REMOTE_USER':'fred'}) + policy = self._makeOne() + self.assertEqual(policy.unauthenticated_userid(request), 'fred') + + def test_authenticated_userid_None(self): + request = DummyRequest({}) + policy = self._makeOne() + self.assertEqual(policy.authenticated_userid(request), None) + + def test_authenticated_userid(self): + request = DummyRequest({'REMOTE_USER':'fred'}) + policy = self._makeOne() + self.assertEqual(policy.authenticated_userid(request), 'fred') + + def test_effective_principals_None(self): + from pyramid.security import Everyone + request = DummyRequest({}) + policy = self._makeOne() + self.assertEqual(policy.effective_principals(request), [Everyone]) + + def test_effective_principals(self): + from pyramid.security import Everyone + from pyramid.security import Authenticated + request = DummyRequest({'REMOTE_USER':'fred'}) + policy = self._makeOne() + self.assertEqual(policy.effective_principals(request), + [Everyone, Authenticated, 'fred']) + + def test_remember(self): + request = DummyRequest({'REMOTE_USER':'fred'}) + policy = self._makeOne() + result = policy.remember(request, 'fred') + self.assertEqual(result, []) + + def test_forget(self): + request = DummyRequest({'REMOTE_USER':'fred'}) + policy = self._makeOne() + result = policy.forget(request) + self.assertEqual(result, []) + +class TestAuthTktAuthenticationPolicy(unittest.TestCase): + def _getTargetClass(self): + from pyramid.authentication import AuthTktAuthenticationPolicy + return AuthTktAuthenticationPolicy + + def _makeOne(self, callback, cookieidentity, **kw): + inst = self._getTargetClass()('secret', callback, **kw) + inst.cookie = DummyCookieHelper(cookieidentity) + return inst + + def setUp(self): + self.warnings = warnings.catch_warnings() + self.warnings.__enter__() + warnings.simplefilter('ignore', DeprecationWarning) + + def tearDown(self): + self.warnings.__exit__(None, None, None) + + def test_allargs(self): + # pass all known args + inst = self._getTargetClass()( + 'secret', callback=None, cookie_name=None, secure=False, + include_ip=False, timeout=None, reissue_time=None, + hashalg='sha512', samesite=None, + ) + self.assertEqual(inst.callback, None) + + def test_hashalg_override(self): + # important to ensure hashalg is passed to cookie helper + inst = self._getTargetClass()('secret', hashalg='sha512') + self.assertEqual(inst.cookie.hashalg, 'sha512') + + def test_unauthenticated_userid_returns_None(self): + request = DummyRequest({}) + policy = self._makeOne(None, None) + self.assertEqual(policy.unauthenticated_userid(request), None) + + def test_unauthenticated_userid(self): + request = DummyRequest({'REMOTE_USER':'fred'}) + policy = self._makeOne(None, {'userid':'fred'}) + self.assertEqual(policy.unauthenticated_userid(request), 'fred') + + def test_authenticated_userid_no_cookie_identity(self): + request = DummyRequest({}) + policy = self._makeOne(None, None) + self.assertEqual(policy.authenticated_userid(request), None) + + def test_authenticated_userid_callback_returns_None(self): + request = DummyRequest({}) + def callback(userid, request): + return None + policy = self._makeOne(callback, {'userid':'fred'}) + self.assertEqual(policy.authenticated_userid(request), None) + + def test_authenticated_userid(self): + request = DummyRequest({}) + def callback(userid, request): + return True + policy = self._makeOne(callback, {'userid':'fred'}) + self.assertEqual(policy.authenticated_userid(request), 'fred') + + def test_effective_principals_no_cookie_identity(self): + from pyramid.security import Everyone + request = DummyRequest({}) + policy = self._makeOne(None, None) + self.assertEqual(policy.effective_principals(request), [Everyone]) + + def test_effective_principals_callback_returns_None(self): + from pyramid.security import Everyone + request = DummyRequest({}) + def callback(userid, request): + return None + policy = self._makeOne(callback, {'userid':'fred'}) + self.assertEqual(policy.effective_principals(request), [Everyone]) + + def test_effective_principals(self): + from pyramid.security import Everyone + from pyramid.security import Authenticated + request = DummyRequest({}) + def callback(userid, request): + return ['group.foo'] + policy = self._makeOne(callback, {'userid':'fred'}) + self.assertEqual(policy.effective_principals(request), + [Everyone, Authenticated, 'fred', 'group.foo']) + + def test_remember(self): + request = DummyRequest({}) + policy = self._makeOne(None, None) + result = policy.remember(request, 'fred') + self.assertEqual(result, []) + + def test_remember_with_extra_kargs(self): + request = DummyRequest({}) + policy = self._makeOne(None, None) + result = policy.remember(request, 'fred', a=1, b=2) + self.assertEqual(policy.cookie.kw, {'a':1, 'b':2}) + self.assertEqual(result, []) + + def test_forget(self): + request = DummyRequest({}) + policy = self._makeOne(None, None) + result = policy.forget(request) + self.assertEqual(result, []) + + def test_class_implements_IAuthenticationPolicy(self): + from zope.interface.verify import verifyClass + from pyramid.interfaces import IAuthenticationPolicy + verifyClass(IAuthenticationPolicy, self._getTargetClass()) + + def test_instance_implements_IAuthenticationPolicy(self): + from zope.interface.verify import verifyObject + from pyramid.interfaces import IAuthenticationPolicy + verifyObject(IAuthenticationPolicy, self._makeOne(None, None)) + +class TestAuthTktCookieHelper(unittest.TestCase): + def _getTargetClass(self): + from pyramid.authentication import AuthTktCookieHelper + return AuthTktCookieHelper + + def _makeOne(self, *arg, **kw): + helper = self._getTargetClass()(*arg, **kw) + # laziness after moving auth_tkt classes and funcs into + # authentication module + auth_tkt = DummyAuthTktModule() + helper.auth_tkt = auth_tkt + helper.AuthTicket = auth_tkt.AuthTicket + helper.parse_ticket = auth_tkt.parse_ticket + helper.BadTicket = auth_tkt.BadTicket + return helper + + def _makeRequest(self, cookie=None, ipv6=False): + environ = {'wsgi.version': (1,0)} + + if ipv6 is False: + environ['REMOTE_ADDR'] = '1.1.1.1' + else: + environ['REMOTE_ADDR'] = '::1' + environ['SERVER_NAME'] = 'localhost' + return DummyRequest(environ, cookie=cookie) + + def _cookieValue(self, cookie): + items = cookie.value.split('/') + D = {} + for item in items: + k, v = item.split('=', 1) + D[k] = v + return D + + def _parseHeaders(self, headers): + return [ self._parseHeader(header) for header in headers ] + + def _parseHeader(self, header): + cookie = self._parseCookie(header[1]) + return cookie + + def _parseCookie(self, cookie): + from pyramid.compat import SimpleCookie + cookies = SimpleCookie() + cookies.load(cookie) + return cookies.get('auth_tkt') + + def test_init_cookie_str_reissue_invalid(self): + self.assertRaises(ValueError, self._makeOne, 'secret', reissue_time='invalid value') + + def test_init_cookie_str_timeout_invalid(self): + self.assertRaises(ValueError, self._makeOne, 'secret', timeout='invalid value') + + def test_init_cookie_str_max_age_invalid(self): + self.assertRaises(ValueError, self._makeOne, 'secret', max_age='invalid value') + + def test_identify_nocookie(self): + helper = self._makeOne('secret') + request = self._makeRequest() + result = helper.identify(request) + self.assertEqual(result, None) + + def test_identify_cookie_value_is_None(self): + helper = self._makeOne('secret') + request = self._makeRequest(None) + result = helper.identify(request) + self.assertEqual(result, None) + + def test_identify_good_cookie_include_ip(self): + helper = self._makeOne('secret', include_ip=True) + request = self._makeRequest('ticket') + result = helper.identify(request) + self.assertEqual(len(result), 4) + self.assertEqual(result['tokens'], ()) + self.assertEqual(result['userid'], 'userid') + self.assertEqual(result['userdata'], '') + self.assertEqual(result['timestamp'], 0) + self.assertEqual(helper.auth_tkt.value, 'ticket') + self.assertEqual(helper.auth_tkt.remote_addr, '1.1.1.1') + self.assertEqual(helper.auth_tkt.secret, 'secret') + environ = request.environ + self.assertEqual(environ['REMOTE_USER_TOKENS'], ()) + self.assertEqual(environ['REMOTE_USER_DATA'],'') + self.assertEqual(environ['AUTH_TYPE'],'cookie') + + def test_identify_good_cookie_include_ipv6(self): + helper = self._makeOne('secret', include_ip=True) + request = self._makeRequest('ticket', ipv6=True) + result = helper.identify(request) + self.assertEqual(len(result), 4) + self.assertEqual(result['tokens'], ()) + self.assertEqual(result['userid'], 'userid') + self.assertEqual(result['userdata'], '') + self.assertEqual(result['timestamp'], 0) + self.assertEqual(helper.auth_tkt.value, 'ticket') + self.assertEqual(helper.auth_tkt.remote_addr, '::1') + self.assertEqual(helper.auth_tkt.secret, 'secret') + environ = request.environ + self.assertEqual(environ['REMOTE_USER_TOKENS'], ()) + self.assertEqual(environ['REMOTE_USER_DATA'],'') + self.assertEqual(environ['AUTH_TYPE'],'cookie') + + def test_identify_good_cookie_dont_include_ip(self): + helper = self._makeOne('secret', include_ip=False) + request = self._makeRequest('ticket') + result = helper.identify(request) + self.assertEqual(len(result), 4) + self.assertEqual(result['tokens'], ()) + self.assertEqual(result['userid'], 'userid') + self.assertEqual(result['userdata'], '') + self.assertEqual(result['timestamp'], 0) + self.assertEqual(helper.auth_tkt.value, 'ticket') + self.assertEqual(helper.auth_tkt.remote_addr, '0.0.0.0') + self.assertEqual(helper.auth_tkt.secret, 'secret') + environ = request.environ + self.assertEqual(environ['REMOTE_USER_TOKENS'], ()) + self.assertEqual(environ['REMOTE_USER_DATA'],'') + self.assertEqual(environ['AUTH_TYPE'],'cookie') + + def test_identify_good_cookie_int_useridtype(self): + helper = self._makeOne('secret', include_ip=False) + helper.auth_tkt.userid = '1' + helper.auth_tkt.user_data = 'userid_type:int' + request = self._makeRequest('ticket') + result = helper.identify(request) + self.assertEqual(len(result), 4) + self.assertEqual(result['tokens'], ()) + self.assertEqual(result['userid'], 1) + self.assertEqual(result['userdata'], 'userid_type:int') + self.assertEqual(result['timestamp'], 0) + environ = request.environ + self.assertEqual(environ['REMOTE_USER_TOKENS'], ()) + self.assertEqual(environ['REMOTE_USER_DATA'],'userid_type:int') + self.assertEqual(environ['AUTH_TYPE'],'cookie') + + def test_identify_nonuseridtype_user_data(self): + helper = self._makeOne('secret', include_ip=False) + helper.auth_tkt.userid = '1' + helper.auth_tkt.user_data = 'bogus:int' + request = self._makeRequest('ticket') + result = helper.identify(request) + self.assertEqual(len(result), 4) + self.assertEqual(result['tokens'], ()) + self.assertEqual(result['userid'], '1') + self.assertEqual(result['userdata'], 'bogus:int') + self.assertEqual(result['timestamp'], 0) + environ = request.environ + self.assertEqual(environ['REMOTE_USER_TOKENS'], ()) + self.assertEqual(environ['REMOTE_USER_DATA'],'bogus:int') + self.assertEqual(environ['AUTH_TYPE'],'cookie') + + def test_identify_good_cookie_unknown_useridtype(self): + helper = self._makeOne('secret', include_ip=False) + helper.auth_tkt.userid = 'abc' + helper.auth_tkt.user_data = 'userid_type:unknown' + request = self._makeRequest('ticket') + result = helper.identify(request) + self.assertEqual(len(result), 4) + self.assertEqual(result['tokens'], ()) + self.assertEqual(result['userid'], 'abc') + self.assertEqual(result['userdata'], 'userid_type:unknown') + self.assertEqual(result['timestamp'], 0) + environ = request.environ + self.assertEqual(environ['REMOTE_USER_TOKENS'], ()) + self.assertEqual(environ['REMOTE_USER_DATA'],'userid_type:unknown') + self.assertEqual(environ['AUTH_TYPE'],'cookie') + + def test_identify_good_cookie_b64str_useridtype(self): + from base64 import b64encode + helper = self._makeOne('secret', include_ip=False) + helper.auth_tkt.userid = b64encode(b'encoded').strip() + helper.auth_tkt.user_data = 'userid_type:b64str' + request = self._makeRequest('ticket') + result = helper.identify(request) + self.assertEqual(len(result), 4) + self.assertEqual(result['tokens'], ()) + self.assertEqual(result['userid'], b'encoded') + self.assertEqual(result['userdata'], 'userid_type:b64str') + self.assertEqual(result['timestamp'], 0) + environ = request.environ + self.assertEqual(environ['REMOTE_USER_TOKENS'], ()) + self.assertEqual(environ['REMOTE_USER_DATA'],'userid_type:b64str') + self.assertEqual(environ['AUTH_TYPE'],'cookie') + + def test_identify_good_cookie_b64unicode_useridtype(self): + from base64 import b64encode + helper = self._makeOne('secret', include_ip=False) + helper.auth_tkt.userid = b64encode(b'\xc3\xa9ncoded').strip() + helper.auth_tkt.user_data = 'userid_type:b64unicode' + request = self._makeRequest('ticket') + result = helper.identify(request) + self.assertEqual(len(result), 4) + self.assertEqual(result['tokens'], ()) + self.assertEqual(result['userid'], text_(b'\xc3\xa9ncoded', 'utf-8')) + self.assertEqual(result['userdata'], 'userid_type:b64unicode') + self.assertEqual(result['timestamp'], 0) + environ = request.environ + self.assertEqual(environ['REMOTE_USER_TOKENS'], ()) + self.assertEqual(environ['REMOTE_USER_DATA'],'userid_type:b64unicode') + self.assertEqual(environ['AUTH_TYPE'],'cookie') + + def test_identify_bad_cookie(self): + helper = self._makeOne('secret', include_ip=True) + helper.auth_tkt.parse_raise = True + request = self._makeRequest('ticket') + result = helper.identify(request) + self.assertEqual(result, None) + + def test_identify_cookie_timeout(self): + helper = self._makeOne('secret', timeout=1) + self.assertEqual(helper.timeout, 1) + + def test_identify_cookie_str_timeout(self): + helper = self._makeOne('secret', timeout='1') + self.assertEqual(helper.timeout, 1) + + def test_identify_cookie_timeout_aged(self): + import time + helper = self._makeOne('secret', timeout=10) + now = time.time() + helper.auth_tkt.timestamp = now - 1 + helper.now = now + 10 + helper.auth_tkt.tokens = (text_('a'), ) + request = self._makeRequest('bogus') + result = helper.identify(request) + self.assertFalse(result) + + def test_identify_cookie_reissue(self): + import time + helper = self._makeOne('secret', timeout=10, reissue_time=0) + now = time.time() + helper.auth_tkt.timestamp = now + helper.now = now + 1 + helper.auth_tkt.tokens = (text_('a'), ) + request = self._makeRequest('bogus') + result = helper.identify(request) + self.assertTrue(result) + self.assertEqual(len(request.callbacks), 1) + response = DummyResponse() + request.callbacks[0](request, response) + self.assertEqual(len(response.headerlist), 3) + self.assertEqual(response.headerlist[0][0], 'Set-Cookie') + + def test_identify_cookie_str_reissue(self): + import time + helper = self._makeOne('secret', timeout=10, reissue_time='0') + now = time.time() + helper.auth_tkt.timestamp = now + helper.now = now + 1 + helper.auth_tkt.tokens = (text_('a'), ) + request = self._makeRequest('bogus') + result = helper.identify(request) + self.assertTrue(result) + self.assertEqual(len(request.callbacks), 1) + response = DummyResponse() + request.callbacks[0](request, response) + self.assertEqual(len(response.headerlist), 3) + self.assertEqual(response.headerlist[0][0], 'Set-Cookie') + + def test_identify_cookie_reissue_already_reissued_this_request(self): + import time + helper = self._makeOne('secret', timeout=10, reissue_time=0) + now = time.time() + helper.auth_tkt.timestamp = now + helper.now = now + 1 + request = self._makeRequest('bogus') + request._authtkt_reissued = True + result = helper.identify(request) + self.assertTrue(result) + self.assertEqual(len(request.callbacks), 0) + + def test_identify_cookie_reissue_notyet(self): + import time + helper = self._makeOne('secret', timeout=10, reissue_time=10) + now = time.time() + helper.auth_tkt.timestamp = now + helper.now = now + 1 + request = self._makeRequest('bogus') + result = helper.identify(request) + self.assertTrue(result) + self.assertEqual(len(request.callbacks), 0) + + def test_identify_cookie_reissue_revoked_by_forget(self): + import time + helper = self._makeOne('secret', timeout=10, reissue_time=0) + now = time.time() + helper.auth_tkt.timestamp = now + helper.now = now + 1 + request = self._makeRequest('bogus') + result = helper.identify(request) + self.assertTrue(result) + self.assertEqual(len(request.callbacks), 1) + result = helper.forget(request) + self.assertTrue(result) + self.assertEqual(len(request.callbacks), 1) + response = DummyResponse() + request.callbacks[0](request, response) + self.assertEqual(len(response.headerlist), 0) + + def test_identify_cookie_reissue_revoked_by_remember(self): + import time + helper = self._makeOne('secret', timeout=10, reissue_time=0) + now = time.time() + helper.auth_tkt.timestamp = now + helper.now = now + 1 + request = self._makeRequest('bogus') + result = helper.identify(request) + self.assertTrue(result) + self.assertEqual(len(request.callbacks), 1) + result = helper.remember(request, 'bob') + self.assertTrue(result) + self.assertEqual(len(request.callbacks), 1) + response = DummyResponse() + request.callbacks[0](request, response) + self.assertEqual(len(response.headerlist), 0) + + def test_identify_cookie_reissue_with_tokens_default(self): + # see https://github.com/Pylons/pyramid/issues#issue/108 + import time + helper = self._makeOne('secret', timeout=10, reissue_time=0) + auth_tkt = DummyAuthTktModule(tokens=['']) + helper.auth_tkt = auth_tkt + helper.AuthTicket = auth_tkt.AuthTicket + helper.parse_ticket = auth_tkt.parse_ticket + helper.BadTicket = auth_tkt.BadTicket + now = time.time() + helper.auth_tkt.timestamp = now + helper.now = now + 1 + request = self._makeRequest('bogus') + result = helper.identify(request) + self.assertTrue(result) + self.assertEqual(len(request.callbacks), 1) + response = DummyResponse() + request.callbacks[0](None, response) + self.assertEqual(len(response.headerlist), 3) + self.assertEqual(response.headerlist[0][0], 'Set-Cookie') + self.assertTrue("/tokens=/" in response.headerlist[0][1]) + + def test_remember(self): + helper = self._makeOne('secret') + request = self._makeRequest() + result = helper.remember(request, 'userid') + self.assertEqual(len(result), 3) + + self.assertEqual(result[0][0], 'Set-Cookie') + self.assertTrue(result[0][1].endswith('; Path=/; SameSite=Lax')) + self.assertTrue(result[0][1].startswith('auth_tkt=')) + + self.assertEqual(result[1][0], 'Set-Cookie') + self.assertTrue(result[1][1].endswith( + '; Domain=localhost; Path=/; SameSite=Lax')) + self.assertTrue(result[1][1].startswith('auth_tkt=')) + + self.assertEqual(result[2][0], 'Set-Cookie') + self.assertTrue(result[2][1].endswith( + '; Domain=.localhost; Path=/; SameSite=Lax')) + self.assertTrue(result[2][1].startswith('auth_tkt=')) + + def test_remember_nondefault_samesite(self): + helper = self._makeOne('secret', samesite='Strict') + request = self._makeRequest() + result = helper.remember(request, 'userid') + self.assertEqual(len(result), 3) + + self.assertEqual(result[0][0], 'Set-Cookie') + self.assertTrue(result[0][1].endswith('; Path=/; SameSite=Strict')) + self.assertTrue(result[0][1].startswith('auth_tkt=')) + + self.assertEqual(result[1][0], 'Set-Cookie') + self.assertTrue(result[1][1].endswith( + '; Domain=localhost; Path=/; SameSite=Strict')) + self.assertTrue(result[1][1].startswith('auth_tkt=')) + + self.assertEqual(result[2][0], 'Set-Cookie') + self.assertTrue(result[2][1].endswith( + '; Domain=.localhost; Path=/; SameSite=Strict')) + self.assertTrue(result[2][1].startswith('auth_tkt=')) + + def test_remember_None_samesite(self): + helper = self._makeOne('secret', samesite=None) + request = self._makeRequest() + result = helper.remember(request, 'userid') + self.assertEqual(len(result), 3) + + self.assertEqual(result[0][0], 'Set-Cookie') + self.assertTrue(result[0][1].endswith('; Path=/')) # no samesite + self.assertTrue(result[0][1].startswith('auth_tkt=')) + + self.assertEqual(result[1][0], 'Set-Cookie') + self.assertTrue(result[1][1].endswith( + '; Domain=localhost; Path=/')) + self.assertTrue(result[1][1].startswith('auth_tkt=')) + + self.assertEqual(result[2][0], 'Set-Cookie') + self.assertTrue(result[2][1].endswith( + '; Domain=.localhost; Path=/')) + self.assertTrue(result[2][1].startswith('auth_tkt=')) + + def test_remember_include_ip(self): + helper = self._makeOne('secret', include_ip=True) + request = self._makeRequest() + result = helper.remember(request, 'other') + self.assertEqual(len(result), 3) + + self.assertEqual(result[0][0], 'Set-Cookie') + self.assertTrue(result[0][1].endswith('; Path=/; SameSite=Lax')) + self.assertTrue(result[0][1].startswith('auth_tkt=')) + + self.assertEqual(result[1][0], 'Set-Cookie') + self.assertTrue(result[1][1].endswith( + '; Domain=localhost; Path=/; SameSite=Lax')) + self.assertTrue(result[1][1].startswith('auth_tkt=')) + + self.assertEqual(result[2][0], 'Set-Cookie') + self.assertTrue(result[2][1].endswith( + '; Domain=.localhost; Path=/; SameSite=Lax')) + self.assertTrue(result[2][1].startswith('auth_tkt=')) + + def test_remember_path(self): + helper = self._makeOne('secret', include_ip=True, + path="/cgi-bin/app.cgi/") + request = self._makeRequest() + result = helper.remember(request, 'other') + self.assertEqual(len(result), 3) + + self.assertEqual(result[0][0], 'Set-Cookie') + self.assertTrue(result[0][1].endswith( + '; Path=/cgi-bin/app.cgi/; SameSite=Lax')) + self.assertTrue(result[0][1].startswith('auth_tkt=')) + + self.assertEqual(result[1][0], 'Set-Cookie') + self.assertTrue(result[1][1].endswith( + '; Domain=localhost; Path=/cgi-bin/app.cgi/; SameSite=Lax')) + self.assertTrue(result[1][1].startswith('auth_tkt=')) + + self.assertEqual(result[2][0], 'Set-Cookie') + self.assertTrue(result[2][1].endswith( + '; Domain=.localhost; Path=/cgi-bin/app.cgi/; SameSite=Lax')) + self.assertTrue(result[2][1].startswith('auth_tkt=')) + + def test_remember_http_only(self): + helper = self._makeOne('secret', include_ip=True, http_only=True) + request = self._makeRequest() + result = helper.remember(request, 'other') + self.assertEqual(len(result), 3) + + self.assertEqual(result[0][0], 'Set-Cookie') + self.assertTrue(result[0][1].endswith('; HttpOnly; SameSite=Lax')) + self.assertTrue(result[0][1].startswith('auth_tkt=')) + + self.assertEqual(result[1][0], 'Set-Cookie') + self.assertTrue('; HttpOnly' in result[1][1]) + self.assertTrue(result[1][1].startswith('auth_tkt=')) + + self.assertEqual(result[2][0], 'Set-Cookie') + self.assertTrue('; HttpOnly' in result[2][1]) + self.assertTrue(result[2][1].startswith('auth_tkt=')) + + def test_remember_secure(self): + helper = self._makeOne('secret', include_ip=True, secure=True) + request = self._makeRequest() + result = helper.remember(request, 'other') + self.assertEqual(len(result), 3) + + self.assertEqual(result[0][0], 'Set-Cookie') + self.assertTrue('; secure' in result[0][1]) + self.assertTrue(result[0][1].startswith('auth_tkt=')) + + self.assertEqual(result[1][0], 'Set-Cookie') + self.assertTrue('; secure' in result[1][1]) + self.assertTrue(result[1][1].startswith('auth_tkt=')) + + self.assertEqual(result[2][0], 'Set-Cookie') + self.assertTrue('; secure' in result[2][1]) + self.assertTrue(result[2][1].startswith('auth_tkt=')) + + def test_remember_wild_domain_disabled(self): + helper = self._makeOne('secret', wild_domain=False) + request = self._makeRequest() + result = helper.remember(request, 'other') + self.assertEqual(len(result), 2) + + self.assertEqual(result[0][0], 'Set-Cookie') + self.assertTrue(result[0][1].endswith('; Path=/; SameSite=Lax')) + self.assertTrue(result[0][1].startswith('auth_tkt=')) + + self.assertEqual(result[1][0], 'Set-Cookie') + self.assertTrue(result[1][1].endswith( + '; Domain=localhost; Path=/; SameSite=Lax')) + self.assertTrue(result[1][1].startswith('auth_tkt=')) + + def test_remember_parent_domain(self): + helper = self._makeOne('secret', parent_domain=True) + request = self._makeRequest() + request.domain = 'www.example.com' + result = helper.remember(request, 'other') + self.assertEqual(len(result), 1) + + self.assertEqual(result[0][0], 'Set-Cookie') + self.assertTrue(result[0][1].endswith( + '; Domain=.example.com; Path=/; SameSite=Lax')) + self.assertTrue(result[0][1].startswith('auth_tkt=')) + + def test_remember_parent_domain_supercedes_wild_domain(self): + helper = self._makeOne('secret', parent_domain=True, wild_domain=True) + request = self._makeRequest() + request.domain = 'www.example.com' + result = helper.remember(request, 'other') + self.assertEqual(len(result), 1) + self.assertTrue(result[0][1].endswith( + '; Domain=.example.com; Path=/; SameSite=Lax')) + + def test_remember_explicit_domain(self): + helper = self._makeOne('secret', domain='pyramid.bazinga') + request = self._makeRequest() + request.domain = 'www.example.com' + result = helper.remember(request, 'other') + self.assertEqual(len(result), 1) + + self.assertEqual(result[0][0], 'Set-Cookie') + self.assertTrue(result[0][1].endswith( + '; Domain=pyramid.bazinga; Path=/; SameSite=Lax')) + self.assertTrue(result[0][1].startswith('auth_tkt=')) + + def test_remember_domain_supercedes_parent_and_wild_domain(self): + helper = self._makeOne('secret', domain='pyramid.bazinga', + parent_domain=True, wild_domain=True) + request = self._makeRequest() + request.domain = 'www.example.com' + result = helper.remember(request, 'other') + self.assertEqual(len(result), 1) + self.assertTrue(result[0][1].endswith( + '; Domain=pyramid.bazinga; Path=/; SameSite=Lax')) + + def test_remember_binary_userid(self): + import base64 + helper = self._makeOne('secret') + request = self._makeRequest() + result = helper.remember(request, b'userid') + values = self._parseHeaders(result) + self.assertEqual(len(result), 3) + val = self._cookieValue(values[0]) + self.assertEqual(val['userid'], + text_(base64.b64encode(b'userid').strip())) + self.assertEqual(val['user_data'], 'userid_type:b64str') + + def test_remember_int_userid(self): + helper = self._makeOne('secret') + request = self._makeRequest() + result = helper.remember(request, 1) + values = self._parseHeaders(result) + self.assertEqual(len(result), 3) + val = self._cookieValue(values[0]) + self.assertEqual(val['userid'], '1') + self.assertEqual(val['user_data'], 'userid_type:int') + + def test_remember_long_userid(self): + from pyramid.compat import long + helper = self._makeOne('secret') + request = self._makeRequest() + result = helper.remember(request, long(1)) + values = self._parseHeaders(result) + self.assertEqual(len(result), 3) + val = self._cookieValue(values[0]) + self.assertEqual(val['userid'], '1') + self.assertEqual(val['user_data'], 'userid_type:int') + + def test_remember_unicode_userid(self): + import base64 + helper = self._makeOne('secret') + request = self._makeRequest() + userid = text_(b'\xc2\xa9', 'utf-8') + result = helper.remember(request, userid) + values = self._parseHeaders(result) + self.assertEqual(len(result), 3) + val = self._cookieValue(values[0]) + self.assertEqual(val['userid'], + text_(base64.b64encode(userid.encode('utf-8')))) + self.assertEqual(val['user_data'], 'userid_type:b64unicode') + + def test_remember_insane_userid(self): + helper = self._makeOne('secret') + request = self._makeRequest() + userid = object() + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter('always', RuntimeWarning) + result = helper.remember(request, userid) + self.assertTrue(str(w[-1].message).startswith('userid is of type')) + values = self._parseHeaders(result) + self.assertEqual(len(result), 3) + value = values[0] + self.assertTrue('userid' in value.value) + + def test_remember_max_age(self): + helper = self._makeOne('secret') + request = self._makeRequest() + result = helper.remember(request, 'userid', max_age=500) + values = self._parseHeaders(result) + self.assertEqual(len(result), 3) + + self.assertEqual(values[0]['max-age'], '500') + self.assertTrue(values[0]['expires']) + + def test_remember_str_max_age(self): + helper = self._makeOne('secret') + request = self._makeRequest() + result = helper.remember(request, 'userid', max_age='500') + values = self._parseHeaders(result) + self.assertEqual(len(result), 3) + + self.assertEqual(values[0]['max-age'], '500') + self.assertTrue(values[0]['expires']) + + def test_remember_str_max_age_invalid(self): + helper = self._makeOne('secret') + request = self._makeRequest() + self.assertRaises(ValueError, helper.remember, request, 'userid', max_age='invalid value') + + def test_remember_tokens(self): + helper = self._makeOne('secret') + request = self._makeRequest() + result = helper.remember(request, 'other', tokens=('foo', 'bar')) + self.assertEqual(len(result), 3) + + self.assertEqual(result[0][0], 'Set-Cookie') + self.assertTrue("/tokens=foo|bar/" in result[0][1]) + + self.assertEqual(result[1][0], 'Set-Cookie') + self.assertTrue("/tokens=foo|bar/" in result[1][1]) + + self.assertEqual(result[2][0], 'Set-Cookie') + self.assertTrue("/tokens=foo|bar/" in result[2][1]) + + def test_remember_samesite_nondefault(self): + helper = self._makeOne('secret', samesite='Strict') + request = self._makeRequest() + result = helper.remember(request, 'userid') + self.assertEqual(len(result), 3) + + self.assertEqual(result[0][0], 'Set-Cookie') + cookieval = result[0][1] + self.assertTrue('SameSite=Strict' in + [x.strip() for x in cookieval.split(';')], cookieval) + + self.assertEqual(result[1][0], 'Set-Cookie') + cookieval = result[1][1] + self.assertTrue('SameSite=Strict' in + [x.strip() for x in cookieval.split(';')], cookieval) + + self.assertEqual(result[2][0], 'Set-Cookie') + cookieval = result[2][1] + self.assertTrue('SameSite=Strict' in + [x.strip() for x in cookieval.split(';')], cookieval) + + def test_remember_samesite_default(self): + helper = self._makeOne('secret') + request = self._makeRequest() + result = helper.remember(request, 'userid') + self.assertEqual(len(result), 3) + + self.assertEqual(result[0][0], 'Set-Cookie') + cookieval = result[0][1] + self.assertTrue('SameSite=Lax' in + [x.strip() for x in cookieval.split(';')], cookieval) + + self.assertEqual(result[1][0], 'Set-Cookie') + cookieval = result[1][1] + self.assertTrue('SameSite=Lax' in + [x.strip() for x in cookieval.split(';')], cookieval) + + self.assertEqual(result[2][0], 'Set-Cookie') + cookieval = result[2][1] + self.assertTrue('SameSite=Lax' in + [x.strip() for x in cookieval.split(';')], cookieval) + + def test_remember_unicode_but_ascii_token(self): + helper = self._makeOne('secret') + request = self._makeRequest() + la = text_(b'foo', 'utf-8') + result = helper.remember(request, 'other', tokens=(la,)) + # tokens must be str type on both Python 2 and 3 + self.assertTrue("/tokens=foo/" in result[0][1]) + + def test_remember_nonascii_token(self): + helper = self._makeOne('secret') + request = self._makeRequest() + la = text_(b'La Pe\xc3\xb1a', 'utf-8') + self.assertRaises(ValueError, helper.remember, request, 'other', + tokens=(la,)) + + def test_remember_invalid_token_format(self): + helper = self._makeOne('secret') + request = self._makeRequest() + self.assertRaises(ValueError, helper.remember, request, 'other', + tokens=('foo bar',)) + self.assertRaises(ValueError, helper.remember, request, 'other', + tokens=('1bar',)) + + def test_forget(self): + helper = self._makeOne('secret') + request = self._makeRequest() + headers = helper.forget(request) + self.assertEqual(len(headers), 3) + name, value = headers[0] + self.assertEqual(name, 'Set-Cookie') + self.assertEqual( + value, + 'auth_tkt=; Max-Age=0; Path=/; ' + 'expires=Wed, 31-Dec-97 23:59:59 GMT; SameSite=Lax' + ) + name, value = headers[1] + self.assertEqual(name, 'Set-Cookie') + self.assertEqual( + value, + 'auth_tkt=; Domain=localhost; Max-Age=0; Path=/; ' + 'expires=Wed, 31-Dec-97 23:59:59 GMT; SameSite=Lax' + ) + name, value = headers[2] + self.assertEqual(name, 'Set-Cookie') + self.assertEqual( + value, + 'auth_tkt=; Domain=.localhost; Max-Age=0; Path=/; ' + 'expires=Wed, 31-Dec-97 23:59:59 GMT; SameSite=Lax' + ) + +class TestAuthTicket(unittest.TestCase): + def _makeOne(self, *arg, **kw): + from pyramid.authentication import AuthTicket + return AuthTicket(*arg, **kw) + + def test_ctor_with_tokens(self): + ticket = self._makeOne('secret', 'userid', 'ip', tokens=('a', 'b')) + self.assertEqual(ticket.tokens, 'a,b') + + def test_ctor_with_time(self): + ticket = self._makeOne('secret', 'userid', 'ip', time='time') + self.assertEqual(ticket.time, 'time') + + def test_digest(self): + ticket = self._makeOne('secret', 'userid', '0.0.0.0', time=10) + result = ticket.digest() + self.assertEqual(result, '126fd6224912187ee9ffa80e0b81420c') + + def test_digest_sha512(self): + ticket = self._makeOne('secret', 'userid', '0.0.0.0', + time=10, hashalg='sha512') + result = ticket.digest() + self.assertEqual(result, '74770b2e0d5b1a54c2a466ec567a40f7d7823576aa49'\ + '3c65fc3445e9b44097f4a80410319ef8cb256a2e60b9'\ + 'c2002e48a9e33a3e8ee4379352c04ef96d2cb278') + + def test_cookie_value(self): + ticket = self._makeOne('secret', 'userid', '0.0.0.0', time=10, + tokens=('a', 'b')) + result = ticket.cookie_value() + self.assertEqual(result, + '66f9cc3e423dc57c91df696cf3d1f0d80000000auserid!a,b!') + + def test_ipv4(self): + ticket = self._makeOne('secret', 'userid', '198.51.100.1', + time=10, hashalg='sha256') + result = ticket.cookie_value() + self.assertEqual(result, 'b3e7156db4f8abde4439c4a6499a0668f9e7ffd7fa27b'\ + '798400ecdade8d76c530000000auserid!') + + def test_ipv6(self): + ticket = self._makeOne('secret', 'userid', '2001:db8::1', + time=10, hashalg='sha256') + result = ticket.cookie_value() + self.assertEqual(result, 'd025b601a0f12ca6d008aa35ff3a22b7d8f3d1c1456c8'\ + '5becf8760cd7a2fa4910000000auserid!') + +class TestBadTicket(unittest.TestCase): + def _makeOne(self, msg, expected=None): + from pyramid.authentication import BadTicket + return BadTicket(msg, expected) + + def test_it(self): + exc = self._makeOne('msg', expected=True) + self.assertEqual(exc.expected, True) + self.assertTrue(isinstance(exc, Exception)) + +class Test_parse_ticket(unittest.TestCase): + def _callFUT(self, secret, ticket, ip, hashalg='md5'): + from pyramid.authentication import parse_ticket + return parse_ticket(secret, ticket, ip, hashalg) + + def _assertRaisesBadTicket(self, secret, ticket, ip, hashalg='md5'): + from pyramid.authentication import BadTicket + self.assertRaises(BadTicket,self._callFUT, secret, ticket, ip, hashalg) + + def test_bad_timestamp(self): + ticket = 'x' * 64 + self._assertRaisesBadTicket('secret', ticket, 'ip') + + def test_bad_userid_or_data(self): + ticket = 'x' * 32 + '11111111' + 'x' * 10 + self._assertRaisesBadTicket('secret', ticket, 'ip') + + def test_digest_sig_incorrect(self): + ticket = 'x' * 32 + '11111111' + 'a!b!c' + self._assertRaisesBadTicket('secret', ticket, '0.0.0.0') + + def test_correct_with_user_data(self): + ticket = text_('66f9cc3e423dc57c91df696cf3d1f0d80000000auserid!a,b!') + result = self._callFUT('secret', ticket, '0.0.0.0') + self.assertEqual(result, (10, 'userid', ['a', 'b'], '')) + + def test_correct_with_user_data_sha512(self): + ticket = text_('7d947cdef99bad55f8e3382a8bd089bb9dd0547f7925b7d189adc1' + '160cab0ec0e6888faa41eba641a18522b26f19109f3ffafb769767' + 'ba8a26d02aaeae56599a0000000auserid!a,b!') + result = self._callFUT('secret', ticket, '0.0.0.0', 'sha512') + self.assertEqual(result, (10, 'userid', ['a', 'b'], '')) + + def test_ipv4(self): + ticket = text_('b3e7156db4f8abde4439c4a6499a0668f9e7ffd7fa27b798400ecd' + 'ade8d76c530000000auserid!') + result = self._callFUT('secret', ticket, '198.51.100.1', 'sha256') + self.assertEqual(result, (10, 'userid', [''], '')) + + def test_ipv6(self): + ticket = text_('d025b601a0f12ca6d008aa35ff3a22b7d8f3d1c1456c85becf8760' + 'cd7a2fa4910000000auserid!') + result = self._callFUT('secret', ticket, '2001:db8::1', 'sha256') + self.assertEqual(result, (10, 'userid', [''], '')) + +class TestSessionAuthenticationPolicy(unittest.TestCase): + def _getTargetClass(self): + from pyramid.authentication import SessionAuthenticationPolicy + return SessionAuthenticationPolicy + + def _makeOne(self, callback=None, prefix=''): + return self._getTargetClass()(prefix=prefix, callback=callback) + + def test_class_implements_IAuthenticationPolicy(self): + from zope.interface.verify import verifyClass + from pyramid.interfaces import IAuthenticationPolicy + verifyClass(IAuthenticationPolicy, self._getTargetClass()) + + def test_instance_implements_IAuthenticationPolicy(self): + from zope.interface.verify import verifyObject + from pyramid.interfaces import IAuthenticationPolicy + verifyObject(IAuthenticationPolicy, self._makeOne()) + + def test_unauthenticated_userid_returns_None(self): + request = DummyRequest() + policy = self._makeOne() + self.assertEqual(policy.unauthenticated_userid(request), None) + + def test_unauthenticated_userid(self): + request = DummyRequest(session={'userid':'fred'}) + policy = self._makeOne() + self.assertEqual(policy.unauthenticated_userid(request), 'fred') + + def test_authenticated_userid_no_cookie_identity(self): + request = DummyRequest() + policy = self._makeOne() + self.assertEqual(policy.authenticated_userid(request), None) + + def test_authenticated_userid_callback_returns_None(self): + request = DummyRequest(session={'userid':'fred'}) + def callback(userid, request): + return None + policy = self._makeOne(callback) + self.assertEqual(policy.authenticated_userid(request), None) + + def test_authenticated_userid(self): + request = DummyRequest(session={'userid':'fred'}) + def callback(userid, request): + return True + policy = self._makeOne(callback) + self.assertEqual(policy.authenticated_userid(request), 'fred') + + def test_effective_principals_no_identity(self): + from pyramid.security import Everyone + request = DummyRequest() + policy = self._makeOne() + self.assertEqual(policy.effective_principals(request), [Everyone]) + + def test_effective_principals_callback_returns_None(self): + from pyramid.security import Everyone + request = DummyRequest(session={'userid':'fred'}) + def callback(userid, request): + return None + policy = self._makeOne(callback) + self.assertEqual(policy.effective_principals(request), [Everyone]) + + def test_effective_principals(self): + from pyramid.security import Everyone + from pyramid.security import Authenticated + request = DummyRequest(session={'userid':'fred'}) + def callback(userid, request): + return ['group.foo'] + policy = self._makeOne(callback) + self.assertEqual(policy.effective_principals(request), + [Everyone, Authenticated, 'fred', 'group.foo']) + + def test_remember(self): + request = DummyRequest() + policy = self._makeOne() + result = policy.remember(request, 'fred') + self.assertEqual(request.session.get('userid'), 'fred') + self.assertEqual(result, []) + + def test_forget(self): + request = DummyRequest(session={'userid':'fred'}) + policy = self._makeOne() + result = policy.forget(request) + self.assertEqual(request.session.get('userid'), None) + self.assertEqual(result, []) + + def test_forget_no_identity(self): + request = DummyRequest() + policy = self._makeOne() + result = policy.forget(request) + self.assertEqual(request.session.get('userid'), None) + self.assertEqual(result, []) + +class TestBasicAuthAuthenticationPolicy(unittest.TestCase): + def _getTargetClass(self): + from pyramid.authentication import BasicAuthAuthenticationPolicy as cls + return cls + + def _makeOne(self, check): + return self._getTargetClass()(check, realm='SomeRealm') + + def test_class_implements_IAuthenticationPolicy(self): + from zope.interface.verify import verifyClass + from pyramid.interfaces import IAuthenticationPolicy + verifyClass(IAuthenticationPolicy, self._getTargetClass()) + + def test_unauthenticated_userid(self): + import base64 + request = testing.DummyRequest() + request.headers['Authorization'] = 'Basic %s' % base64.b64encode( + bytes_('chrisr:password')).decode('ascii') + policy = self._makeOne(None) + self.assertEqual(policy.unauthenticated_userid(request), 'chrisr') + + def test_unauthenticated_userid_no_credentials(self): + request = testing.DummyRequest() + policy = self._makeOne(None) + self.assertEqual(policy.unauthenticated_userid(request), None) + + def test_unauthenticated_bad_header(self): + request = testing.DummyRequest() + request.headers['Authorization'] = '...' + policy = self._makeOne(None) + self.assertEqual(policy.unauthenticated_userid(request), None) + + def test_unauthenticated_userid_not_basic(self): + request = testing.DummyRequest() + request.headers['Authorization'] = 'Complicated things' + policy = self._makeOne(None) + self.assertEqual(policy.unauthenticated_userid(request), None) + + def test_unauthenticated_userid_corrupt_base64(self): + request = testing.DummyRequest() + request.headers['Authorization'] = 'Basic chrisr:password' + policy = self._makeOne(None) + self.assertEqual(policy.unauthenticated_userid(request), None) + + def test_authenticated_userid(self): + import base64 + request = testing.DummyRequest() + request.headers['Authorization'] = 'Basic %s' % base64.b64encode( + bytes_('chrisr:password')).decode('ascii') + def check(username, password, request): + return [] + policy = self._makeOne(check) + self.assertEqual(policy.authenticated_userid(request), 'chrisr') + + def test_authenticated_userid_utf8(self): + import base64 + request = testing.DummyRequest() + inputs = (b'm\xc3\xb6rk\xc3\xb6:' + b'm\xc3\xb6rk\xc3\xb6password').decode('utf-8') + request.headers['Authorization'] = 'Basic %s' % ( + base64.b64encode(inputs.encode('utf-8')).decode('latin-1')) + def check(username, password, request): + return [] + policy = self._makeOne(check) + self.assertEqual(policy.authenticated_userid(request), + b'm\xc3\xb6rk\xc3\xb6'.decode('utf-8')) + + def test_authenticated_userid_latin1(self): + import base64 + request = testing.DummyRequest() + inputs = (b'm\xc3\xb6rk\xc3\xb6:' + b'm\xc3\xb6rk\xc3\xb6password').decode('utf-8') + request.headers['Authorization'] = 'Basic %s' % ( + base64.b64encode(inputs.encode('latin-1')).decode('latin-1')) + def check(username, password, request): + return [] + policy = self._makeOne(check) + self.assertEqual(policy.authenticated_userid(request), + b'm\xc3\xb6rk\xc3\xb6'.decode('utf-8')) + + def test_unauthenticated_userid_invalid_payload(self): + import base64 + request = testing.DummyRequest() + request.headers['Authorization'] = 'Basic %s' % base64.b64encode( + bytes_('chrisrpassword')).decode('ascii') + policy = self._makeOne(None) + self.assertEqual(policy.unauthenticated_userid(request), None) + + def test_remember(self): + policy = self._makeOne(None) + self.assertEqual(policy.remember(None, None), []) + + def test_forget(self): + policy = self._makeOne(None) + self.assertEqual(policy.forget(None), [ + ('WWW-Authenticate', 'Basic realm="SomeRealm"')]) + + +class TestExtractHTTPBasicCredentials(unittest.TestCase): + def _get_func(self): + from pyramid.authentication import extract_http_basic_credentials + return extract_http_basic_credentials + + def test_no_auth_header(self): + request = testing.DummyRequest() + fn = self._get_func() + + self.assertIsNone(fn(request)) + + def test_invalid_payload(self): + import base64 + request = testing.DummyRequest() + request.headers['Authorization'] = 'Basic %s' % base64.b64encode( + bytes_('chrisrpassword')).decode('ascii') + fn = self._get_func() + self.assertIsNone(fn(request)) + + def test_not_a_basic_auth_scheme(self): + import base64 + request = testing.DummyRequest() + request.headers['Authorization'] = 'OtherScheme %s' % base64.b64encode( + bytes_('chrisr:password')).decode('ascii') + fn = self._get_func() + self.assertIsNone(fn(request)) + + def test_no_base64_encoding(self): + request = testing.DummyRequest() + request.headers['Authorization'] = 'Basic ...' + fn = self._get_func() + self.assertIsNone(fn(request)) + + def test_latin1_payload(self): + import base64 + request = testing.DummyRequest() + inputs = (b'm\xc3\xb6rk\xc3\xb6:' + b'm\xc3\xb6rk\xc3\xb6password').decode('utf-8') + request.headers['Authorization'] = 'Basic %s' % ( + base64.b64encode(inputs.encode('latin-1')).decode('latin-1')) + fn = self._get_func() + self.assertEqual(fn(request), ( + b'm\xc3\xb6rk\xc3\xb6'.decode('utf-8'), + b'm\xc3\xb6rk\xc3\xb6password'.decode('utf-8') + )) + + def test_utf8_payload(self): + import base64 + request = testing.DummyRequest() + inputs = (b'm\xc3\xb6rk\xc3\xb6:' + b'm\xc3\xb6rk\xc3\xb6password').decode('utf-8') + request.headers['Authorization'] = 'Basic %s' % ( + base64.b64encode(inputs.encode('utf-8')).decode('latin-1')) + fn = self._get_func() + self.assertEqual(fn(request), ( + b'm\xc3\xb6rk\xc3\xb6'.decode('utf-8'), + b'm\xc3\xb6rk\xc3\xb6password'.decode('utf-8') + )) + + def test_namedtuple_return(self): + import base64 + request = testing.DummyRequest() + request.headers['Authorization'] = 'Basic %s' % base64.b64encode( + bytes_('chrisr:pass')).decode('ascii') + fn = self._get_func() + result = fn(request) + + self.assertEqual(result.username, 'chrisr') + self.assertEqual(result.password, 'pass') + +class DummyContext: + pass + +class DummyCookies(object): + def __init__(self, cookie): + self.cookie = cookie + + def get(self, name): + return self.cookie + +class DummyRequest: + domain = 'localhost' + def __init__(self, environ=None, session=None, registry=None, cookie=None): + self.environ = environ or {} + self.session = session or {} + self.registry = registry + self.callbacks = [] + self.cookies = DummyCookies(cookie) + + def add_response_callback(self, callback): + self.callbacks.append(callback) + +class DummyWhoPlugin: + def remember(self, environ, identity): + return environ, identity + + def forget(self, environ, identity): + return environ, identity + +class DummyCookieHelper: + def __init__(self, result): + self.result = result + + def identify(self, *arg, **kw): + return self.result + + def remember(self, *arg, **kw): + self.kw = kw + return [] + + def forget(self, *arg): + return [] + +class DummyAuthTktModule(object): + def __init__(self, timestamp=0, userid='userid', tokens=(), user_data='', + parse_raise=False, hashalg="md5"): + self.timestamp = timestamp + self.userid = userid + self.tokens = tokens + self.user_data = user_data + self.parse_raise = parse_raise + self.hashalg = hashalg + def parse_ticket(secret, value, remote_addr, hashalg): + self.secret = secret + self.value = value + self.remote_addr = remote_addr + if self.parse_raise: + raise self.BadTicket() + return self.timestamp, self.userid, self.tokens, self.user_data + self.parse_ticket = parse_ticket + + class AuthTicket(object): + def __init__(self, secret, userid, remote_addr, **kw): + self.secret = secret + self.userid = userid + self.remote_addr = remote_addr + self.kw = kw + + def cookie_value(self): + result = { + 'secret':self.secret, + 'userid':self.userid, + 'remote_addr':self.remote_addr + } + result.update(self.kw) + tokens = result.pop('tokens', None) + if tokens is not None: + tokens = '|'.join(tokens) + result['tokens'] = tokens + items = sorted(result.items()) + new_items = [] + for k, v in items: + if isinstance(v, bytes): + v = text_(v) + new_items.append((k,v)) + result = '/'.join(['%s=%s' % (k, v) for k,v in new_items ]) + return result + self.AuthTicket = AuthTicket + + class BadTicket(Exception): + pass + +class DummyResponse: + def __init__(self): + self.headerlist = [] + diff --git a/src/pyramid/tests/test_authorization.py b/src/pyramid/tests/test_authorization.py new file mode 100644 index 000000000..05cd3b4f8 --- /dev/null +++ b/src/pyramid/tests/test_authorization.py @@ -0,0 +1,259 @@ +import unittest + +from pyramid.testing import cleanUp + +class TestACLAuthorizationPolicy(unittest.TestCase): + def setUp(self): + cleanUp() + + def tearDown(self): + cleanUp() + + def _getTargetClass(self): + from pyramid.authorization import ACLAuthorizationPolicy + return ACLAuthorizationPolicy + + def _makeOne(self): + return self._getTargetClass()() + + def test_class_implements_IAuthorizationPolicy(self): + from zope.interface.verify import verifyClass + from pyramid.interfaces import IAuthorizationPolicy + verifyClass(IAuthorizationPolicy, self._getTargetClass()) + + def test_instance_implements_IAuthorizationPolicy(self): + from zope.interface.verify import verifyObject + from pyramid.interfaces import IAuthorizationPolicy + verifyObject(IAuthorizationPolicy, self._makeOne()) + + def test_permits_no_acl(self): + context = DummyContext() + policy = self._makeOne() + self.assertEqual(policy.permits(context, [], 'view'), False) + + def test_permits(self): + from pyramid.security import Deny + from pyramid.security import Allow + from pyramid.security import Everyone + from pyramid.security import Authenticated + from pyramid.security import ALL_PERMISSIONS + from pyramid.security import DENY_ALL + root = DummyContext() + community = DummyContext(__name__='community', __parent__=root) + blog = DummyContext(__name__='blog', __parent__=community) + root.__acl__ = [ + (Allow, Authenticated, VIEW), + ] + community.__acl__ = [ + (Allow, 'fred', ALL_PERMISSIONS), + (Allow, 'wilma', VIEW), + DENY_ALL, + ] + blog.__acl__ = [ + (Allow, 'barney', MEMBER_PERMS), + (Allow, 'wilma', VIEW), + ] + + policy = self._makeOne() + + result = policy.permits(blog, [Everyone, Authenticated, 'wilma'], + 'view') + self.assertEqual(result, True) + self.assertEqual(result.context, blog) + self.assertEqual(result.ace, (Allow, 'wilma', VIEW)) + self.assertEqual(result.acl, blog.__acl__) + + result = policy.permits(blog, [Everyone, Authenticated, 'wilma'], + 'delete') + self.assertEqual(result, False) + self.assertEqual(result.context, community) + self.assertEqual(result.ace, (Deny, Everyone, ALL_PERMISSIONS)) + self.assertEqual(result.acl, community.__acl__) + + result = policy.permits(blog, [Everyone, Authenticated, 'fred'], 'view') + self.assertEqual(result, True) + self.assertEqual(result.context, community) + self.assertEqual(result.ace, (Allow, 'fred', ALL_PERMISSIONS)) + result = policy.permits(blog, [Everyone, Authenticated, 'fred'], + 'doesntevenexistyet') + self.assertEqual(result, True) + self.assertEqual(result.context, community) + self.assertEqual(result.ace, (Allow, 'fred', ALL_PERMISSIONS)) + self.assertEqual(result.acl, community.__acl__) + + result = policy.permits(blog, [Everyone, Authenticated, 'barney'], + 'view') + self.assertEqual(result, True) + self.assertEqual(result.context, blog) + self.assertEqual(result.ace, (Allow, 'barney', MEMBER_PERMS)) + result = policy.permits(blog, [Everyone, Authenticated, 'barney'], + 'administer') + self.assertEqual(result, False) + self.assertEqual(result.context, community) + self.assertEqual(result.ace, (Deny, Everyone, ALL_PERMISSIONS)) + self.assertEqual(result.acl, community.__acl__) + + result = policy.permits(root, [Everyone, Authenticated, 'someguy'], + 'view') + self.assertEqual(result, True) + self.assertEqual(result.context, root) + self.assertEqual(result.ace, (Allow, Authenticated, VIEW)) + result = policy.permits(blog, + [Everyone, Authenticated, 'someguy'], 'view') + self.assertEqual(result, False) + self.assertEqual(result.context, community) + self.assertEqual(result.ace, (Deny, Everyone, ALL_PERMISSIONS)) + self.assertEqual(result.acl, community.__acl__) + + result = policy.permits(root, [Everyone], 'view') + self.assertEqual(result, False) + self.assertEqual(result.context, root) + self.assertEqual(result.ace, '') + self.assertEqual(result.acl, root.__acl__) + + context = DummyContext() + result = policy.permits(context, [Everyone], 'view') + self.assertEqual(result, False) + self.assertEqual(result.ace, '') + self.assertEqual( + result.acl, + '') + + def test_permits_string_permissions_in_acl(self): + from pyramid.security import Allow + root = DummyContext() + root.__acl__ = [ + (Allow, 'wilma', 'view_stuff'), + ] + + policy = self._makeOne() + + result = policy.permits(root, ['wilma'], 'view') + # would be True if matching against 'view_stuff' instead of against + # ['view_stuff'] + self.assertEqual(result, False) + + def test_principals_allowed_by_permission_direct(self): + from pyramid.security import Allow + from pyramid.security import DENY_ALL + context = DummyContext() + acl = [ (Allow, 'chrism', ('read', 'write')), + DENY_ALL, + (Allow, 'other', 'read') ] + context.__acl__ = acl + policy = self._makeOne() + result = sorted( + policy.principals_allowed_by_permission(context, 'read')) + self.assertEqual(result, ['chrism']) + + def test_principals_allowed_by_permission_callable_acl(self): + from pyramid.security import Allow + from pyramid.security import DENY_ALL + context = DummyContext() + acl = lambda: [ (Allow, 'chrism', ('read', 'write')), + DENY_ALL, + (Allow, 'other', 'read') ] + context.__acl__ = acl + policy = self._makeOne() + result = sorted( + policy.principals_allowed_by_permission(context, 'read')) + self.assertEqual(result, ['chrism']) + + def test_principals_allowed_by_permission_string_permission(self): + from pyramid.security import Allow + context = DummyContext() + acl = [ (Allow, 'chrism', 'read_it')] + context.__acl__ = acl + policy = self._makeOne() + result = policy.principals_allowed_by_permission(context, 'read') + # would be ['chrism'] if 'read' were compared against 'read_it' instead + # of against ['read_it'] + self.assertEqual(list(result), []) + + def test_principals_allowed_by_permission(self): + from pyramid.security import Allow + from pyramid.security import Deny + from pyramid.security import DENY_ALL + from pyramid.security import ALL_PERMISSIONS + root = DummyContext(__name__='', __parent__=None) + community = DummyContext(__name__='community', __parent__=root) + blog = DummyContext(__name__='blog', __parent__=community) + root.__acl__ = [ (Allow, 'chrism', ('read', 'write')), + (Allow, 'other', ('read',)), + (Allow, 'jim', ALL_PERMISSIONS)] + community.__acl__ = [ (Deny, 'flooz', 'read'), + (Allow, 'flooz', 'read'), + (Allow, 'mork', 'read'), + (Deny, 'jim', 'read'), + (Allow, 'someguy', 'manage')] + blog.__acl__ = [ (Allow, 'fred', 'read'), + DENY_ALL] + + policy = self._makeOne() + + result = sorted(policy.principals_allowed_by_permission(blog, 'read')) + self.assertEqual(result, ['fred']) + result = sorted(policy.principals_allowed_by_permission(community, + 'read')) + self.assertEqual(result, ['chrism', 'mork', 'other']) + result = sorted(policy.principals_allowed_by_permission(community, + 'read')) + result = sorted(policy.principals_allowed_by_permission(root, 'read')) + self.assertEqual(result, ['chrism', 'jim', 'other']) + + def test_principals_allowed_by_permission_no_acls(self): + context = DummyContext() + policy = self._makeOne() + result = sorted(policy.principals_allowed_by_permission(context,'read')) + self.assertEqual(result, []) + + def test_principals_allowed_by_permission_deny_not_permission_in_acl(self): + from pyramid.security import Deny + from pyramid.security import Everyone + context = DummyContext() + acl = [ (Deny, Everyone, 'write') ] + context.__acl__ = acl + policy = self._makeOne() + result = sorted( + policy.principals_allowed_by_permission(context, 'read')) + self.assertEqual(result, []) + + def test_principals_allowed_by_permission_deny_permission_in_acl(self): + from pyramid.security import Deny + from pyramid.security import Everyone + context = DummyContext() + acl = [ (Deny, Everyone, 'read') ] + context.__acl__ = acl + policy = self._makeOne() + result = sorted( + policy.principals_allowed_by_permission(context, 'read')) + self.assertEqual(result, []) + + def test_callable_acl(self): + from pyramid.security import Allow + context = DummyContext() + fn = lambda self: [(Allow, 'bob', 'read')] + context.__acl__ = fn.__get__(context, context.__class__) + policy = self._makeOne() + result = policy.permits(context, ['bob'], 'read') + self.assertTrue(result) + + +class DummyContext: + def __init__(self, *arg, **kw): + self.__dict__.update(kw) + + +VIEW = 'view' +EDIT = 'edit' +CREATE = 'create' +DELETE = 'delete' +MODERATE = 'moderate' +ADMINISTER = 'administer' +COMMENT = 'comment' + +GUEST_PERMS = (VIEW, COMMENT) +MEMBER_PERMS = GUEST_PERMS + (EDIT, CREATE, DELETE) +MODERATOR_PERMS = MEMBER_PERMS + (MODERATE,) +ADMINISTRATOR_PERMS = MODERATOR_PERMS + (ADMINISTER,) + diff --git a/src/pyramid/tests/test_compat.py b/src/pyramid/tests/test_compat.py new file mode 100644 index 000000000..23ccce82e --- /dev/null +++ b/src/pyramid/tests/test_compat.py @@ -0,0 +1,26 @@ +import unittest +from pyramid.compat import is_unbound_method + +class TestUnboundMethods(unittest.TestCase): + def test_old_style_bound(self): + self.assertFalse(is_unbound_method(OldStyle().run)) + + def test_new_style_bound(self): + self.assertFalse(is_unbound_method(NewStyle().run)) + + def test_old_style_unbound(self): + self.assertTrue(is_unbound_method(OldStyle.run)) + + def test_new_style_unbound(self): + self.assertTrue(is_unbound_method(NewStyle.run)) + + def test_normal_func_unbound(self): + def func(): return 'OK' + + self.assertFalse(is_unbound_method(func)) + +class OldStyle: + def run(self): return 'OK' + +class NewStyle(object): + def run(self): return 'OK' diff --git a/src/pyramid/tests/test_config/__init__.py b/src/pyramid/tests/test_config/__init__.py new file mode 100644 index 000000000..81d9f4965 --- /dev/null +++ b/src/pyramid/tests/test_config/__init__.py @@ -0,0 +1,53 @@ +# package + +from zope.interface import implementer +from zope.interface import Interface + +class IFactory(Interface): + pass + +def dummy_tween_factory(handler, registry): pass + +def dummy_tween_factory2(handler, registry): pass + +def dummy_include(config): + config.registry.included = True + config.action('discrim', None, config.package) + +def dummy_include2(config): + config.registry.also_included = True + config.action('discrim', None, config.package) + +includeme = dummy_include + +class DummyContext: + pass + +@implementer(IFactory) +class DummyFactory(object): + def __call__(self): + """ """ + +def dummyfactory(request): + """ """ + +class IDummy(Interface): + pass + +def dummy_view(request): + return 'OK' + +def dummy_extend(config, discrim): + config.action(discrim, None, config.package) + +def dummy_extend2(config, discrim): + config.action(discrim, None, config.registry) + +from functools import partial +dummy_partial = partial(dummy_extend, discrim='partial') + +class DummyCallable(object): + def __call__(self, config, discrim): + config.action(discrim, None, config.package) +dummy_callable = DummyCallable() + diff --git a/src/pyramid/tests/test_config/files/assets/dummy.txt b/src/pyramid/tests/test_config/files/assets/dummy.txt new file mode 100644 index 000000000..18832d351 --- /dev/null +++ b/src/pyramid/tests/test_config/files/assets/dummy.txt @@ -0,0 +1 @@ +Hello. diff --git a/src/pyramid/tests/test_config/files/minimal.txt b/src/pyramid/tests/test_config/files/minimal.txt new file mode 100644 index 000000000..19fe66dfa --- /dev/null +++ b/src/pyramid/tests/test_config/files/minimal.txt @@ -0,0 +1 @@ +
diff --git a/src/pyramid/tests/test_config/path/scanerror/__init__.py b/src/pyramid/tests/test_config/path/scanerror/__init__.py new file mode 100644 index 000000000..86770ad89 --- /dev/null +++ b/src/pyramid/tests/test_config/path/scanerror/__init__.py @@ -0,0 +1,3 @@ +# scan error package + + diff --git a/src/pyramid/tests/test_config/path/scanerror/will_raise_error.py b/src/pyramid/tests/test_config/path/scanerror/will_raise_error.py new file mode 100644 index 000000000..9098ff1fe --- /dev/null +++ b/src/pyramid/tests/test_config/path/scanerror/will_raise_error.py @@ -0,0 +1 @@ +import wont.exist diff --git a/src/pyramid/tests/test_config/pkgs/__init__.py b/src/pyramid/tests/test_config/pkgs/__init__.py new file mode 100644 index 000000000..ed88d78b4 --- /dev/null +++ b/src/pyramid/tests/test_config/pkgs/__init__.py @@ -0,0 +1,2 @@ +# package + diff --git a/src/pyramid/tests/test_config/pkgs/asset/__init__.py b/src/pyramid/tests/test_config/pkgs/asset/__init__.py new file mode 100644 index 000000000..db5619fbc --- /dev/null +++ b/src/pyramid/tests/test_config/pkgs/asset/__init__.py @@ -0,0 +1,3 @@ +# package + + diff --git a/src/pyramid/tests/test_config/pkgs/asset/subpackage/__init__.py b/src/pyramid/tests/test_config/pkgs/asset/subpackage/__init__.py new file mode 100644 index 000000000..d3173e636 --- /dev/null +++ b/src/pyramid/tests/test_config/pkgs/asset/subpackage/__init__.py @@ -0,0 +1 @@ +#package diff --git a/src/pyramid/tests/test_config/pkgs/asset/subpackage/templates/bar.pt b/src/pyramid/tests/test_config/pkgs/asset/subpackage/templates/bar.pt new file mode 100644 index 000000000..e69de29bb diff --git a/src/pyramid/tests/test_config/pkgs/scanextrakw/__init__.py b/src/pyramid/tests/test_config/pkgs/scanextrakw/__init__.py new file mode 100644 index 000000000..ce5e07238 --- /dev/null +++ b/src/pyramid/tests/test_config/pkgs/scanextrakw/__init__.py @@ -0,0 +1,14 @@ +import venusian + +def foo(wrapped): + def bar(scanner, name, wrapped): + scanner.config.a = scanner.a + venusian.attach(wrapped, bar) + return wrapped + +@foo +def hello(): + pass + +hello() # appease coverage + diff --git a/src/pyramid/tests/test_config/pkgs/scannable/__init__.py b/src/pyramid/tests/test_config/pkgs/scannable/__init__.py new file mode 100644 index 000000000..562413a41 --- /dev/null +++ b/src/pyramid/tests/test_config/pkgs/scannable/__init__.py @@ -0,0 +1,96 @@ +from pyramid.view import view_config +from pyramid.renderers import null_renderer + +@view_config(renderer=null_renderer) +def grokked(context, request): + return 'grokked' + +@view_config(request_method='POST', renderer=null_renderer) +def grokked_post(context, request): + return 'grokked_post' + +@view_config(name='stacked2', renderer=null_renderer) +@view_config(name='stacked1', renderer=null_renderer) +def stacked(context, request): + return 'stacked' + +class stacked_class(object): + def __init__(self, context, request): + self.context = context + self.request = request + + def __call__(self): + return 'stacked_class' + +stacked_class = view_config(name='stacked_class1', + renderer=null_renderer)(stacked_class) +stacked_class = view_config(name='stacked_class2', + renderer=null_renderer)(stacked_class) + +class oldstyle_grokked_class: + def __init__(self, context, request): + self.context = context + self.request = request + + def __call__(self): + return 'oldstyle_grokked_class' + +oldstyle_grokked_class = view_config(name='oldstyle_grokked_class', + renderer=null_renderer)( + oldstyle_grokked_class) + +class grokked_class(object): + def __init__(self, context, request): + self.context = context + self.request = request + + def __call__(self): + return 'grokked_class' + +grokked_class = view_config(name='grokked_class', + renderer=null_renderer)(grokked_class) + +class Foo(object): + def __call__(self, context, request): + return 'grokked_instance' + +grokked_instance = Foo() +grokked_instance = view_config(name='grokked_instance', + renderer=null_renderer)(grokked_instance) + +class Base(object): + @view_config(name='basemethod', renderer=null_renderer) + def basemethod(self): + """ """ + +class MethodViews(Base): + def __init__(self, context, request): + self.context = context + self.request = request + + @view_config(name='method1', renderer=null_renderer) + def method1(self): + return 'method1' + + @view_config(name='method2', renderer=null_renderer) + def method2(self): + return 'method2' + + @view_config(name='stacked_method2', renderer=null_renderer) + @view_config(name='stacked_method1', renderer=null_renderer) + def stacked(self): + return 'stacked_method' + +# ungrokkable + +A = 1 +B = {} + +def stuff(): + """ """ + +class Whatever(object): + pass + +class Whatever2: + pass diff --git a/src/pyramid/tests/test_config/pkgs/scannable/another.py b/src/pyramid/tests/test_config/pkgs/scannable/another.py new file mode 100644 index 000000000..529821b5c --- /dev/null +++ b/src/pyramid/tests/test_config/pkgs/scannable/another.py @@ -0,0 +1,69 @@ +from pyramid.view import view_config +from pyramid.renderers import null_renderer + +@view_config(name='another', renderer=null_renderer) +def grokked(context, request): + return 'another_grokked' + +@view_config(request_method='POST', name='another', renderer=null_renderer) +def grokked_post(context, request): + return 'another_grokked_post' + +@view_config(name='another_stacked2', renderer=null_renderer) +@view_config(name='another_stacked1', renderer=null_renderer) +def stacked(context, request): + return 'another_stacked' + +class stacked_class(object): + def __init__(self, context, request): + self.context = context + self.request = request + + def __call__(self): + return 'another_stacked_class' + +stacked_class = view_config(name='another_stacked_class1', + renderer=null_renderer)(stacked_class) +stacked_class = view_config(name='another_stacked_class2', + renderer=null_renderer)(stacked_class) + +class oldstyle_grokked_class: + def __init__(self, context, request): + self.context = context + self.request = request + + def __call__(self): + return 'another_oldstyle_grokked_class' + +oldstyle_grokked_class = view_config(name='another_oldstyle_grokked_class', + renderer=null_renderer)( + oldstyle_grokked_class) + +class grokked_class(object): + def __init__(self, context, request): + self.context = context + self.request = request + + def __call__(self): + return 'another_grokked_class' + +grokked_class = view_config(name='another_grokked_class', + renderer=null_renderer)(grokked_class) + +class Foo(object): + def __call__(self, context, request): + return 'another_grokked_instance' + +grokked_instance = Foo() +grokked_instance = view_config(name='another_grokked_instance', + renderer=null_renderer)( + grokked_instance) + +# ungrokkable + +A = 1 +B = {} + +def stuff(): + """ """ + diff --git a/src/pyramid/tests/test_config/pkgs/scannable/pod/notinit.py b/src/pyramid/tests/test_config/pkgs/scannable/pod/notinit.py new file mode 100644 index 000000000..91dcd161b --- /dev/null +++ b/src/pyramid/tests/test_config/pkgs/scannable/pod/notinit.py @@ -0,0 +1,6 @@ +from pyramid.view import view_config +from pyramid.renderers import null_renderer + +@view_config(name='pod_notinit', renderer=null_renderer) +def subpackage_notinit(context, request): + return 'pod_notinit' diff --git a/src/pyramid/tests/test_config/pkgs/scannable/subpackage/__init__.py b/src/pyramid/tests/test_config/pkgs/scannable/subpackage/__init__.py new file mode 100644 index 000000000..9e0ddacbd --- /dev/null +++ b/src/pyramid/tests/test_config/pkgs/scannable/subpackage/__init__.py @@ -0,0 +1,6 @@ +from pyramid.view import view_config +from pyramid.renderers import null_renderer + +@view_config(name='subpackage_init', renderer=null_renderer) +def subpackage_init(context, request): + return 'subpackage_init' diff --git a/src/pyramid/tests/test_config/pkgs/scannable/subpackage/notinit.py b/src/pyramid/tests/test_config/pkgs/scannable/subpackage/notinit.py new file mode 100644 index 000000000..f7edd0c68 --- /dev/null +++ b/src/pyramid/tests/test_config/pkgs/scannable/subpackage/notinit.py @@ -0,0 +1,6 @@ +from pyramid.view import view_config +from pyramid.renderers import null_renderer + +@view_config(name='subpackage_notinit', renderer=null_renderer) +def subpackage_notinit(context, request): + return 'subpackage_notinit' diff --git a/src/pyramid/tests/test_config/pkgs/scannable/subpackage/subsubpackage/__init__.py b/src/pyramid/tests/test_config/pkgs/scannable/subpackage/subsubpackage/__init__.py new file mode 100644 index 000000000..fdda0dffe --- /dev/null +++ b/src/pyramid/tests/test_config/pkgs/scannable/subpackage/subsubpackage/__init__.py @@ -0,0 +1,6 @@ +from pyramid.view import view_config +from pyramid.renderers import null_renderer + +@view_config(name='subsubpackage_init', renderer=null_renderer) +def subpackage_init(context, request): + return 'subsubpackage_init' diff --git a/src/pyramid/tests/test_config/pkgs/selfscan/__init__.py b/src/pyramid/tests/test_config/pkgs/selfscan/__init__.py new file mode 100644 index 000000000..779ea3eed --- /dev/null +++ b/src/pyramid/tests/test_config/pkgs/selfscan/__init__.py @@ -0,0 +1,11 @@ +from pyramid.view import view_config + +@view_config(renderer='string') +def abc(request): + return 'root' + +def main(): + from pyramid.config import Configurator + c = Configurator() + c.scan() + return c diff --git a/src/pyramid/tests/test_config/pkgs/selfscan/another.py b/src/pyramid/tests/test_config/pkgs/selfscan/another.py new file mode 100644 index 000000000..a30ad3297 --- /dev/null +++ b/src/pyramid/tests/test_config/pkgs/selfscan/another.py @@ -0,0 +1,6 @@ +from pyramid.view import view_config + +@view_config(name='two', renderer='string') +def two(request): + return 'two' + diff --git a/src/pyramid/tests/test_config/test_adapters.py b/src/pyramid/tests/test_config/test_adapters.py new file mode 100644 index 000000000..ab5d6ef61 --- /dev/null +++ b/src/pyramid/tests/test_config/test_adapters.py @@ -0,0 +1,365 @@ +import unittest + +from pyramid.compat import PY2 +from pyramid.tests.test_config import IDummy + +class AdaptersConfiguratorMixinTests(unittest.TestCase): + def _makeOne(self, *arg, **kw): + from pyramid.config import Configurator + config = Configurator(*arg, **kw) + return config + + def test_add_subscriber_defaults(self): + from zope.interface import implementer + from zope.interface import Interface + class IEvent(Interface): + pass + @implementer(IEvent) + class Event: + pass + L = [] + def subscriber(event): + L.append(event) + config = self._makeOne(autocommit=True) + config.add_subscriber(subscriber) + event = Event() + config.registry.notify(event) + self.assertEqual(len(L), 1) + self.assertEqual(L[0], event) + config.registry.notify(object()) + self.assertEqual(len(L), 2) + + def test_add_subscriber_iface_specified(self): + from zope.interface import implementer + from zope.interface import Interface + class IEvent(Interface): + pass + @implementer(IEvent) + class Event: + pass + L = [] + def subscriber(event): + L.append(event) + config = self._makeOne(autocommit=True) + config.add_subscriber(subscriber, IEvent) + event = Event() + config.registry.notify(event) + self.assertEqual(len(L), 1) + self.assertEqual(L[0], event) + config.registry.notify(object()) + self.assertEqual(len(L), 1) + + def test_add_subscriber_dottednames(self): + import pyramid.tests.test_config + from pyramid.interfaces import INewRequest + config = self._makeOne(autocommit=True) + config.add_subscriber('pyramid.tests.test_config', + 'pyramid.interfaces.INewRequest') + handlers = list(config.registry.registeredHandlers()) + self.assertEqual(len(handlers), 1) + handler = handlers[0] + self.assertEqual(handler.handler, pyramid.tests.test_config) + self.assertEqual(handler.required, (INewRequest,)) + + def test_add_object_event_subscriber(self): + from zope.interface import implementer + from zope.interface import Interface + class IEvent(Interface): + pass + @implementer(IEvent) + class Event: + object = 'foo' + event = Event() + L = [] + def subscriber(object, event): + L.append(event) + config = self._makeOne(autocommit=True) + config.add_subscriber(subscriber, (Interface, IEvent)) + config.registry.subscribers((event.object, event), None) + self.assertEqual(len(L), 1) + self.assertEqual(L[0], event) + config.registry.subscribers((event.object, IDummy), None) + self.assertEqual(len(L), 1) + + def test_add_subscriber_with_specific_type_and_predicates_True(self): + from zope.interface import implementer + from zope.interface import Interface + class IEvent(Interface): + pass + @implementer(IEvent) + class Event: + pass + L = [] + def subscriber(event): + L.append(event) + config = self._makeOne(autocommit=True) + predlist = config.get_predlist('subscriber') + jam_predicate = predicate_maker('jam') + jim_predicate = predicate_maker('jim') + predlist.add('jam', jam_predicate) + predlist.add('jim', jim_predicate) + config.add_subscriber(subscriber, IEvent, jam=True, jim=True) + event = Event() + event.jam = True + event.jim = True + config.registry.notify(event) + self.assertEqual(len(L), 1) + self.assertEqual(L[0], event) + config.registry.notify(object()) + self.assertEqual(len(L), 1) + + def test_add_subscriber_with_default_type_predicates_True(self): + from zope.interface import implementer + from zope.interface import Interface + class IEvent(Interface): + pass + @implementer(IEvent) + class Event: + pass + L = [] + def subscriber(event): + L.append(event) + config = self._makeOne(autocommit=True) + predlist = config.get_predlist('subscriber') + jam_predicate = predicate_maker('jam') + jim_predicate = predicate_maker('jim') + predlist.add('jam', jam_predicate) + predlist.add('jim', jim_predicate) + config.add_subscriber(subscriber, jam=True, jim=True) + event = Event() + event.jam = True + event.jim = True + config.registry.notify(event) + self.assertEqual(len(L), 1) + self.assertEqual(L[0], event) + config.registry.notify(object()) + self.assertEqual(len(L), 1) + + def test_add_subscriber_with_specific_type_and_predicates_False(self): + from zope.interface import implementer + from zope.interface import Interface + class IEvent(Interface): + pass + @implementer(IEvent) + class Event: + pass + L = [] + def subscriber(event): L.append(event) + config = self._makeOne(autocommit=True) + predlist = config.get_predlist('subscriber') + jam_predicate = predicate_maker('jam') + jim_predicate = predicate_maker('jim') + predlist.add('jam', jam_predicate) + predlist.add('jim', jim_predicate) + config.add_subscriber(subscriber, IEvent, jam=True, jim=True) + event = Event() + event.jam = True + event.jim = False + config.registry.notify(event) + self.assertEqual(len(L), 0) + + def test_add_subscriber_with_default_type_predicates_False(self): + from zope.interface import implementer + from zope.interface import Interface + class IEvent(Interface): + pass + @implementer(IEvent) + class Event: + pass + L = [] + def subscriber(event): L.append(event) + config = self._makeOne(autocommit=True) + predlist = config.get_predlist('subscriber') + jam_predicate = predicate_maker('jam') + jim_predicate = predicate_maker('jim') + predlist.add('jam', jam_predicate) + predlist.add('jim', jim_predicate) + config.add_subscriber(subscriber, jam=True, jim=True) + event = Event() + event.jam = False + event.jim = True + config.registry.notify(event) + self.assertEqual(len(L), 0) + + def test_add_subscriber_predicate(self): + config = self._makeOne() + L = [] + def add_predicate(type, name, factory, weighs_less_than=None, + weighs_more_than=None): + self.assertEqual(type, 'subscriber') + self.assertEqual(name, 'name') + self.assertEqual(factory, 'factory') + self.assertEqual(weighs_more_than, 1) + self.assertEqual(weighs_less_than, 2) + L.append(1) + config._add_predicate = add_predicate + config.add_subscriber_predicate('name', 'factory', 1, 2) + self.assertTrue(L) + + def test_add_response_adapter(self): + from pyramid.interfaces import IResponse + config = self._makeOne(autocommit=True) + class Adapter(object): + def __init__(self, other): + self.other = other + config.add_response_adapter(Adapter, str) + result = config.registry.queryAdapter('foo', IResponse) + self.assertTrue(result.other, 'foo') + + def test_add_response_adapter_self(self): + from pyramid.interfaces import IResponse + config = self._makeOne(autocommit=True) + class Adapter(object): + pass + config.add_response_adapter(None, Adapter) + adapter = Adapter() + result = config.registry.queryAdapter(adapter, IResponse) + self.assertTrue(result is adapter) + + def test_add_response_adapter_dottednames(self): + from pyramid.interfaces import IResponse + config = self._makeOne(autocommit=True) + if PY2: + str_name = '__builtin__.str' + else: + str_name = 'builtins.str' + config.add_response_adapter('pyramid.response.Response', str_name) + result = config.registry.queryAdapter('foo', IResponse) + self.assertTrue(result.body, b'foo') + + def test_add_traverser_dotted_names(self): + from pyramid.interfaces import ITraverser + config = self._makeOne(autocommit=True) + config.add_traverser( + 'pyramid.tests.test_config.test_adapters.DummyTraverser', + 'pyramid.tests.test_config.test_adapters.DummyIface') + iface = DummyIface() + traverser = config.registry.getAdapter(iface, ITraverser) + self.assertEqual(traverser.__class__, DummyTraverser) + self.assertEqual(traverser.root, iface) + + def test_add_traverser_default_iface_means_Interface(self): + from pyramid.interfaces import ITraverser + config = self._makeOne(autocommit=True) + config.add_traverser(DummyTraverser) + traverser = config.registry.getAdapter(None, ITraverser) + self.assertEqual(traverser.__class__, DummyTraverser) + + def test_add_traverser_nondefault_iface(self): + from pyramid.interfaces import ITraverser + config = self._makeOne(autocommit=True) + config.add_traverser(DummyTraverser, DummyIface) + iface = DummyIface() + traverser = config.registry.getAdapter(iface, ITraverser) + self.assertEqual(traverser.__class__, DummyTraverser) + self.assertEqual(traverser.root, iface) + + def test_add_traverser_introspectables(self): + config = self._makeOne() + config.add_traverser(DummyTraverser, DummyIface) + actions = config.action_state.actions + self.assertEqual(len(actions), 1) + intrs = actions[0]['introspectables'] + self.assertEqual(len(intrs), 1) + intr = intrs[0] + self.assertEqual(intr.type_name, 'traverser') + self.assertEqual(intr.discriminator, ('traverser', DummyIface)) + self.assertEqual(intr.category_name, 'traversers') + self.assertEqual(intr.title, 'traverser for %r' % DummyIface) + self.assertEqual(intr['adapter'], DummyTraverser) + self.assertEqual(intr['iface'], DummyIface) + + def test_add_resource_url_adapter_dotted_names(self): + from pyramid.interfaces import IResourceURL + config = self._makeOne(autocommit=True) + config.add_resource_url_adapter( + 'pyramid.tests.test_config.test_adapters.DummyResourceURL', + 'pyramid.tests.test_config.test_adapters.DummyIface', + ) + iface = DummyIface() + adapter = config.registry.getMultiAdapter((iface, iface), + IResourceURL) + self.assertEqual(adapter.__class__, DummyResourceURL) + self.assertEqual(adapter.resource, iface) + self.assertEqual(adapter.request, iface) + + def test_add_resource_url_default_resource_iface_means_Interface(self): + from pyramid.interfaces import IResourceURL + config = self._makeOne(autocommit=True) + config.add_resource_url_adapter(DummyResourceURL) + iface = DummyIface() + adapter = config.registry.getMultiAdapter((iface, iface), + IResourceURL) + self.assertEqual(adapter.__class__, DummyResourceURL) + self.assertEqual(adapter.resource, iface) + self.assertEqual(adapter.request, iface) + + def test_add_resource_url_nodefault_resource_iface(self): + from zope.interface import Interface + from pyramid.interfaces import IResourceURL + config = self._makeOne(autocommit=True) + config.add_resource_url_adapter(DummyResourceURL, DummyIface) + iface = DummyIface() + adapter = config.registry.getMultiAdapter((iface, iface), + IResourceURL) + self.assertEqual(adapter.__class__, DummyResourceURL) + self.assertEqual(adapter.resource, iface) + self.assertEqual(adapter.request, iface) + bad_result = config.registry.queryMultiAdapter( + (Interface, Interface), + IResourceURL, + ) + self.assertEqual(bad_result, None) + + def test_add_resource_url_adapter_introspectables(self): + config = self._makeOne() + config.add_resource_url_adapter(DummyResourceURL, DummyIface) + actions = config.action_state.actions + self.assertEqual(len(actions), 1) + intrs = actions[0]['introspectables'] + self.assertEqual(len(intrs), 1) + intr = intrs[0] + self.assertEqual(intr.type_name, 'resource url adapter') + self.assertEqual(intr.discriminator, + ('resource url adapter', DummyIface)) + self.assertEqual(intr.category_name, 'resource url adapters') + self.assertEqual( + intr.title, + "resource url adapter for resource iface " + "" + ) + self.assertEqual(intr['adapter'], DummyResourceURL) + self.assertEqual(intr['resource_iface'], DummyIface) + +class Test_eventonly(unittest.TestCase): + def _callFUT(self, callee): + from pyramid.config.adapters import eventonly + return eventonly(callee) + + def test_defaults(self): + def acallable(event, a=1, b=2): pass + self.assertTrue(self._callFUT(acallable)) + +class DummyTraverser(object): + def __init__(self, root): + self.root = root + +class DummyIface(object): + pass + +class DummyResourceURL(object): + def __init__(self, resource, request): + self.resource = resource + self.request = request + +def predicate_maker(name): + class Predicate(object): + def __init__(self, val, config): + self.val = val + def phash(self): + return 'phash' + text = phash + def __call__(self, event): + return getattr(event, name, None) == self.val + return Predicate + diff --git a/src/pyramid/tests/test_config/test_assets.py b/src/pyramid/tests/test_config/test_assets.py new file mode 100644 index 000000000..842c73da6 --- /dev/null +++ b/src/pyramid/tests/test_config/test_assets.py @@ -0,0 +1,945 @@ +import os.path +import unittest +from pyramid.testing import cleanUp + +# we use this folder +here = os.path.dirname(os.path.abspath(__file__)) + +class TestAssetsConfiguratorMixin(unittest.TestCase): + def _makeOne(self, *arg, **kw): + from pyramid.config import Configurator + config = Configurator(*arg, **kw) + return config + + def test_override_asset_samename(self): + from pyramid.exceptions import ConfigurationError + config = self._makeOne() + self.assertRaises(ConfigurationError, config.override_asset, 'a', 'a') + + def test_override_asset_directory_with_file(self): + from pyramid.exceptions import ConfigurationError + config = self._makeOne() + self.assertRaises(ConfigurationError, config.override_asset, + 'a:foo/', + 'pyramid.tests.test_config.pkgs.asset:foo.pt') + + def test_override_asset_file_with_directory(self): + from pyramid.exceptions import ConfigurationError + config = self._makeOne() + self.assertRaises(ConfigurationError, config.override_asset, + 'a:foo.pt', + 'pyramid.tests.test_config.pkgs.asset:templates/') + + def test_override_asset_file_with_package(self): + from pyramid.exceptions import ConfigurationError + config = self._makeOne() + self.assertRaises(ConfigurationError, config.override_asset, + 'a:foo.pt', + 'pyramid.tests.test_config.pkgs.asset') + + def test_override_asset_file_with_file(self): + from pyramid.config.assets import PackageAssetSource + config = self._makeOne(autocommit=True) + override = DummyUnderOverride() + config.override_asset( + 'pyramid.tests.test_config.pkgs.asset:templates/foo.pt', + 'pyramid.tests.test_config.pkgs.asset.subpackage:templates/bar.pt', + _override=override) + from pyramid.tests.test_config.pkgs import asset + from pyramid.tests.test_config.pkgs.asset import subpackage + self.assertEqual(override.package, asset) + self.assertEqual(override.path, 'templates/foo.pt') + source = override.source + self.assertTrue(isinstance(source, PackageAssetSource)) + self.assertEqual(source.package, subpackage) + self.assertEqual(source.prefix, 'templates/bar.pt') + + resource_name = '' + expected = os.path.join(here, 'pkgs', 'asset', + 'subpackage', 'templates', 'bar.pt') + self.assertEqual(override.source.get_filename(resource_name), + expected) + + def test_override_asset_package_with_package(self): + from pyramid.config.assets import PackageAssetSource + config = self._makeOne(autocommit=True) + override = DummyUnderOverride() + config.override_asset( + 'pyramid.tests.test_config.pkgs.asset', + 'pyramid.tests.test_config.pkgs.asset.subpackage', + _override=override) + from pyramid.tests.test_config.pkgs import asset + from pyramid.tests.test_config.pkgs.asset import subpackage + self.assertEqual(override.package, asset) + self.assertEqual(override.path, '') + source = override.source + self.assertTrue(isinstance(source, PackageAssetSource)) + self.assertEqual(source.package, subpackage) + self.assertEqual(source.prefix, '') + + resource_name = 'templates/bar.pt' + expected = os.path.join(here, 'pkgs', 'asset', + 'subpackage', 'templates', 'bar.pt') + self.assertEqual(override.source.get_filename(resource_name), + expected) + + def test_override_asset_directory_with_directory(self): + from pyramid.config.assets import PackageAssetSource + config = self._makeOne(autocommit=True) + override = DummyUnderOverride() + config.override_asset( + 'pyramid.tests.test_config.pkgs.asset:templates/', + 'pyramid.tests.test_config.pkgs.asset.subpackage:templates/', + _override=override) + from pyramid.tests.test_config.pkgs import asset + from pyramid.tests.test_config.pkgs.asset import subpackage + self.assertEqual(override.package, asset) + self.assertEqual(override.path, 'templates/') + source = override.source + self.assertTrue(isinstance(source, PackageAssetSource)) + self.assertEqual(source.package, subpackage) + self.assertEqual(source.prefix, 'templates/') + + resource_name = 'bar.pt' + expected = os.path.join(here, 'pkgs', 'asset', + 'subpackage', 'templates', 'bar.pt') + self.assertEqual(override.source.get_filename(resource_name), + expected) + + def test_override_asset_directory_with_package(self): + from pyramid.config.assets import PackageAssetSource + config = self._makeOne(autocommit=True) + override = DummyUnderOverride() + config.override_asset( + 'pyramid.tests.test_config.pkgs.asset:templates/', + 'pyramid.tests.test_config.pkgs.asset.subpackage', + _override=override) + from pyramid.tests.test_config.pkgs import asset + from pyramid.tests.test_config.pkgs.asset import subpackage + self.assertEqual(override.package, asset) + self.assertEqual(override.path, 'templates/') + source = override.source + self.assertTrue(isinstance(source, PackageAssetSource)) + self.assertEqual(source.package, subpackage) + self.assertEqual(source.prefix, '') + + resource_name = 'templates/bar.pt' + expected = os.path.join(here, 'pkgs', 'asset', + 'subpackage', 'templates', 'bar.pt') + self.assertEqual(override.source.get_filename(resource_name), + expected) + + def test_override_asset_package_with_directory(self): + from pyramid.config.assets import PackageAssetSource + config = self._makeOne(autocommit=True) + override = DummyUnderOverride() + config.override_asset( + 'pyramid.tests.test_config.pkgs.asset', + 'pyramid.tests.test_config.pkgs.asset.subpackage:templates/', + _override=override) + from pyramid.tests.test_config.pkgs import asset + from pyramid.tests.test_config.pkgs.asset import subpackage + self.assertEqual(override.package, asset) + self.assertEqual(override.path, '') + source = override.source + self.assertTrue(isinstance(source, PackageAssetSource)) + self.assertEqual(source.package, subpackage) + self.assertEqual(source.prefix, 'templates/') + + resource_name = 'bar.pt' + expected = os.path.join(here, 'pkgs', 'asset', + 'subpackage', 'templates', 'bar.pt') + self.assertEqual(override.source.get_filename(resource_name), + expected) + + def test_override_asset_directory_with_absfile(self): + from pyramid.exceptions import ConfigurationError + config = self._makeOne() + self.assertRaises(ConfigurationError, config.override_asset, + 'a:foo/', + os.path.join(here, 'pkgs', 'asset', 'foo.pt')) + + def test_override_asset_file_with_absdirectory(self): + from pyramid.exceptions import ConfigurationError + config = self._makeOne() + abspath = os.path.join(here, 'pkgs', 'asset', 'subpackage', 'templates') + self.assertRaises(ConfigurationError, config.override_asset, + 'a:foo.pt', + abspath) + + def test_override_asset_file_with_missing_abspath(self): + from pyramid.exceptions import ConfigurationError + config = self._makeOne() + self.assertRaises(ConfigurationError, config.override_asset, + 'a:foo.pt', + os.path.join(here, 'wont_exist')) + + def test_override_asset_file_with_absfile(self): + from pyramid.config.assets import FSAssetSource + config = self._makeOne(autocommit=True) + override = DummyUnderOverride() + abspath = os.path.join(here, 'pkgs', 'asset', 'subpackage', + 'templates', 'bar.pt') + config.override_asset( + 'pyramid.tests.test_config.pkgs.asset:templates/foo.pt', + abspath, + _override=override) + from pyramid.tests.test_config.pkgs import asset + self.assertEqual(override.package, asset) + self.assertEqual(override.path, 'templates/foo.pt') + source = override.source + self.assertTrue(isinstance(source, FSAssetSource)) + self.assertEqual(source.prefix, abspath) + + resource_name = '' + expected = os.path.join(here, 'pkgs', 'asset', + 'subpackage', 'templates', 'bar.pt') + self.assertEqual(override.source.get_filename(resource_name), + expected) + + def test_override_asset_directory_with_absdirectory(self): + from pyramid.config.assets import FSAssetSource + config = self._makeOne(autocommit=True) + override = DummyUnderOverride() + abspath = os.path.join(here, 'pkgs', 'asset', 'subpackage', 'templates') + config.override_asset( + 'pyramid.tests.test_config.pkgs.asset:templates/', + abspath, + _override=override) + from pyramid.tests.test_config.pkgs import asset + self.assertEqual(override.package, asset) + self.assertEqual(override.path, 'templates/') + source = override.source + self.assertTrue(isinstance(source, FSAssetSource)) + self.assertEqual(source.prefix, abspath) + + resource_name = 'bar.pt' + expected = os.path.join(here, 'pkgs', 'asset', + 'subpackage', 'templates', 'bar.pt') + self.assertEqual(override.source.get_filename(resource_name), + expected) + + def test_override_asset_package_with_absdirectory(self): + from pyramid.config.assets import FSAssetSource + config = self._makeOne(autocommit=True) + override = DummyUnderOverride() + abspath = os.path.join(here, 'pkgs', 'asset', 'subpackage', 'templates') + config.override_asset( + 'pyramid.tests.test_config.pkgs.asset', + abspath, + _override=override) + from pyramid.tests.test_config.pkgs import asset + self.assertEqual(override.package, asset) + self.assertEqual(override.path, '') + source = override.source + self.assertTrue(isinstance(source, FSAssetSource)) + self.assertEqual(source.prefix, abspath) + + resource_name = 'bar.pt' + expected = os.path.join(here, 'pkgs', 'asset', + 'subpackage', 'templates', 'bar.pt') + self.assertEqual(override.source.get_filename(resource_name), + expected) + + def test__override_not_yet_registered(self): + from pyramid.interfaces import IPackageOverrides + package = DummyPackage('package') + source = DummyAssetSource() + config = self._makeOne() + config._override(package, 'path', source, + PackageOverrides=DummyPackageOverrides) + overrides = config.registry.queryUtility(IPackageOverrides, + name='package') + self.assertEqual(overrides.inserted, [('path', source)]) + self.assertEqual(overrides.package, package) + + def test__override_already_registered(self): + from pyramid.interfaces import IPackageOverrides + package = DummyPackage('package') + source = DummyAssetSource() + overrides = DummyPackageOverrides(package) + config = self._makeOne() + config.registry.registerUtility(overrides, IPackageOverrides, + name='package') + config._override(package, 'path', source, + PackageOverrides=DummyPackageOverrides) + self.assertEqual(overrides.inserted, [('path', source)]) + self.assertEqual(overrides.package, package) + + +class TestOverrideProvider(unittest.TestCase): + def setUp(self): + cleanUp() + + def tearDown(self): + cleanUp() + + def _getTargetClass(self): + from pyramid.config.assets import OverrideProvider + return OverrideProvider + + def _makeOne(self, module): + klass = self._getTargetClass() + return klass(module) + + def _registerOverrides(self, overrides, name='pyramid.tests.test_config'): + from pyramid.interfaces import IPackageOverrides + from pyramid.threadlocal import get_current_registry + reg = get_current_registry() + reg.registerUtility(overrides, IPackageOverrides, name=name) + + def test_get_resource_filename_no_overrides(self): + resource_name = 'test_assets.py' + import pyramid.tests.test_config + provider = self._makeOne(pyramid.tests.test_config) + expected = os.path.join(here, resource_name) + result = provider.get_resource_filename(None, resource_name) + self.assertEqual(result, expected) + + def test_get_resource_stream_no_overrides(self): + resource_name = 'test_assets.py' + import pyramid.tests.test_config + provider = self._makeOne(pyramid.tests.test_config) + with provider.get_resource_stream(None, resource_name) as result: + _assertBody(result.read(), os.path.join(here, resource_name)) + + def test_get_resource_string_no_overrides(self): + resource_name = 'test_assets.py' + import pyramid.tests.test_config + provider = self._makeOne(pyramid.tests.test_config) + result = provider.get_resource_string(None, resource_name) + _assertBody(result, os.path.join(here, resource_name)) + + def test_has_resource_no_overrides(self): + resource_name = 'test_assets.py' + import pyramid.tests.test_config + provider = self._makeOne(pyramid.tests.test_config) + result = provider.has_resource(resource_name) + self.assertEqual(result, True) + + def test_resource_isdir_no_overrides(self): + file_resource_name = 'test_assets.py' + directory_resource_name = 'files' + import pyramid.tests.test_config + provider = self._makeOne(pyramid.tests.test_config) + result = provider.resource_isdir(file_resource_name) + self.assertEqual(result, False) + result = provider.resource_isdir(directory_resource_name) + self.assertEqual(result, True) + + def test_resource_listdir_no_overrides(self): + resource_name = 'files' + import pyramid.tests.test_config + provider = self._makeOne(pyramid.tests.test_config) + result = provider.resource_listdir(resource_name) + self.assertTrue(result) + + def test_get_resource_filename_override_returns_None(self): + overrides = DummyOverrides(None) + self._registerOverrides(overrides) + resource_name = 'test_assets.py' + import pyramid.tests.test_config + provider = self._makeOne(pyramid.tests.test_config) + expected = os.path.join(here, resource_name) + result = provider.get_resource_filename(None, resource_name) + self.assertEqual(result, expected) + + def test_get_resource_stream_override_returns_None(self): + overrides = DummyOverrides(None) + self._registerOverrides(overrides) + resource_name = 'test_assets.py' + import pyramid.tests.test_config + provider = self._makeOne(pyramid.tests.test_config) + with provider.get_resource_stream(None, resource_name) as result: + _assertBody(result.read(), os.path.join(here, resource_name)) + + def test_get_resource_string_override_returns_None(self): + overrides = DummyOverrides(None) + self._registerOverrides(overrides) + resource_name = 'test_assets.py' + import pyramid.tests.test_config + provider = self._makeOne(pyramid.tests.test_config) + result = provider.get_resource_string(None, resource_name) + _assertBody(result, os.path.join(here, resource_name)) + + def test_has_resource_override_returns_None(self): + overrides = DummyOverrides(None) + self._registerOverrides(overrides) + resource_name = 'test_assets.py' + import pyramid.tests.test_config + provider = self._makeOne(pyramid.tests.test_config) + result = provider.has_resource(resource_name) + self.assertEqual(result, True) + + def test_resource_isdir_override_returns_None(self): + overrides = DummyOverrides(None) + self._registerOverrides(overrides) + resource_name = 'files' + import pyramid.tests.test_config + provider = self._makeOne(pyramid.tests.test_config) + result = provider.resource_isdir(resource_name) + self.assertEqual(result, True) + + def test_resource_listdir_override_returns_None(self): + overrides = DummyOverrides(None) + self._registerOverrides(overrides) + resource_name = 'files' + import pyramid.tests.test_config + provider = self._makeOne(pyramid.tests.test_config) + result = provider.resource_listdir(resource_name) + self.assertTrue(result) + + def test_get_resource_filename_override_returns_value(self): + overrides = DummyOverrides('value') + import pyramid.tests.test_config + self._registerOverrides(overrides) + provider = self._makeOne(pyramid.tests.test_config) + result = provider.get_resource_filename(None, 'test_assets.py') + self.assertEqual(result, 'value') + + def test_get_resource_stream_override_returns_value(self): + from io import BytesIO + overrides = DummyOverrides(BytesIO(b'value')) + import pyramid.tests.test_config + self._registerOverrides(overrides) + provider = self._makeOne(pyramid.tests.test_config) + with provider.get_resource_stream(None, 'test_assets.py') as stream: + self.assertEqual(stream.getvalue(), b'value') + + def test_get_resource_string_override_returns_value(self): + overrides = DummyOverrides('value') + import pyramid.tests.test_config + self._registerOverrides(overrides) + provider = self._makeOne(pyramid.tests.test_config) + result = provider.get_resource_string(None, 'test_assets.py') + self.assertEqual(result, 'value') + + def test_has_resource_override_returns_True(self): + overrides = DummyOverrides(True) + import pyramid.tests.test_config + self._registerOverrides(overrides) + provider = self._makeOne(pyramid.tests.test_config) + result = provider.has_resource('test_assets.py') + self.assertEqual(result, True) + + def test_resource_isdir_override_returns_False(self): + overrides = DummyOverrides(False) + import pyramid.tests.test_config + self._registerOverrides(overrides) + provider = self._makeOne(pyramid.tests.test_config) + result = provider.resource_isdir('files') + self.assertEqual(result, False) + + def test_resource_listdir_override_returns_values(self): + overrides = DummyOverrides(['a']) + import pyramid.tests.test_config + self._registerOverrides(overrides) + provider = self._makeOne(pyramid.tests.test_config) + result = provider.resource_listdir('files') + self.assertEqual(result, ['a']) + +class TestPackageOverrides(unittest.TestCase): + def _getTargetClass(self): + from pyramid.config.assets import PackageOverrides + return PackageOverrides + + def _makeOne(self, package=None, pkg_resources=None): + if package is None: + package = DummyPackage('package') + klass = self._getTargetClass() + if pkg_resources is None: + pkg_resources = DummyPkgResources() + return klass(package, pkg_resources=pkg_resources) + + def test_class_conforms_to_IPackageOverrides(self): + from zope.interface.verify import verifyClass + from pyramid.interfaces import IPackageOverrides + verifyClass(IPackageOverrides, self._getTargetClass()) + + def test_instance_conforms_to_IPackageOverrides(self): + from zope.interface.verify import verifyObject + from pyramid.interfaces import IPackageOverrides + verifyObject(IPackageOverrides, self._makeOne()) + + def test_class_conforms_to_IPEP302Loader(self): + from zope.interface.verify import verifyClass + from pyramid.interfaces import IPEP302Loader + verifyClass(IPEP302Loader, self._getTargetClass()) + + def test_instance_conforms_to_IPEP302Loader(self): + from zope.interface.verify import verifyObject + from pyramid.interfaces import IPEP302Loader + verifyObject(IPEP302Loader, self._makeOne()) + + def test_ctor_package_already_has_loader_of_different_type(self): + package = DummyPackage('package') + loader = package.__loader__ = DummyLoader() + po = self._makeOne(package) + self.assertTrue(package.__loader__ is po) + self.assertTrue(po.real_loader is loader) + + def test_ctor_package_already_has_loader_of_same_type(self): + package = DummyPackage('package') + package.__loader__ = self._makeOne(package) + po = self._makeOne(package) + self.assertEqual(package.__loader__, po) + + def test_ctor_sets_loader(self): + package = DummyPackage('package') + po = self._makeOne(package) + self.assertEqual(package.__loader__, po) + + def test_ctor_registers_loader_type(self): + from pyramid.config.assets import OverrideProvider + dummy_pkg_resources = DummyPkgResources() + package = DummyPackage('package') + po = self._makeOne(package, dummy_pkg_resources) + self.assertEqual(dummy_pkg_resources.registered, [(po.__class__, + OverrideProvider)]) + + def test_ctor_sets_local_state(self): + package = DummyPackage('package') + po = self._makeOne(package) + self.assertEqual(po.overrides, []) + self.assertEqual(po.overridden_package_name, 'package') + + def test_insert_directory(self): + from pyramid.config.assets import DirectoryOverride + package = DummyPackage('package') + po = self._makeOne(package) + po.overrides = [None] + po.insert('foo/', DummyAssetSource()) + self.assertEqual(len(po.overrides), 2) + override = po.overrides[0] + self.assertEqual(override.__class__, DirectoryOverride) + + def test_insert_file(self): + from pyramid.config.assets import FileOverride + package = DummyPackage('package') + po = self._makeOne(package) + po.overrides = [None] + po.insert('foo.pt', DummyAssetSource()) + self.assertEqual(len(po.overrides), 2) + override = po.overrides[0] + self.assertEqual(override.__class__, FileOverride) + + def test_insert_emptystring(self): + # XXX is this a valid case for a directory? + from pyramid.config.assets import DirectoryOverride + package = DummyPackage('package') + po = self._makeOne(package) + po.overrides = [None] + source = DummyAssetSource() + po.insert('', source) + self.assertEqual(len(po.overrides), 2) + override = po.overrides[0] + self.assertEqual(override.__class__, DirectoryOverride) + + def test_filtered_sources(self): + overrides = [ DummyOverride(None), DummyOverride('foo')] + package = DummyPackage('package') + po = self._makeOne(package) + po.overrides = overrides + self.assertEqual(list(po.filtered_sources('whatever')), ['foo']) + + def test_get_filename(self): + source = DummyAssetSource(filename='foo.pt') + overrides = [ DummyOverride(None), DummyOverride((source, ''))] + package = DummyPackage('package') + po = self._makeOne(package) + po.overrides = overrides + result = po.get_filename('whatever') + self.assertEqual(result, 'foo.pt') + self.assertEqual(source.resource_name, '') + + def test_get_filename_file_doesnt_exist(self): + source = DummyAssetSource(filename=None) + overrides = [DummyOverride(None), DummyOverride((source, 'wont_exist'))] + package = DummyPackage('package') + po = self._makeOne(package) + po.overrides = overrides + self.assertEqual(po.get_filename('whatever'), None) + self.assertEqual(source.resource_name, 'wont_exist') + + def test_get_stream(self): + source = DummyAssetSource(stream='a stream?') + overrides = [DummyOverride(None), DummyOverride((source, 'foo.pt'))] + package = DummyPackage('package') + po = self._makeOne(package) + po.overrides = overrides + self.assertEqual(po.get_stream('whatever'), 'a stream?') + self.assertEqual(source.resource_name, 'foo.pt') + + def test_get_stream_file_doesnt_exist(self): + source = DummyAssetSource(stream=None) + overrides = [DummyOverride(None), DummyOverride((source, 'wont_exist'))] + package = DummyPackage('package') + po = self._makeOne(package) + po.overrides = overrides + self.assertEqual(po.get_stream('whatever'), None) + self.assertEqual(source.resource_name, 'wont_exist') + + def test_get_string(self): + source = DummyAssetSource(string='a string') + overrides = [DummyOverride(None), DummyOverride((source, 'foo.pt'))] + package = DummyPackage('package') + po = self._makeOne(package) + po.overrides = overrides + self.assertEqual(po.get_string('whatever'), 'a string') + self.assertEqual(source.resource_name, 'foo.pt') + + def test_get_string_file_doesnt_exist(self): + source = DummyAssetSource(string=None) + overrides = [DummyOverride(None), DummyOverride((source, 'wont_exist'))] + package = DummyPackage('package') + po = self._makeOne(package) + po.overrides = overrides + self.assertEqual(po.get_string('whatever'), None) + self.assertEqual(source.resource_name, 'wont_exist') + + def test_has_resource(self): + source = DummyAssetSource(exists=True) + overrides = [DummyOverride(None), DummyOverride((source, 'foo.pt'))] + package = DummyPackage('package') + po = self._makeOne(package) + po.overrides = overrides + self.assertEqual(po.has_resource('whatever'), True) + self.assertEqual(source.resource_name, 'foo.pt') + + def test_has_resource_file_doesnt_exist(self): + source = DummyAssetSource(exists=None) + overrides = [DummyOverride(None), DummyOverride((source, 'wont_exist'))] + package = DummyPackage('package') + po = self._makeOne(package) + po.overrides = overrides + self.assertEqual(po.has_resource('whatever'), None) + self.assertEqual(source.resource_name, 'wont_exist') + + def test_isdir_false(self): + source = DummyAssetSource(isdir=False) + overrides = [DummyOverride(None), DummyOverride((source, 'foo.pt'))] + package = DummyPackage('package') + po = self._makeOne(package) + po.overrides = overrides + self.assertEqual(po.isdir('whatever'), False) + self.assertEqual(source.resource_name, 'foo.pt') + + def test_isdir_true(self): + source = DummyAssetSource(isdir=True) + overrides = [DummyOverride(None), DummyOverride((source, 'foo.pt'))] + package = DummyPackage('package') + po = self._makeOne(package) + po.overrides = overrides + self.assertEqual(po.isdir('whatever'), True) + self.assertEqual(source.resource_name, 'foo.pt') + + def test_isdir_doesnt_exist(self): + source = DummyAssetSource(isdir=None) + overrides = [DummyOverride(None), DummyOverride((source, 'wont_exist'))] + package = DummyPackage('package') + po = self._makeOne(package) + po.overrides = overrides + self.assertEqual(po.isdir('whatever'), None) + self.assertEqual(source.resource_name, 'wont_exist') + + def test_listdir(self): + source = DummyAssetSource(listdir=True) + overrides = [DummyOverride(None), DummyOverride((source, 'foo.pt'))] + package = DummyPackage('package') + po = self._makeOne(package) + po.overrides = overrides + self.assertEqual(po.listdir('whatever'), True) + self.assertEqual(source.resource_name, 'foo.pt') + + def test_listdir_doesnt_exist(self): + source = DummyAssetSource(listdir=None) + overrides = [DummyOverride(None), DummyOverride((source, 'wont_exist'))] + package = DummyPackage('package') + po = self._makeOne(package) + po.overrides = overrides + self.assertEqual(po.listdir('whatever'), None) + self.assertEqual(source.resource_name, 'wont_exist') + + # PEP 302 __loader__ extensions: use the "real" __loader__, if present. + def test_get_data_pkg_has_no___loader__(self): + package = DummyPackage('package') + po = self._makeOne(package) + self.assertRaises(NotImplementedError, po.get_data, 'whatever') + + def test_get_data_pkg_has___loader__(self): + package = DummyPackage('package') + loader = package.__loader__ = DummyLoader() + po = self._makeOne(package) + self.assertEqual(po.get_data('whatever'), b'DEADBEEF') + self.assertEqual(loader._got_data, 'whatever') + + def test_is_package_pkg_has_no___loader__(self): + package = DummyPackage('package') + po = self._makeOne(package) + self.assertRaises(NotImplementedError, po.is_package, 'whatever') + + def test_is_package_pkg_has___loader__(self): + package = DummyPackage('package') + loader = package.__loader__ = DummyLoader() + po = self._makeOne(package) + self.assertTrue(po.is_package('whatever')) + self.assertEqual(loader._is_package, 'whatever') + + def test_get_code_pkg_has_no___loader__(self): + package = DummyPackage('package') + po = self._makeOne(package) + self.assertRaises(NotImplementedError, po.get_code, 'whatever') + + def test_get_code_pkg_has___loader__(self): + package = DummyPackage('package') + loader = package.__loader__ = DummyLoader() + po = self._makeOne(package) + self.assertEqual(po.get_code('whatever'), b'DEADBEEF') + self.assertEqual(loader._got_code, 'whatever') + + def test_get_source_pkg_has_no___loader__(self): + package = DummyPackage('package') + po = self._makeOne(package) + self.assertRaises(NotImplementedError, po.get_source, 'whatever') + + def test_get_source_pkg_has___loader__(self): + package = DummyPackage('package') + loader = package.__loader__ = DummyLoader() + po = self._makeOne(package) + self.assertEqual(po.get_source('whatever'), 'def foo():\n pass') + self.assertEqual(loader._got_source, 'whatever') + +class AssetSourceIntegrationTests(object): + + def test_get_filename(self): + source = self._makeOne('') + self.assertEqual(source.get_filename('test_assets.py'), + os.path.join(here, 'test_assets.py')) + + def test_get_filename_with_prefix(self): + source = self._makeOne('test_assets.py') + self.assertEqual(source.get_filename(''), + os.path.join(here, 'test_assets.py')) + + def test_get_filename_file_doesnt_exist(self): + source = self._makeOne('') + self.assertEqual(source.get_filename('wont_exist'), None) + + def test_get_stream(self): + source = self._makeOne('') + with source.get_stream('test_assets.py') as stream: + _assertBody(stream.read(), os.path.join(here, 'test_assets.py')) + + def test_get_stream_with_prefix(self): + source = self._makeOne('test_assets.py') + with source.get_stream('') as stream: + _assertBody(stream.read(), os.path.join(here, 'test_assets.py')) + + def test_get_stream_file_doesnt_exist(self): + source = self._makeOne('') + self.assertEqual(source.get_stream('wont_exist'), None) + + def test_get_string(self): + source = self._makeOne('') + _assertBody(source.get_string('test_assets.py'), + os.path.join(here, 'test_assets.py')) + + def test_get_string_with_prefix(self): + source = self._makeOne('test_assets.py') + _assertBody(source.get_string(''), + os.path.join(here, 'test_assets.py')) + + def test_get_string_file_doesnt_exist(self): + source = self._makeOne('') + self.assertEqual(source.get_string('wont_exist'), None) + + def test_exists(self): + source = self._makeOne('') + self.assertEqual(source.exists('test_assets.py'), True) + + def test_exists_with_prefix(self): + source = self._makeOne('test_assets.py') + self.assertEqual(source.exists(''), True) + + def test_exists_file_doesnt_exist(self): + source = self._makeOne('') + self.assertEqual(source.exists('wont_exist'), None) + + def test_isdir_false(self): + source = self._makeOne('') + self.assertEqual(source.isdir('test_assets.py'), False) + + def test_isdir_true(self): + source = self._makeOne('') + self.assertEqual(source.isdir('files'), True) + + def test_isdir_doesnt_exist(self): + source = self._makeOne('') + self.assertEqual(source.isdir('wont_exist'), None) + + def test_listdir(self): + source = self._makeOne('') + self.assertTrue(source.listdir('files')) + + def test_listdir_doesnt_exist(self): + source = self._makeOne('') + self.assertEqual(source.listdir('wont_exist'), None) + +class TestPackageAssetSource(AssetSourceIntegrationTests, unittest.TestCase): + + def _getTargetClass(self): + from pyramid.config.assets import PackageAssetSource + return PackageAssetSource + + def _makeOne(self, prefix, package='pyramid.tests.test_config'): + klass = self._getTargetClass() + return klass(package, prefix) + +class TestFSAssetSource(AssetSourceIntegrationTests, unittest.TestCase): + def _getTargetClass(self): + from pyramid.config.assets import FSAssetSource + return FSAssetSource + + def _makeOne(self, prefix, base_prefix=here): + klass = self._getTargetClass() + return klass(os.path.join(base_prefix, prefix)) + +class TestDirectoryOverride(unittest.TestCase): + def _getTargetClass(self): + from pyramid.config.assets import DirectoryOverride + return DirectoryOverride + + def _makeOne(self, path, source): + klass = self._getTargetClass() + return klass(path, source) + + def test_it_match(self): + source = DummyAssetSource() + o = self._makeOne('foo/', source) + result = o('foo/something.pt') + self.assertEqual(result, (source, 'something.pt')) + + def test_it_no_match(self): + source = DummyAssetSource() + o = self._makeOne('foo/', source) + result = o('baz/notfound.pt') + self.assertEqual(result, None) + +class TestFileOverride(unittest.TestCase): + def _getTargetClass(self): + from pyramid.config.assets import FileOverride + return FileOverride + + def _makeOne(self, path, source): + klass = self._getTargetClass() + return klass(path, source) + + def test_it_match(self): + source = DummyAssetSource() + o = self._makeOne('foo.pt', source) + result = o('foo.pt') + self.assertEqual(result, (source, '')) + + def test_it_no_match(self): + source = DummyAssetSource() + o = self._makeOne('foo.pt', source) + result = o('notfound.pt') + self.assertEqual(result, None) + +class DummyOverride: + def __init__(self, result): + self.result = result + + def __call__(self, resource_name): + return self.result + +class DummyOverrides: + def __init__(self, result): + self.result = result + + def get_filename(self, resource_name): + return self.result + + listdir = isdir = has_resource = get_stream = get_string = get_filename + +class DummyPackageOverrides: + def __init__(self, package): + self.package = package + self.inserted = [] + + def insert(self, path, source): + self.inserted.append((path, source)) + +class DummyPkgResources: + def __init__(self): + self.registered = [] + + def register_loader_type(self, typ, inst): + self.registered.append((typ, inst)) + +class DummyPackage: + def __init__(self, name): + self.__name__ = name + +class DummyAssetSource: + def __init__(self, **kw): + self.kw = kw + + def get_filename(self, resource_name): + self.resource_name = resource_name + return self.kw['filename'] + + def get_stream(self, resource_name): + self.resource_name = resource_name + return self.kw['stream'] + + def get_string(self, resource_name): + self.resource_name = resource_name + return self.kw['string'] + + def exists(self, resource_name): + self.resource_name = resource_name + return self.kw['exists'] + + def isdir(self, resource_name): + self.resource_name = resource_name + return self.kw['isdir'] + + def listdir(self, resource_name): + self.resource_name = resource_name + return self.kw['listdir'] + +class DummyLoader: + _got_data = _is_package = None + def get_data(self, path): + self._got_data = path + return b'DEADBEEF' + def is_package(self, fullname): + self._is_package = fullname + return True + def get_code(self, fullname): + self._got_code = fullname + return b'DEADBEEF' + def get_source(self, fullname): + self._got_source = fullname + return 'def foo():\n pass' + +class DummyUnderOverride: + def __call__(self, package, path, source, _info=''): + self.package = package + self.path = path + self.source = source + +def read_(src): + with open(src, 'rb') as f: + contents = f.read() + return contents + +def _assertBody(body, filename): + # strip both \n and \r for windows + body = body.replace(b'\r', b'') + body = body.replace(b'\n', b'') + data = read_(filename) + data = data.replace(b'\r', b'') + data = data.replace(b'\n', b'') + assert(body == data) diff --git a/src/pyramid/tests/test_config/test_factories.py b/src/pyramid/tests/test_config/test_factories.py new file mode 100644 index 000000000..7e6ea0476 --- /dev/null +++ b/src/pyramid/tests/test_config/test_factories.py @@ -0,0 +1,163 @@ +import unittest + +from pyramid.tests.test_config import dummyfactory + +class TestFactoriesMixin(unittest.TestCase): + def _makeOne(self, *arg, **kw): + from pyramid.config import Configurator + config = Configurator(*arg, **kw) + return config + + def test_set_request_factory(self): + from pyramid.interfaces import IRequestFactory + config = self._makeOne(autocommit=True) + factory = object() + config.set_request_factory(factory) + self.assertEqual(config.registry.getUtility(IRequestFactory), factory) + + def test_set_request_factory_dottedname(self): + from pyramid.interfaces import IRequestFactory + config = self._makeOne(autocommit=True) + config.set_request_factory( + 'pyramid.tests.test_config.dummyfactory') + self.assertEqual(config.registry.getUtility(IRequestFactory), + dummyfactory) + + def test_set_response_factory(self): + from pyramid.interfaces import IResponseFactory + config = self._makeOne(autocommit=True) + factory = lambda r: object() + config.set_response_factory(factory) + self.assertEqual(config.registry.getUtility(IResponseFactory), factory) + + def test_set_response_factory_dottedname(self): + from pyramid.interfaces import IResponseFactory + config = self._makeOne(autocommit=True) + config.set_response_factory( + 'pyramid.tests.test_config.dummyfactory') + self.assertEqual(config.registry.getUtility(IResponseFactory), + dummyfactory) + + def test_set_root_factory(self): + from pyramid.interfaces import IRootFactory + config = self._makeOne() + config.set_root_factory(dummyfactory) + self.assertEqual(config.registry.queryUtility(IRootFactory), None) + config.commit() + self.assertEqual(config.registry.getUtility(IRootFactory), dummyfactory) + + def test_set_root_factory_as_None(self): + from pyramid.interfaces import IRootFactory + from pyramid.traversal import DefaultRootFactory + config = self._makeOne() + config.set_root_factory(None) + self.assertEqual(config.registry.queryUtility(IRootFactory), None) + config.commit() + self.assertEqual(config.registry.getUtility(IRootFactory), + DefaultRootFactory) + + def test_set_root_factory_dottedname(self): + from pyramid.interfaces import IRootFactory + config = self._makeOne() + config.set_root_factory('pyramid.tests.test_config.dummyfactory') + self.assertEqual(config.registry.queryUtility(IRootFactory), None) + config.commit() + self.assertEqual(config.registry.getUtility(IRootFactory), dummyfactory) + + def test_set_session_factory(self): + from pyramid.interfaces import ISessionFactory + config = self._makeOne() + config.set_session_factory(dummyfactory) + self.assertEqual(config.registry.queryUtility(ISessionFactory), None) + config.commit() + self.assertEqual(config.registry.getUtility(ISessionFactory), + dummyfactory) + + def test_set_session_factory_dottedname(self): + from pyramid.interfaces import ISessionFactory + config = self._makeOne() + config.set_session_factory('pyramid.tests.test_config.dummyfactory') + self.assertEqual(config.registry.queryUtility(ISessionFactory), None) + config.commit() + self.assertEqual(config.registry.getUtility(ISessionFactory), + dummyfactory) + + def test_add_request_method_with_callable(self): + from pyramid.interfaces import IRequestExtensions + config = self._makeOne(autocommit=True) + callable = lambda x: None + config.add_request_method(callable, name='foo') + exts = config.registry.getUtility(IRequestExtensions) + self.assertTrue('foo' in exts.methods) + + def test_add_request_method_with_unnamed_callable(self): + from pyramid.interfaces import IRequestExtensions + config = self._makeOne(autocommit=True) + def foo(self): pass + config.add_request_method(foo) + exts = config.registry.getUtility(IRequestExtensions) + self.assertTrue('foo' in exts.methods) + + def test_set_multiple_request_methods_conflict(self): + from pyramid.exceptions import ConfigurationConflictError + config = self._makeOne() + def foo(self): pass + def bar(self): pass + config.add_request_method(foo, name='bar') + config.add_request_method(bar, name='bar') + self.assertRaises(ConfigurationConflictError, config.commit) + + def test_add_request_method_with_None_callable(self): + from pyramid.interfaces import IRequestExtensions + config = self._makeOne(autocommit=True) + config.add_request_method(name='foo') + exts = config.registry.queryUtility(IRequestExtensions) + self.assertTrue(exts is None) + + def test_add_request_method_with_None_callable_conflict(self): + from pyramid.exceptions import ConfigurationConflictError + config = self._makeOne() + def bar(self): pass + config.add_request_method(name='foo') + config.add_request_method(bar, name='foo') + self.assertRaises(ConfigurationConflictError, config.commit) + + def test_add_request_method_with_None_callable_and_no_name(self): + config = self._makeOne(autocommit=True) + self.assertRaises(AttributeError, config.add_request_method) + + def test_add_request_method_with_text_type_name(self): + from pyramid.interfaces import IRequestExtensions + from pyramid.compat import text_, PY2 + from pyramid.exceptions import ConfigurationError + + config = self._makeOne(autocommit=True) + def boomshaka(r): pass + + def get_bad_name(): + if PY2: + name = text_(b'La Pe\xc3\xb1a', 'utf-8') + else: + name = b'La Pe\xc3\xb1a' + + config.add_request_method(boomshaka, name=name) + + self.assertRaises(ConfigurationError, get_bad_name) + + def test_set_execution_policy(self): + from pyramid.interfaces import IExecutionPolicy + config = self._makeOne(autocommit=True) + def dummy_policy(environ, router): pass + config.set_execution_policy(dummy_policy) + registry = config.registry + result = registry.queryUtility(IExecutionPolicy) + self.assertEqual(result, dummy_policy) + + def test_set_execution_policy_to_None(self): + from pyramid.interfaces import IExecutionPolicy + from pyramid.router import default_execution_policy + config = self._makeOne(autocommit=True) + config.set_execution_policy(None) + registry = config.registry + result = registry.queryUtility(IExecutionPolicy) + self.assertEqual(result, default_execution_policy) diff --git a/src/pyramid/tests/test_config/test_i18n.py b/src/pyramid/tests/test_config/test_i18n.py new file mode 100644 index 000000000..c10ab6bdb --- /dev/null +++ b/src/pyramid/tests/test_config/test_i18n.py @@ -0,0 +1,132 @@ +import os +import unittest + +from pyramid.tests.test_config import dummyfactory + +here = os.path.dirname(__file__) +locale = os.path.abspath( + os.path.join(here, '..', 'pkgs', 'localeapp', 'locale')) +locale2 = os.path.abspath( + os.path.join(here, '..', 'pkgs', 'localeapp', 'locale2')) +locale3 = os.path.abspath( + os.path.join(here, '..', 'pkgs', 'localeapp', 'locale3')) + +class TestI18NConfiguratorMixin(unittest.TestCase): + def _makeOne(self, *arg, **kw): + from pyramid.config import Configurator + config = Configurator(*arg, **kw) + return config + + def test_set_locale_negotiator(self): + from pyramid.interfaces import ILocaleNegotiator + config = self._makeOne(autocommit=True) + def negotiator(request): pass + config.set_locale_negotiator(negotiator) + self.assertEqual(config.registry.getUtility(ILocaleNegotiator), + negotiator) + + def test_set_locale_negotiator_dottedname(self): + from pyramid.interfaces import ILocaleNegotiator + config = self._makeOne(autocommit=True) + config.set_locale_negotiator( + 'pyramid.tests.test_config.dummyfactory') + self.assertEqual(config.registry.getUtility(ILocaleNegotiator), + dummyfactory) + + def test_add_translation_dirs_missing_dir(self): + from pyramid.exceptions import ConfigurationError + config = self._makeOne() + config.add_translation_dirs('/wont/exist/on/my/system') + self.assertRaises(ConfigurationError, config.commit) + + def test_add_translation_dirs_no_specs(self): + from pyramid.interfaces import ITranslationDirectories + config = self._makeOne() + config.add_translation_dirs() + self.assertEqual(config.registry.queryUtility(ITranslationDirectories), + None) + + def test_add_translation_dirs_asset_spec(self): + from pyramid.interfaces import ITranslationDirectories + config = self._makeOne(autocommit=True) + config.add_translation_dirs('pyramid.tests.pkgs.localeapp:locale') + self.assertEqual(config.registry.getUtility(ITranslationDirectories), + [locale]) + + def test_add_translation_dirs_asset_spec_existing_translation_dirs(self): + from pyramid.interfaces import ITranslationDirectories + config = self._makeOne(autocommit=True) + directories = ['abc'] + config.registry.registerUtility(directories, ITranslationDirectories) + config.add_translation_dirs('pyramid.tests.pkgs.localeapp:locale') + result = config.registry.getUtility(ITranslationDirectories) + self.assertEqual(result, [locale, 'abc']) + + def test_add_translation_dirs_multiple_specs(self): + from pyramid.interfaces import ITranslationDirectories + config = self._makeOne(autocommit=True) + config.add_translation_dirs('pyramid.tests.pkgs.localeapp:locale', + 'pyramid.tests.pkgs.localeapp:locale2') + self.assertEqual(config.registry.getUtility(ITranslationDirectories), + [locale, locale2]) + + def test_add_translation_dirs_multiple_specs_multiple_calls(self): + from pyramid.interfaces import ITranslationDirectories + config = self._makeOne(autocommit=True) + config.add_translation_dirs('pyramid.tests.pkgs.localeapp:locale', + 'pyramid.tests.pkgs.localeapp:locale2') + config.add_translation_dirs('pyramid.tests.pkgs.localeapp:locale3') + self.assertEqual(config.registry.getUtility(ITranslationDirectories), + [locale3, locale, locale2]) + + def test_add_translation_dirs_override_multiple_specs_multiple_calls(self): + from pyramid.interfaces import ITranslationDirectories + config = self._makeOne(autocommit=True) + config.add_translation_dirs('pyramid.tests.pkgs.localeapp:locale', + 'pyramid.tests.pkgs.localeapp:locale2') + config.add_translation_dirs('pyramid.tests.pkgs.localeapp:locale3', + override=True) + self.assertEqual(config.registry.getUtility(ITranslationDirectories), + [locale, locale2, locale3]) + + def test_add_translation_dirs_invalid_kwargs(self): + from pyramid.interfaces import ITranslationDirectories + config = self._makeOne(autocommit=True) + with self.assertRaises(TypeError): + config.add_translation_dirs('pyramid.tests.pkgs.localeapp:locale', + foo=1) + + def test_add_translation_dirs_abspath(self): + from pyramid.interfaces import ITranslationDirectories + config = self._makeOne(autocommit=True) + config.add_translation_dirs(locale) + self.assertEqual(config.registry.getUtility(ITranslationDirectories), + [locale]) + + def test_add_translation_dirs_uses_override_out_of_order(self): + from pyramid.interfaces import ITranslationDirectories + config = self._makeOne() + config.add_translation_dirs('pyramid.tests.pkgs.localeapp:locale') + config.override_asset('pyramid.tests.pkgs.localeapp:locale/', + 'pyramid.tests.pkgs.localeapp:locale2/') + config.commit() + self.assertEqual(config.registry.getUtility(ITranslationDirectories), + [locale2]) + + def test_add_translation_dirs_doesnt_use_override_w_autocommit(self): + from pyramid.interfaces import ITranslationDirectories + config = self._makeOne(autocommit=True) + config.add_translation_dirs('pyramid.tests.pkgs.localeapp:locale') + config.override_asset('pyramid.tests.pkgs.localeapp:locale/', + 'pyramid.tests.pkgs.localeapp:locale2/') + self.assertEqual(config.registry.getUtility(ITranslationDirectories), + [locale]) + + def test_add_translation_dirs_uses_override_w_autocommit(self): + from pyramid.interfaces import ITranslationDirectories + config = self._makeOne(autocommit=True) + config.override_asset('pyramid.tests.pkgs.localeapp:locale/', + 'pyramid.tests.pkgs.localeapp:locale2/') + config.add_translation_dirs('pyramid.tests.pkgs.localeapp:locale') + self.assertEqual(config.registry.getUtility(ITranslationDirectories), + [locale2]) diff --git a/src/pyramid/tests/test_config/test_init.py b/src/pyramid/tests/test_config/test_init.py new file mode 100644 index 000000000..76a3d703d --- /dev/null +++ b/src/pyramid/tests/test_config/test_init.py @@ -0,0 +1,2068 @@ +import unittest + +import os + +from pyramid.compat import im_func +from pyramid.testing import skip_on + +from pyramid.tests.test_config import dummy_tween_factory +from pyramid.tests.test_config import dummy_include +from pyramid.tests.test_config import dummy_extend +from pyramid.tests.test_config import dummy_extend2 +from pyramid.tests.test_config import IDummy +from pyramid.tests.test_config import DummyContext + +from pyramid.exceptions import ConfigurationExecutionError +from pyramid.exceptions import ConfigurationConflictError + +from pyramid.interfaces import IRequest + +class ConfiguratorTests(unittest.TestCase): + def _makeOne(self, *arg, **kw): + from pyramid.config import Configurator + config = Configurator(*arg, **kw) + return config + + def _getViewCallable(self, config, ctx_iface=None, request_iface=None, + name='', exception_view=False): + from zope.interface import Interface + from pyramid.interfaces import IRequest + from pyramid.interfaces import IView + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IExceptionViewClassifier + if exception_view: # pragma: no cover + classifier = IExceptionViewClassifier + else: + classifier = IViewClassifier + if ctx_iface is None: + ctx_iface = Interface + if request_iface is None: + request_iface = IRequest + return config.registry.adapters.lookup( + (classifier, request_iface, ctx_iface), IView, name=name, + default=None) + + def _registerEventListener(self, config, event_iface=None): + if event_iface is None: # pragma: no cover + from zope.interface import Interface + event_iface = Interface + L = [] + def subscriber(*event): + L.extend(event) + config.registry.registerHandler(subscriber, (event_iface,)) + return L + + def _makeRequest(self, config): + request = DummyRequest() + request.registry = config.registry + return request + + def test_ctor_no_registry(self): + import sys + from pyramid.interfaces import ISettings + from pyramid.config import Configurator + from pyramid.interfaces import IRendererFactory + config = Configurator() + this_pkg = sys.modules['pyramid.tests.test_config'] + self.assertTrue(config.registry.getUtility(ISettings)) + self.assertEqual(config.package, this_pkg) + config.commit() + self.assertTrue(config.registry.getUtility(IRendererFactory, 'json')) + self.assertTrue(config.registry.getUtility(IRendererFactory, 'string')) + + def test_begin(self): + from pyramid.config import Configurator + config = Configurator() + manager = DummyThreadLocalManager() + config.manager = manager + config.begin() + self.assertEqual(manager.pushed, + {'registry':config.registry, 'request':None}) + self.assertEqual(manager.popped, False) + + def test_begin_with_request(self): + from pyramid.config import Configurator + config = Configurator() + request = object() + manager = DummyThreadLocalManager() + config.manager = manager + config.begin(request=request) + self.assertEqual(manager.pushed, + {'registry':config.registry, 'request':request}) + self.assertEqual(manager.popped, False) + + def test_begin_overrides_request(self): + from pyramid.config import Configurator + config = Configurator() + manager = DummyThreadLocalManager() + req = object() + # set it up for auto-propagation + pushed = {'registry': config.registry, 'request': None} + manager.pushed = pushed + config.manager = manager + config.begin(req) + self.assertTrue(manager.pushed is not pushed) + self.assertEqual(manager.pushed['request'], req) + self.assertEqual(manager.pushed['registry'], config.registry) + + def test_begin_propagates_request_for_same_registry(self): + from pyramid.config import Configurator + config = Configurator() + manager = DummyThreadLocalManager() + req = object() + pushed = {'registry': config.registry, 'request': req} + manager.pushed = pushed + config.manager = manager + config.begin() + self.assertTrue(manager.pushed is not pushed) + self.assertEqual(manager.pushed['request'], req) + self.assertEqual(manager.pushed['registry'], config.registry) + + def test_begin_does_not_propagate_request_for_diff_registry(self): + from pyramid.config import Configurator + config = Configurator() + manager = DummyThreadLocalManager() + req = object() + pushed = {'registry': object(), 'request': req} + manager.pushed = pushed + config.manager = manager + config.begin() + self.assertTrue(manager.pushed is not pushed) + self.assertEqual(manager.pushed['request'], None) + self.assertEqual(manager.pushed['registry'], config.registry) + + def test_end(self): + from pyramid.config import Configurator + config = Configurator() + manager = DummyThreadLocalManager() + pushed = manager.pushed + config.manager = manager + config.end() + self.assertEqual(manager.pushed, pushed) + self.assertEqual(manager.popped, True) + + def test_context_manager(self): + from pyramid.config import Configurator + config = Configurator() + manager = DummyThreadLocalManager() + config.manager = manager + view = lambda r: None + with config as ctx: + self.assertTrue(config is ctx) + self.assertEqual(manager.pushed, + {'registry': config.registry, 'request': None}) + self.assertFalse(manager.popped) + config.add_view(view) + self.assertTrue(manager.popped) + config.add_view(view) # did not raise a conflict because of commit + config.commit() + + def test_ctor_with_package_registry(self): + import sys + from pyramid.config import Configurator + pkg = sys.modules['pyramid'] + config = Configurator(package=pkg) + self.assertEqual(config.package, pkg) + + def test_ctor_noreg_custom_settings(self): + from pyramid.interfaces import ISettings + settings = {'reload_templates':True, + 'mysetting':True} + config = self._makeOne(settings=settings) + settings = config.registry.getUtility(ISettings) + self.assertEqual(settings['reload_templates'], True) + self.assertEqual(settings['debug_authorization'], False) + self.assertEqual(settings['mysetting'], True) + + def test_ctor_noreg_debug_logger_None_default(self): + from pyramid.interfaces import IDebugLogger + config = self._makeOne() + logger = config.registry.getUtility(IDebugLogger) + self.assertEqual(logger.name, 'pyramid.tests.test_config') + + def test_ctor_noreg_debug_logger_non_None(self): + from pyramid.interfaces import IDebugLogger + logger = object() + config = self._makeOne(debug_logger=logger) + result = config.registry.getUtility(IDebugLogger) + self.assertEqual(logger, result) + + def test_ctor_authentication_policy(self): + from pyramid.interfaces import IAuthenticationPolicy + policy = object() + config = self._makeOne(authentication_policy=policy) + config.commit() + result = config.registry.getUtility(IAuthenticationPolicy) + self.assertEqual(policy, result) + + def test_ctor_authorization_policy_only(self): + policy = object() + config = self._makeOne(authorization_policy=policy) + self.assertRaises(ConfigurationExecutionError, config.commit) + + def test_ctor_no_root_factory(self): + from pyramid.interfaces import IRootFactory + config = self._makeOne() + self.assertEqual(config.registry.queryUtility(IRootFactory), None) + config.commit() + self.assertEqual(config.registry.queryUtility(IRootFactory), None) + + def test_ctor_with_root_factory(self): + from pyramid.interfaces import IRootFactory + factory = object() + config = self._makeOne(root_factory=factory) + self.assertEqual(config.registry.queryUtility(IRootFactory), None) + config.commit() + self.assertEqual(config.registry.queryUtility(IRootFactory), factory) + + def test_ctor_alternate_renderers(self): + from pyramid.interfaces import IRendererFactory + renderer = object() + config = self._makeOne(renderers=[('yeah', renderer)]) + config.commit() + self.assertEqual(config.registry.getUtility(IRendererFactory, 'yeah'), + renderer) + + def test_ctor_default_renderers(self): + from pyramid.interfaces import IRendererFactory + from pyramid.renderers import json_renderer_factory + config = self._makeOne() + self.assertEqual(config.registry.getUtility(IRendererFactory, 'json'), + json_renderer_factory) + + def test_ctor_default_permission(self): + from pyramid.interfaces import IDefaultPermission + config = self._makeOne(default_permission='view') + config.commit() + self.assertEqual(config.registry.getUtility(IDefaultPermission), 'view') + + def test_ctor_session_factory(self): + from pyramid.interfaces import ISessionFactory + factory = object() + config = self._makeOne(session_factory=factory) + self.assertEqual(config.registry.queryUtility(ISessionFactory), None) + config.commit() + self.assertEqual(config.registry.getUtility(ISessionFactory), factory) + + def test_ctor_default_view_mapper(self): + from pyramid.interfaces import IViewMapperFactory + mapper = object() + config = self._makeOne(default_view_mapper=mapper) + config.commit() + self.assertEqual(config.registry.getUtility(IViewMapperFactory), + mapper) + + def test_ctor_httpexception_view_default(self): + from pyramid.interfaces import IExceptionResponse + from pyramid.httpexceptions import default_exceptionresponse_view + from pyramid.interfaces import IRequest + config = self._makeOne() + view = self._getViewCallable(config, + ctx_iface=IExceptionResponse, + request_iface=IRequest) + self.assertTrue(view.__wraps__ is default_exceptionresponse_view) + + def test_ctor_exceptionresponse_view_None(self): + from pyramid.interfaces import IExceptionResponse + from pyramid.interfaces import IRequest + config = self._makeOne(exceptionresponse_view=None) + view = self._getViewCallable(config, + ctx_iface=IExceptionResponse, + request_iface=IRequest) + self.assertTrue(view is None) + + def test_ctor_exceptionresponse_view_custom(self): + from pyramid.interfaces import IExceptionResponse + from pyramid.interfaces import IRequest + def exceptionresponse_view(context, request): pass + config = self._makeOne(exceptionresponse_view=exceptionresponse_view) + view = self._getViewCallable(config, + ctx_iface=IExceptionResponse, + request_iface=IRequest) + self.assertTrue(view.__wraps__ is exceptionresponse_view) + + def test_ctor_with_introspection(self): + config = self._makeOne(introspection=False) + self.assertEqual(config.introspection, False) + + def test_ctor_default_webob_response_adapter_registered(self): + from webob import Response as WebobResponse + response = WebobResponse() + from pyramid.interfaces import IResponse + config = self._makeOne(autocommit=True) + result = config.registry.queryAdapter(response, IResponse) + self.assertEqual(result, response) + + def test_with_package_module(self): + from pyramid.tests.test_config import test_init + import pyramid.tests + config = self._makeOne() + newconfig = config.with_package(test_init) + self.assertEqual(newconfig.package, pyramid.tests.test_config) + + def test_with_package_package(self): + import pyramid.tests.test_config + config = self._makeOne() + newconfig = config.with_package(pyramid.tests.test_config) + self.assertEqual(newconfig.package, pyramid.tests.test_config) + + def test_with_package(self): + import pyramid.tests + config = self._makeOne() + config.basepath = 'basepath' + config.info = 'info' + config.includepath = ('spec',) + config.autocommit = True + config.route_prefix = 'prefix' + newconfig = config.with_package(pyramid.tests) + self.assertEqual(newconfig.package, pyramid.tests) + self.assertEqual(newconfig.registry, config.registry) + self.assertEqual(newconfig.autocommit, True) + self.assertEqual(newconfig.route_prefix, 'prefix') + self.assertEqual(newconfig.info, 'info') + self.assertEqual(newconfig.basepath, 'basepath') + self.assertEqual(newconfig.includepath, ('spec',)) + + def test_maybe_dotted_string_success(self): + import pyramid.tests.test_config + config = self._makeOne() + result = config.maybe_dotted('pyramid.tests.test_config') + self.assertEqual(result, pyramid.tests.test_config) + + def test_maybe_dotted_string_fail(self): + config = self._makeOne() + self.assertRaises(ImportError, config.maybe_dotted, 'cant.be.found') + + def test_maybe_dotted_notstring_success(self): + import pyramid.tests.test_config + config = self._makeOne() + result = config.maybe_dotted(pyramid.tests.test_config) + self.assertEqual(result, pyramid.tests.test_config) + + def test_absolute_asset_spec_already_absolute(self): + import pyramid.tests.test_config + config = self._makeOne(package=pyramid.tests.test_config) + result = config.absolute_asset_spec('already:absolute') + self.assertEqual(result, 'already:absolute') + + def test_absolute_asset_spec_notastring(self): + import pyramid.tests.test_config + config = self._makeOne(package=pyramid.tests.test_config) + result = config.absolute_asset_spec(None) + self.assertEqual(result, None) + + def test_absolute_asset_spec_relative(self): + import pyramid.tests.test_config + config = self._makeOne(package=pyramid.tests.test_config) + result = config.absolute_asset_spec('files') + self.assertEqual(result, 'pyramid.tests.test_config:files') + + def test__fix_registry_has_listeners(self): + reg = DummyRegistry() + config = self._makeOne(reg) + config._fix_registry() + self.assertEqual(reg.has_listeners, True) + + def test__fix_registry_notify(self): + reg = DummyRegistry() + config = self._makeOne(reg) + config._fix_registry() + self.assertEqual(reg.notify(1), None) + self.assertEqual(reg.events, (1,)) + + def test__fix_registry_queryAdapterOrSelf(self): + from zope.interface import Interface + from zope.interface import implementer + class IFoo(Interface): + pass + @implementer(IFoo) + class Foo(object): + pass + class Bar(object): + pass + adaptation = () + foo = Foo() + bar = Bar() + reg = DummyRegistry(adaptation) + config = self._makeOne(reg) + config._fix_registry() + self.assertTrue(reg.queryAdapterOrSelf(foo, IFoo) is foo) + self.assertTrue(reg.queryAdapterOrSelf(bar, IFoo) is adaptation) + + def test__fix_registry_registerSelfAdapter(self): + reg = DummyRegistry() + config = self._makeOne(reg) + config._fix_registry() + reg.registerSelfAdapter('required', 'provided', name='abc') + self.assertEqual(len(reg.adapters), 1) + args, kw = reg.adapters[0] + self.assertEqual(args[0]('abc'), 'abc') + self.assertEqual(kw, + {'info': '', 'provided': 'provided', + 'required': 'required', 'name': 'abc', 'event': True}) + + def test__fix_registry_adds__lock(self): + reg = DummyRegistry() + config = self._makeOne(reg) + config._fix_registry() + self.assertTrue(hasattr(reg, '_lock')) + + def test__fix_registry_adds_clear_view_lookup_cache(self): + reg = DummyRegistry() + config = self._makeOne(reg) + self.assertFalse(hasattr(reg, '_clear_view_lookup_cache')) + config._fix_registry() + self.assertFalse(hasattr(reg, '_view_lookup_cache')) + reg._clear_view_lookup_cache() + self.assertEqual(reg._view_lookup_cache, {}) + + def test_setup_registry_calls_fix_registry(self): + reg = DummyRegistry() + config = self._makeOne(reg) + config.add_view = lambda *arg, **kw: False + config._add_tween = lambda *arg, **kw: False + config.setup_registry() + self.assertEqual(reg.has_listeners, True) + + def test_setup_registry_registers_default_exceptionresponse_views(self): + from webob.exc import WSGIHTTPException + from pyramid.interfaces import IExceptionResponse + from pyramid.view import default_exceptionresponse_view + reg = DummyRegistry() + config = self._makeOne(reg) + views = [] + config.add_view = lambda *arg, **kw: views.append((arg, kw)) + config.add_default_view_predicates = lambda *arg: None + config._add_tween = lambda *arg, **kw: False + config.setup_registry() + self.assertEqual(views[0], ((default_exceptionresponse_view,), + {'context':IExceptionResponse})) + self.assertEqual(views[1], ((default_exceptionresponse_view,), + {'context':WSGIHTTPException})) + + def test_setup_registry_registers_default_view_predicates(self): + reg = DummyRegistry() + config = self._makeOne(reg) + vp_called = [] + config.add_view = lambda *arg, **kw: None + config.add_default_view_predicates = lambda *arg: vp_called.append(True) + config._add_tween = lambda *arg, **kw: False + config.setup_registry() + self.assertTrue(vp_called) + + def test_setup_registry_registers_default_webob_iresponse_adapter(self): + from webob import Response + from pyramid.interfaces import IResponse + config = self._makeOne() + config.setup_registry() + response = Response() + self.assertTrue( + config.registry.queryAdapter(response, IResponse) is response) + + def test_setup_registry_explicit_notfound_trumps_iexceptionresponse(self): + from pyramid.renderers import null_renderer + from zope.interface import implementedBy + from pyramid.interfaces import IRequest + from pyramid.httpexceptions import HTTPNotFound + from pyramid.registry import Registry + reg = Registry() + config = self._makeOne(reg, autocommit=True) + config.setup_registry() # registers IExceptionResponse default view + def myview(context, request): + return 'OK' + config.add_view(myview, context=HTTPNotFound, renderer=null_renderer) + request = self._makeRequest(config) + view = self._getViewCallable(config, + ctx_iface=implementedBy(HTTPNotFound), + request_iface=IRequest) + result = view(None, request) + self.assertEqual(result, 'OK') + + def test_setup_registry_custom_settings(self): + from pyramid.registry import Registry + from pyramid.interfaces import ISettings + settings = {'reload_templates':True, + 'mysetting':True} + reg = Registry() + config = self._makeOne(reg) + config.setup_registry(settings=settings) + settings = reg.getUtility(ISettings) + self.assertEqual(settings['reload_templates'], True) + self.assertEqual(settings['debug_authorization'], False) + self.assertEqual(settings['mysetting'], True) + + def test_setup_registry_debug_logger_None_default(self): + from pyramid.registry import Registry + from pyramid.interfaces import IDebugLogger + reg = Registry() + config = self._makeOne(reg) + config.setup_registry() + logger = reg.getUtility(IDebugLogger) + self.assertEqual(logger.name, 'pyramid.tests.test_config') + + def test_setup_registry_debug_logger_non_None(self): + from pyramid.registry import Registry + from pyramid.interfaces import IDebugLogger + logger = object() + reg = Registry() + config = self._makeOne(reg) + config.setup_registry(debug_logger=logger) + result = reg.getUtility(IDebugLogger) + self.assertEqual(logger, result) + + def test_setup_registry_debug_logger_name(self): + from pyramid.registry import Registry + from pyramid.interfaces import IDebugLogger + reg = Registry() + config = self._makeOne(reg) + config.setup_registry(debug_logger='foo') + result = reg.getUtility(IDebugLogger) + self.assertEqual(result.name, 'foo') + + def test_setup_registry_authentication_policy(self): + from pyramid.registry import Registry + from pyramid.interfaces import IAuthenticationPolicy + policy = object() + reg = Registry() + config = self._makeOne(reg) + config.setup_registry(authentication_policy=policy) + config.commit() + result = reg.getUtility(IAuthenticationPolicy) + self.assertEqual(policy, result) + + def test_setup_registry_authentication_policy_dottedname(self): + from pyramid.registry import Registry + from pyramid.interfaces import IAuthenticationPolicy + reg = Registry() + config = self._makeOne(reg) + config.setup_registry(authentication_policy='pyramid.tests.test_config') + config.commit() + result = reg.getUtility(IAuthenticationPolicy) + import pyramid.tests.test_config + self.assertEqual(result, pyramid.tests.test_config) + + def test_setup_registry_authorization_policy_dottedname(self): + from pyramid.registry import Registry + from pyramid.interfaces import IAuthorizationPolicy + reg = Registry() + config = self._makeOne(reg) + dummy = object() + config.setup_registry(authentication_policy=dummy, + authorization_policy='pyramid.tests.test_config') + config.commit() + result = reg.getUtility(IAuthorizationPolicy) + import pyramid.tests.test_config + self.assertEqual(result, pyramid.tests.test_config) + + def test_setup_registry_authorization_policy_only(self): + from pyramid.registry import Registry + policy = object() + reg = Registry() + config = self._makeOne(reg) + config.setup_registry(authorization_policy=policy) + config = self.assertRaises(ConfigurationExecutionError, config.commit) + + def test_setup_registry_no_default_root_factory(self): + from pyramid.registry import Registry + from pyramid.interfaces import IRootFactory + reg = Registry() + config = self._makeOne(reg) + config.setup_registry() + config.commit() + self.assertEqual(reg.queryUtility(IRootFactory), None) + + def test_setup_registry_dottedname_root_factory(self): + from pyramid.registry import Registry + from pyramid.interfaces import IRootFactory + reg = Registry() + config = self._makeOne(reg) + import pyramid.tests.test_config + config.setup_registry(root_factory='pyramid.tests.test_config') + self.assertEqual(reg.queryUtility(IRootFactory), None) + config.commit() + self.assertEqual(reg.getUtility(IRootFactory), + pyramid.tests.test_config) + + def test_setup_registry_locale_negotiator_dottedname(self): + from pyramid.registry import Registry + from pyramid.interfaces import ILocaleNegotiator + reg = Registry() + config = self._makeOne(reg) + import pyramid.tests.test_config + config.setup_registry(locale_negotiator='pyramid.tests.test_config') + self.assertEqual(reg.queryUtility(ILocaleNegotiator), None) + config.commit() + utility = reg.getUtility(ILocaleNegotiator) + self.assertEqual(utility, pyramid.tests.test_config) + + def test_setup_registry_locale_negotiator(self): + from pyramid.registry import Registry + from pyramid.interfaces import ILocaleNegotiator + reg = Registry() + config = self._makeOne(reg) + negotiator = object() + config.setup_registry(locale_negotiator=negotiator) + self.assertEqual(reg.queryUtility(ILocaleNegotiator), None) + config.commit() + utility = reg.getUtility(ILocaleNegotiator) + self.assertEqual(utility, negotiator) + + def test_setup_registry_request_factory(self): + from pyramid.registry import Registry + from pyramid.interfaces import IRequestFactory + reg = Registry() + config = self._makeOne(reg) + factory = object() + config.setup_registry(request_factory=factory) + self.assertEqual(reg.queryUtility(IRequestFactory), None) + config.commit() + utility = reg.getUtility(IRequestFactory) + self.assertEqual(utility, factory) + + def test_setup_registry_response_factory(self): + from pyramid.registry import Registry + from pyramid.interfaces import IResponseFactory + reg = Registry() + config = self._makeOne(reg) + factory = lambda r: object() + config.setup_registry(response_factory=factory) + self.assertEqual(reg.queryUtility(IResponseFactory), None) + config.commit() + utility = reg.getUtility(IResponseFactory) + self.assertEqual(utility, factory) + + def test_setup_registry_request_factory_dottedname(self): + from pyramid.registry import Registry + from pyramid.interfaces import IRequestFactory + reg = Registry() + config = self._makeOne(reg) + import pyramid.tests.test_config + config.setup_registry(request_factory='pyramid.tests.test_config') + self.assertEqual(reg.queryUtility(IRequestFactory), None) + config.commit() + utility = reg.getUtility(IRequestFactory) + self.assertEqual(utility, pyramid.tests.test_config) + + def test_setup_registry_alternate_renderers(self): + from pyramid.registry import Registry + from pyramid.interfaces import IRendererFactory + renderer = object() + reg = Registry() + config = self._makeOne(reg) + config.setup_registry(renderers=[('yeah', renderer)]) + config.commit() + self.assertEqual(reg.getUtility(IRendererFactory, 'yeah'), + renderer) + + def test_setup_registry_default_permission(self): + from pyramid.registry import Registry + from pyramid.interfaces import IDefaultPermission + reg = Registry() + config = self._makeOne(reg) + config.setup_registry(default_permission='view') + config.commit() + self.assertEqual(reg.getUtility(IDefaultPermission), 'view') + + def test_setup_registry_includes(self): + from pyramid.registry import Registry + reg = Registry() + config = self._makeOne(reg) + settings = { + 'pyramid.includes': +"""pyramid.tests.test_config.dummy_include +pyramid.tests.test_config.dummy_include2""", + } + config.setup_registry(settings=settings) + self.assertTrue(reg.included) + self.assertTrue(reg.also_included) + + def test_setup_registry_includes_spaces(self): + from pyramid.registry import Registry + reg = Registry() + config = self._makeOne(reg) + settings = { + 'pyramid.includes': +"""pyramid.tests.test_config.dummy_include pyramid.tests.test_config.dummy_include2""", + } + config.setup_registry(settings=settings) + self.assertTrue(reg.included) + self.assertTrue(reg.also_included) + + def test_setup_registry_tweens(self): + from pyramid.interfaces import ITweens + from pyramid.registry import Registry + reg = Registry() + config = self._makeOne(reg) + settings = { + 'pyramid.tweens': + 'pyramid.tests.test_config.dummy_tween_factory' + } + config.setup_registry(settings=settings) + config.commit() + tweens = config.registry.getUtility(ITweens) + self.assertEqual( + tweens.explicit, + [('pyramid.tests.test_config.dummy_tween_factory', + dummy_tween_factory)]) + + def test_introspector_decorator(self): + inst = self._makeOne() + default = inst.introspector + self.assertTrue(hasattr(default, 'add')) + self.assertEqual(inst.introspector, inst.registry.introspector) + introspector = object() + inst.introspector = introspector + new = inst.introspector + self.assertTrue(new is introspector) + self.assertEqual(inst.introspector, inst.registry.introspector) + del inst.introspector + default = inst.introspector + self.assertFalse(default is new) + self.assertTrue(hasattr(default, 'add')) + + def test_make_wsgi_app(self): + import pyramid.config + from pyramid.router import Router + from pyramid.interfaces import IApplicationCreated + manager = DummyThreadLocalManager() + config = self._makeOne() + subscriber = self._registerEventListener(config, IApplicationCreated) + config.manager = manager + app = config.make_wsgi_app() + self.assertEqual(app.__class__, Router) + self.assertEqual(manager.pushed['registry'], config.registry) + self.assertEqual(manager.pushed['request'], None) + self.assertTrue(manager.popped) + self.assertEqual(pyramid.config.global_registries.last, app.registry) + self.assertEqual(len(subscriber), 1) + self.assertTrue(IApplicationCreated.providedBy(subscriber[0])) + pyramid.config.global_registries.empty() + + def test_include_with_dotted_name(self): + from pyramid.tests import test_config + config = self._makeOne() + config.include('pyramid.tests.test_config.dummy_include') + after = config.action_state + actions = after.actions + self.assertEqual(len(actions), 1) + action = after.actions[0] + self.assertEqual(action['discriminator'], 'discrim') + self.assertEqual(action['callable'], None) + self.assertEqual(action['args'], test_config) + + def test_include_with_python_callable(self): + from pyramid.tests import test_config + config = self._makeOne() + config.include(dummy_include) + after = config.action_state + actions = after.actions + self.assertEqual(len(actions), 1) + action = actions[0] + self.assertEqual(action['discriminator'], 'discrim') + self.assertEqual(action['callable'], None) + self.assertEqual(action['args'], test_config) + + def test_include_with_module_defaults_to_includeme(self): + from pyramid.tests import test_config + config = self._makeOne() + config.include('pyramid.tests.test_config') + after = config.action_state + actions = after.actions + self.assertEqual(len(actions), 1) + action = actions[0] + self.assertEqual(action['discriminator'], 'discrim') + self.assertEqual(action['callable'], None) + self.assertEqual(action['args'], test_config) + + def test_include_with_module_defaults_to_includeme_missing(self): + from pyramid.exceptions import ConfigurationError + config = self._makeOne() + self.assertRaises(ConfigurationError, config.include, 'pyramid.tests') + + def test_include_with_route_prefix(self): + root_config = self._makeOne(autocommit=True) + def dummy_subapp(config): + self.assertEqual(config.route_prefix, 'root') + root_config.include(dummy_subapp, route_prefix='root') + + def test_include_with_nested_route_prefix(self): + root_config = self._makeOne(autocommit=True, route_prefix='root') + def dummy_subapp2(config): + self.assertEqual(config.route_prefix, 'root/nested') + def dummy_subapp3(config): + self.assertEqual(config.route_prefix, 'root/nested/nested2') + config.include(dummy_subapp4) + def dummy_subapp4(config): + self.assertEqual(config.route_prefix, 'root/nested/nested2') + def dummy_subapp(config): + self.assertEqual(config.route_prefix, 'root/nested') + config.include(dummy_subapp2) + config.include(dummy_subapp3, route_prefix='nested2') + + root_config.include(dummy_subapp, route_prefix='nested') + + def test_include_with_missing_source_file(self): + from pyramid.exceptions import ConfigurationError + import inspect + config = self._makeOne() + class DummyInspect(object): + def getmodule(self, c): + return inspect.getmodule(c) + def getsourcefile(self, c): + return None + config.inspect = DummyInspect() + try: + config.include('pyramid.tests.test_config.dummy_include') + except ConfigurationError as e: + self.assertEqual( + e.args[0], + "No source file for module 'pyramid.tests.test_config' (.py " + "file must exist, refusing to use orphan .pyc or .pyo file).") + else: # pragma: no cover + raise AssertionError + + def test_include_constant_root_package(self): + from pyramid import tests + from pyramid.tests import test_config + config = self._makeOne(root_package=tests) + results = {} + def include(config): + results['package'] = config.package + results['root_package'] = config.root_package + config.include(include) + self.assertEqual(results['root_package'], tests) + self.assertEqual(results['package'], test_config) + + def test_include_threadlocals_active(self): + from pyramid.tests import test_config + from pyramid.threadlocal import get_current_registry + stack = [] + def include(config): + stack.append(get_current_registry()) + config = self._makeOne() + config.include(include) + self.assertTrue(stack[0] is config.registry) + + def test_action_branching_kw_is_None(self): + config = self._makeOne(autocommit=True) + self.assertEqual(config.action('discrim'), None) + + def test_action_branching_kw_is_not_None(self): + config = self._makeOne(autocommit=True) + self.assertEqual(config.action('discrim', kw={'a':1}), None) + + def test_action_autocommit_with_introspectables(self): + from pyramid.config.util import ActionInfo + config = self._makeOne(autocommit=True) + intr = DummyIntrospectable() + config.action('discrim', introspectables=(intr,)) + self.assertEqual(len(intr.registered), 1) + self.assertEqual(intr.registered[0][0], config.introspector) + self.assertEqual(intr.registered[0][1].__class__, ActionInfo) + + def test_action_autocommit_with_introspectables_introspection_off(self): + config = self._makeOne(autocommit=True) + config.introspection = False + intr = DummyIntrospectable() + config.action('discrim', introspectables=(intr,)) + self.assertEqual(len(intr.registered), 0) + + def test_action_branching_nonautocommit_with_config_info(self): + config = self._makeOne(autocommit=False) + config.info = 'abc' + state = DummyActionState() + state.autocommit = False + config.action_state = state + config.action('discrim', kw={'a':1}) + self.assertEqual( + state.actions, + [((), + {'args': (), + 'callable': None, + 'discriminator': 'discrim', + 'includepath': (), + 'info': 'abc', + 'introspectables': (), + 'kw': {'a': 1}, + 'order': 0})]) + + def test_action_branching_nonautocommit_without_config_info(self): + config = self._makeOne(autocommit=False) + config.info = '' + config._ainfo = ['z'] + state = DummyActionState() + config.action_state = state + state.autocommit = False + config.action('discrim', kw={'a':1}) + self.assertEqual( + state.actions, + [((), + {'args': (), + 'callable': None, + 'discriminator': 'discrim', + 'includepath': (), + 'info': 'z', + 'introspectables': (), + 'kw': {'a': 1}, + 'order': 0})]) + + def test_action_branching_nonautocommit_with_introspectables(self): + config = self._makeOne(autocommit=False) + config.info = '' + config._ainfo = [] + state = DummyActionState() + config.action_state = state + state.autocommit = False + intr = DummyIntrospectable() + config.action('discrim', introspectables=(intr,)) + self.assertEqual( + state.actions[0][1]['introspectables'], (intr,)) + + def test_action_nonautocommit_with_introspectables_introspection_off(self): + config = self._makeOne(autocommit=False) + config.info = '' + config._ainfo = [] + config.introspection = False + state = DummyActionState() + config.action_state = state + state.autocommit = False + intr = DummyIntrospectable() + config.action('discrim', introspectables=(intr,)) + self.assertEqual( + state.actions[0][1]['introspectables'], ()) + + def test_scan_integration(self): + from zope.interface import alsoProvides + from pyramid.interfaces import IRequest + from pyramid.view import render_view_to_response + import pyramid.tests.test_config.pkgs.scannable as package + config = self._makeOne(autocommit=True) + config.scan(package) + + ctx = DummyContext() + req = DummyRequest() + alsoProvides(req, IRequest) + req.registry = config.registry + + req.method = 'GET' + result = render_view_to_response(ctx, req, '') + self.assertEqual(result, 'grokked') + + req.method = 'POST' + result = render_view_to_response(ctx, req, '') + self.assertEqual(result, 'grokked_post') + + result= render_view_to_response(ctx, req, 'grokked_class') + self.assertEqual(result, 'grokked_class') + + result= render_view_to_response(ctx, req, 'grokked_instance') + self.assertEqual(result, 'grokked_instance') + + result= render_view_to_response(ctx, req, 'oldstyle_grokked_class') + self.assertEqual(result, 'oldstyle_grokked_class') + + req.method = 'GET' + result = render_view_to_response(ctx, req, 'another') + self.assertEqual(result, 'another_grokked') + + req.method = 'POST' + result = render_view_to_response(ctx, req, 'another') + self.assertEqual(result, 'another_grokked_post') + + result= render_view_to_response(ctx, req, 'another_grokked_class') + self.assertEqual(result, 'another_grokked_class') + + result= render_view_to_response(ctx, req, 'another_grokked_instance') + self.assertEqual(result, 'another_grokked_instance') + + result= render_view_to_response(ctx, req, + 'another_oldstyle_grokked_class') + self.assertEqual(result, 'another_oldstyle_grokked_class') + + result = render_view_to_response(ctx, req, 'stacked1') + self.assertEqual(result, 'stacked') + + result = render_view_to_response(ctx, req, 'stacked2') + self.assertEqual(result, 'stacked') + + result = render_view_to_response(ctx, req, 'another_stacked1') + self.assertEqual(result, 'another_stacked') + + result = render_view_to_response(ctx, req, 'another_stacked2') + self.assertEqual(result, 'another_stacked') + + result = render_view_to_response(ctx, req, 'stacked_class1') + self.assertEqual(result, 'stacked_class') + + result = render_view_to_response(ctx, req, 'stacked_class2') + self.assertEqual(result, 'stacked_class') + + result = render_view_to_response(ctx, req, 'another_stacked_class1') + self.assertEqual(result, 'another_stacked_class') + + result = render_view_to_response(ctx, req, 'another_stacked_class2') + self.assertEqual(result, 'another_stacked_class') + + # NB: on Jython, a class without an __init__ apparently accepts + # any number of arguments without raising a TypeError, so the next + # assertion may fail there. We don't support Jython at the moment, + # this is just a note to a future self. + + self.assertRaises(TypeError, + render_view_to_response, ctx, req, 'basemethod') + + result = render_view_to_response(ctx, req, 'method1') + self.assertEqual(result, 'method1') + + result = render_view_to_response(ctx, req, 'method2') + self.assertEqual(result, 'method2') + + result = render_view_to_response(ctx, req, 'stacked_method1') + self.assertEqual(result, 'stacked_method') + + result = render_view_to_response(ctx, req, 'stacked_method2') + self.assertEqual(result, 'stacked_method') + + result = render_view_to_response(ctx, req, 'subpackage_init') + self.assertEqual(result, 'subpackage_init') + + result = render_view_to_response(ctx, req, 'subpackage_notinit') + self.assertEqual(result, 'subpackage_notinit') + + result = render_view_to_response(ctx, req, 'subsubpackage_init') + self.assertEqual(result, 'subsubpackage_init') + + result = render_view_to_response(ctx, req, 'pod_notinit') + self.assertEqual(result, None) + + def test_scan_integration_with_ignore(self): + from zope.interface import alsoProvides + from pyramid.interfaces import IRequest + from pyramid.view import render_view_to_response + import pyramid.tests.test_config.pkgs.scannable as package + config = self._makeOne(autocommit=True) + config.scan(package, + ignore='pyramid.tests.test_config.pkgs.scannable.another') + + ctx = DummyContext() + req = DummyRequest() + alsoProvides(req, IRequest) + req.registry = config.registry + + req.method = 'GET' + result = render_view_to_response(ctx, req, '') + self.assertEqual(result, 'grokked') + + # ignored + v = render_view_to_response(ctx, req, 'another_stacked_class2') + self.assertEqual(v, None) + + def test_scan_integration_dottedname_package(self): + from zope.interface import alsoProvides + from pyramid.interfaces import IRequest + from pyramid.view import render_view_to_response + config = self._makeOne(autocommit=True) + config.scan('pyramid.tests.test_config.pkgs.scannable') + + ctx = DummyContext() + req = DummyRequest() + alsoProvides(req, IRequest) + req.registry = config.registry + + req.method = 'GET' + result = render_view_to_response(ctx, req, '') + self.assertEqual(result, 'grokked') + + def test_scan_integration_with_extra_kw(self): + config = self._makeOne(autocommit=True) + config.scan('pyramid.tests.test_config.pkgs.scanextrakw', a=1) + self.assertEqual(config.a, 1) + + def test_scan_integration_with_onerror(self): + # fancy sys.path manipulation here to appease "setup.py test" which + # fails miserably when it can't import something in the package + import sys + try: + here = os.path.dirname(__file__) + path = os.path.join(here, 'path') + sys.path.append(path) + config = self._makeOne(autocommit=True) + class FooException(Exception): + pass + def onerror(name): + raise FooException + self.assertRaises(FooException, config.scan, 'scanerror', + onerror=onerror) + finally: + sys.path.remove(path) + + def test_scan_integration_conflict(self): + from pyramid.tests.test_config.pkgs import selfscan + from pyramid.config import Configurator + c = Configurator() + c.scan(selfscan) + c.scan(selfscan) + try: + c.commit() + except ConfigurationConflictError as why: + def scanconflicts(e): + conflicts = e._conflicts.values() + for conflict in conflicts: + for confinst in conflict: + yield confinst.src + which = list(scanconflicts(why)) + self.assertEqual(len(which), 4) + self.assertTrue("@view_config(renderer='string')" in which) + self.assertTrue("@view_config(name='two', renderer='string')" in + which) + + @skip_on('py3') + def test_hook_zca(self): + from zope.component import getSiteManager + def foo(): + '123' + try: + config = self._makeOne() + config.hook_zca() + config.begin() + sm = getSiteManager() + self.assertEqual(sm, config.registry) + finally: + getSiteManager.reset() + + @skip_on('py3') + def test_unhook_zca(self): + from zope.component import getSiteManager + def foo(): + '123' + try: + getSiteManager.sethook(foo) + config = self._makeOne() + config.unhook_zca() + sm = getSiteManager() + self.assertNotEqual(sm, '123') + finally: + getSiteManager.reset() + + def test_commit_conflict_simple(self): + config = self._makeOne() + def view1(request): pass + def view2(request): pass + config.add_view(view1) + config.add_view(view2) + self.assertRaises(ConfigurationConflictError, config.commit) + + def test_commit_conflict_resolved_with_include(self): + config = self._makeOne() + def view1(request): pass + def view2(request): pass + def includeme(config): + config.add_view(view2) + config.add_view(view1) + config.include(includeme) + config.commit() + registeredview = self._getViewCallable(config) + self.assertEqual(registeredview.__name__, 'view1') + + def test_commit_conflict_with_two_includes(self): + config = self._makeOne() + def view1(request): pass + def view2(request): pass + def includeme1(config): + config.add_view(view1) + def includeme2(config): + config.add_view(view2) + config.include(includeme1) + config.include(includeme2) + try: + config.commit() + except ConfigurationConflictError as why: + c1, c2 = _conflictFunctions(why) + self.assertEqual(c1, 'includeme1') + self.assertEqual(c2, 'includeme2') + else: #pragma: no cover + raise AssertionError + + def test_commit_conflict_resolved_with_two_includes_and_local(self): + config = self._makeOne() + def view1(request): pass + def view2(request): pass + def view3(request): pass + def includeme1(config): + config.add_view(view1) + def includeme2(config): + config.add_view(view2) + config.include(includeme1) + config.include(includeme2) + config.add_view(view3) + config.commit() + registeredview = self._getViewCallable(config) + self.assertEqual(registeredview.__name__, 'view3') + + def test_autocommit_no_conflicts(self): + from pyramid.renderers import null_renderer + config = self._makeOne(autocommit=True) + def view1(request): pass + def view2(request): pass + def view3(request): pass + config.add_view(view1, renderer=null_renderer) + config.add_view(view2, renderer=null_renderer) + config.add_view(view3, renderer=null_renderer) + config.commit() + registeredview = self._getViewCallable(config) + self.assertEqual(registeredview.__name__, 'view3') + + def test_conflict_set_notfound_view(self): + config = self._makeOne() + def view1(request): pass + def view2(request): pass + config.set_notfound_view(view1) + config.set_notfound_view(view2) + try: + config.commit() + except ConfigurationConflictError as why: + c1, c2 = _conflictFunctions(why) + self.assertEqual(c1, 'test_conflict_set_notfound_view') + self.assertEqual(c2, 'test_conflict_set_notfound_view') + else: # pragma: no cover + raise AssertionError + + def test_conflict_set_forbidden_view(self): + config = self._makeOne() + def view1(request): pass + def view2(request): pass + config.set_forbidden_view(view1) + config.set_forbidden_view(view2) + try: + config.commit() + except ConfigurationConflictError as why: + c1, c2 = _conflictFunctions(why) + self.assertEqual(c1, 'test_conflict_set_forbidden_view') + self.assertEqual(c2, 'test_conflict_set_forbidden_view') + else: # pragma: no cover + raise AssertionError + + def test___getattr__missing_when_directives_exist(self): + config = self._makeOne() + directives = {} + config.registry._directives = directives + self.assertRaises(AttributeError, config.__getattr__, 'wontexist') + + def test___getattr__missing_when_directives_dont_exist(self): + config = self._makeOne() + self.assertRaises(AttributeError, config.__getattr__, 'wontexist') + + def test___getattr__matches(self): + config = self._makeOne() + def foo(config): pass + directives = {'foo':(foo, True)} + config.registry._directives = directives + foo_meth = config.foo + self.assertTrue(getattr(foo_meth, im_func).__docobj__ is foo) + + def test___getattr__matches_no_action_wrap(self): + config = self._makeOne() + def foo(config): pass + directives = {'foo':(foo, False)} + config.registry._directives = directives + foo_meth = config.foo + self.assertTrue(getattr(foo_meth, im_func) is foo) + +class TestConfigurator_add_directive(unittest.TestCase): + + def setUp(self): + from pyramid.config import Configurator + self.config = Configurator() + + def test_extend_with_dotted_name(self): + from pyramid.tests import test_config + config = self.config + config.add_directive( + 'dummy_extend', 'pyramid.tests.test_config.dummy_extend') + self.assertTrue(hasattr(config, 'dummy_extend')) + config.dummy_extend('discrim') + after = config.action_state + action = after.actions[-1] + self.assertEqual(action['discriminator'], 'discrim') + self.assertEqual(action['callable'], None) + self.assertEqual(action['args'], test_config) + + def test_add_directive_with_partial(self): + from pyramid.tests import test_config + config = self.config + config.add_directive( + 'dummy_partial', 'pyramid.tests.test_config.dummy_partial') + self.assertTrue(hasattr(config, 'dummy_partial')) + config.dummy_partial() + after = config.action_state + action = after.actions[-1] + self.assertEqual(action['discriminator'], 'partial') + self.assertEqual(action['callable'], None) + self.assertEqual(action['args'], test_config) + + def test_add_directive_with_custom_callable(self): + from pyramid.tests import test_config + config = self.config + config.add_directive( + 'dummy_callable', 'pyramid.tests.test_config.dummy_callable') + self.assertTrue(hasattr(config, 'dummy_callable')) + config.dummy_callable('discrim') + after = config.action_state + action = after.actions[-1] + self.assertEqual(action['discriminator'], 'discrim') + self.assertEqual(action['callable'], None) + self.assertEqual(action['args'], test_config) + + def test_extend_with_python_callable(self): + from pyramid.tests import test_config + config = self.config + config.add_directive( + 'dummy_extend', dummy_extend) + self.assertTrue(hasattr(config, 'dummy_extend')) + config.dummy_extend('discrim') + after = config.action_state + action = after.actions[-1] + self.assertEqual(action['discriminator'], 'discrim') + self.assertEqual(action['callable'], None) + self.assertEqual(action['args'], test_config) + + def test_extend_same_name_doesnt_conflict(self): + config = self.config + config.add_directive( + 'dummy_extend', dummy_extend) + config.add_directive( + 'dummy_extend', dummy_extend2) + self.assertTrue(hasattr(config, 'dummy_extend')) + config.dummy_extend('discrim') + after = config.action_state + action = after.actions[-1] + self.assertEqual(action['discriminator'], 'discrim') + self.assertEqual(action['callable'], None) + self.assertEqual(action['args'], config.registry) + + def test_extend_action_method_successful(self): + config = self.config + config.add_directive( + 'dummy_extend', dummy_extend) + config.dummy_extend('discrim') + config.dummy_extend('discrim') + self.assertRaises(ConfigurationConflictError, config.commit) + + def test_directive_persists_across_configurator_creations(self): + config = self.config + config.add_directive('dummy_extend', dummy_extend) + config2 = config.with_package('pyramid.tests') + config2.dummy_extend('discrim') + after = config2.action_state + actions = after.actions + self.assertEqual(len(actions), 1) + action = actions[0] + self.assertEqual(action['discriminator'], 'discrim') + self.assertEqual(action['callable'], None) + self.assertEqual(action['args'], config2.package) + +class TestConfigurator__add_predicate(unittest.TestCase): + def _makeOne(self): + from pyramid.config import Configurator + return Configurator() + + def test_factory_as_object(self): + config = self._makeOne() + + def _fakeAction(discriminator, callable=None, args=(), kw=None, + order=0, introspectables=(), **extra): + self.assertEqual(len(introspectables), 1) + self.assertEqual(introspectables[0]['name'], 'testing') + self.assertEqual(introspectables[0]['factory'], DummyPredicate) + + config.action = _fakeAction + config._add_predicate('route', 'testing', DummyPredicate) + + def test_factory_as_dotted_name(self): + config = self._makeOne() + + def _fakeAction(discriminator, callable=None, args=(), + kw=None, order=0, introspectables=(), **extra): + self.assertEqual(len(introspectables), 1) + self.assertEqual(introspectables[0]['name'], 'testing') + self.assertEqual(introspectables[0]['factory'], DummyPredicate) + + config.action = _fakeAction + config._add_predicate( + 'route', + 'testing', + 'pyramid.tests.test_config.test_init.DummyPredicate' + ) + +class TestActionState(unittest.TestCase): + def _makeOne(self): + from pyramid.config import ActionState + return ActionState() + + def test_it(self): + c = self._makeOne() + self.assertEqual(c.actions, []) + + def test_action_simple(self): + from pyramid.tests.test_config import dummyfactory as f + c = self._makeOne() + c.actions = [] + c.action(1, f, (1,), {'x':1}) + self.assertEqual( + c.actions, + [{'args': (1,), + 'callable': f, + 'discriminator': 1, + 'includepath': (), + 'info': None, + 'introspectables': (), + 'kw': {'x': 1}, + 'order': 0}]) + c.action(None) + self.assertEqual( + c.actions, + [{'args': (1,), + 'callable': f, + 'discriminator': 1, + 'includepath': (), + 'info': None, + 'introspectables': (), + 'kw': {'x': 1}, + 'order': 0}, + + {'args': (), + 'callable': None, + 'discriminator': None, + 'includepath': (), + 'info': None, + 'introspectables': (), + 'kw': {}, + 'order': 0},]) + + def test_action_with_includepath(self): + c = self._makeOne() + c.actions = [] + c.action(None, includepath=('abc',)) + self.assertEqual( + c.actions, + [{'args': (), + 'callable': None, + 'discriminator': None, + 'includepath': ('abc',), + 'info': None, + 'introspectables': (), + 'kw': {}, + 'order': 0}]) + + def test_action_with_info(self): + c = self._makeOne() + c.action(None, info='abc') + self.assertEqual( + c.actions, + [{'args': (), + 'callable': None, + 'discriminator': None, + 'includepath': (), + 'info': 'abc', + 'introspectables': (), + 'kw': {}, + 'order': 0}]) + + def test_action_with_includepath_and_info(self): + c = self._makeOne() + c.action(None, includepath=('spec',), info='bleh') + self.assertEqual( + c.actions, + [{'args': (), + 'callable': None, + 'discriminator': None, + 'includepath': ('spec',), + 'info': 'bleh', + 'introspectables': (), + 'kw': {}, + 'order': 0}]) + + def test_action_with_order(self): + c = self._makeOne() + c.actions = [] + c.action(None, order=99999) + self.assertEqual( + c.actions, + [{'args': (), + 'callable': None, + 'discriminator': None, + 'includepath': (), + 'info': None, + 'introspectables': (), + 'kw': {}, + 'order': 99999}]) + + def test_action_with_introspectables(self): + c = self._makeOne() + c.actions = [] + intr = DummyIntrospectable() + c.action(None, introspectables=(intr,)) + self.assertEqual( + c.actions, + [{'args': (), + 'callable': None, + 'discriminator': None, + 'includepath': (), + 'info': None, + 'introspectables': (intr,), + 'kw': {}, + 'order': 0}]) + + def test_processSpec(self): + c = self._makeOne() + self.assertTrue(c.processSpec('spec')) + self.assertFalse(c.processSpec('spec')) + + def test_execute_actions_tuples(self): + output = [] + def f(*a, **k): + output.append((a, k)) + c = self._makeOne() + c.actions = [ + (1, f, (1,)), + (1, f, (11,), {}, ('x', )), + (2, f, (2,)), + (None, None), + ] + c.execute_actions() + self.assertEqual(output, [((1,), {}), ((2,), {})]) + + def test_execute_actions_dicts(self): + output = [] + def f(*a, **k): + output.append((a, k)) + c = self._makeOne() + c.actions = [ + {'discriminator':1, 'callable':f, 'args':(1,), 'kw':{}, + 'order':0, 'includepath':(), 'info':None, + 'introspectables':()}, + {'discriminator':1, 'callable':f, 'args':(11,), 'kw':{}, + 'includepath':('x',), 'order': 0, 'info':None, + 'introspectables':()}, + {'discriminator':2, 'callable':f, 'args':(2,), 'kw':{}, + 'order':0, 'includepath':(), 'info':None, + 'introspectables':()}, + {'discriminator':None, 'callable':None, 'args':(), 'kw':{}, + 'order':0, 'includepath':(), 'info':None, + 'introspectables':()}, + ] + c.execute_actions() + self.assertEqual(output, [((1,), {}), ((2,), {})]) + + def test_execute_actions_with_introspectables(self): + output = [] + def f(*a, **k): + output.append((a, k)) + c = self._makeOne() + intr = DummyIntrospectable() + c.actions = [ + {'discriminator':1, 'callable':f, 'args':(1,), 'kw':{}, + 'order':0, 'includepath':(), 'info':None, + 'introspectables':(intr,)}, + ] + introspector = object() + c.execute_actions(introspector=introspector) + self.assertEqual(output, [((1,), {})]) + self.assertEqual(intr.registered, [(introspector, None)]) + + def test_execute_actions_with_introspectable_no_callable(self): + c = self._makeOne() + intr = DummyIntrospectable() + c.actions = [ + {'discriminator':1, 'callable':None, 'args':(1,), 'kw':{}, + 'order':0, 'includepath':(), 'info':None, + 'introspectables':(intr,)}, + ] + introspector = object() + c.execute_actions(introspector=introspector) + self.assertEqual(intr.registered, [(introspector, None)]) + + def test_execute_actions_error(self): + output = [] + def f(*a, **k): + output.append(('f', a, k)) + def bad(): + raise NotImplementedError + c = self._makeOne() + c.actions = [ + (1, f, (1,)), + (1, f, (11,), {}, ('x', )), + (2, f, (2,)), + (3, bad, (), {}, (), 'oops') + ] + self.assertRaises(ConfigurationExecutionError, c.execute_actions) + self.assertEqual(output, [('f', (1,), {}), ('f', (2,), {})]) + + def test_reentrant_action(self): + output = [] + c = self._makeOne() + def f(*a, **k): + output.append(('f', a, k)) + c.actions.append((3, g, (8,), {})) + def g(*a, **k): + output.append(('g', a, k)) + c.actions = [ + (1, f, (1,)), + ] + c.execute_actions() + self.assertEqual(output, [('f', (1,), {}), ('g', (8,), {})]) + + def test_reentrant_action_with_deferred_discriminator(self): + # see https://github.com/Pylons/pyramid/issues/2697 + from pyramid.registry import Deferred + output = [] + c = self._makeOne() + def f(*a, **k): + output.append(('f', a, k)) + c.actions.append((4, g, (4,), {}, (), None, 2)) + def g(*a, **k): + output.append(('g', a, k)) + def h(*a, **k): + output.append(('h', a, k)) + def discrim(): + self.assertEqual(output, [('f', (1,), {}), ('g', (2,), {})]) + return 3 + d = Deferred(discrim) + c.actions = [ + (d, h, (3,), {}, (), None, 1), # order 1 + (1, f, (1,)), # order 0 + (2, g, (2,)), # order 0 + ] + c.execute_actions() + self.assertEqual(output, [ + ('f', (1,), {}), ('g', (2,), {}), ('h', (3,), {}), ('g', (4,), {})]) + + def test_reentrant_action_error(self): + from pyramid.exceptions import ConfigurationError + c = self._makeOne() + def f(*a, **k): + c.actions.append((3, g, (8,), {}, (), None, -1)) + def g(*a, **k): pass + c.actions = [ + (1, f, (1,)), + ] + self.assertRaises(ConfigurationError, c.execute_actions) + + def test_reentrant_action_without_clear(self): + c = self._makeOne() + def f(*a, **k): + c.actions.append((3, g, (8,))) + def g(*a, **k): pass + c.actions = [ + (1, f, (1,)), + ] + c.execute_actions(clear=False) + self.assertEqual(c.actions, [ + (1, f, (1,)), + (3, g, (8,)), + ]) + + def test_executing_conflicting_action_across_orders(self): + from pyramid.exceptions import ConfigurationConflictError + c = self._makeOne() + def f(*a, **k): pass + def g(*a, **k): pass + c.actions = [ + (1, f, (1,), {}, (), None, -1), + (1, g, (2,)), + ] + self.assertRaises(ConfigurationConflictError, c.execute_actions) + + def test_executing_conflicting_action_across_reentrant_orders(self): + from pyramid.exceptions import ConfigurationConflictError + c = self._makeOne() + def f(*a, **k): + c.actions.append((1, g, (8,))) + def g(*a, **k): pass + c.actions = [ + (1, f, (1,), {}, (), None, -1), + ] + self.assertRaises(ConfigurationConflictError, c.execute_actions) + +class Test_reentrant_action_functional(unittest.TestCase): + def _makeConfigurator(self, *arg, **kw): + from pyramid.config import Configurator + config = Configurator(*arg, **kw) + return config + + def test_functional(self): + def add_auto_route(config, name, view): + def register(): + config.add_view(route_name=name, view=view) + config.add_route(name, '/' + name) + config.action( + ('auto route', name), register, order=-30 + ) + config = self._makeConfigurator() + config.add_directive('add_auto_route', add_auto_route) + def my_view(request): return request.response + config.add_auto_route('foo', my_view) + config.commit() + from pyramid.interfaces import IRoutesMapper + mapper = config.registry.getUtility(IRoutesMapper) + routes = mapper.get_routes() + route = routes[0] + self.assertEqual(len(routes), 1) + self.assertEqual(route.name, 'foo') + self.assertEqual(route.path, '/foo') + + def test_deferred_discriminator(self): + # see https://github.com/Pylons/pyramid/issues/2697 + from pyramid.config import PHASE0_CONFIG + config = self._makeConfigurator() + def deriver(view, info): return view + deriver.options = ('foo',) + config.add_view_deriver(deriver, 'foo_view') + # add_view uses a deferred discriminator and will fail if executed + # prior to add_view_deriver executing its action + config.add_view(lambda r: r.response, name='', foo=1) + def dummy_action(): + # trigger a re-entrant action + config.action(None, lambda: None) + config.action(None, dummy_action, order=PHASE0_CONFIG) + config.commit() + +class Test_resolveConflicts(unittest.TestCase): + def _callFUT(self, actions): + from pyramid.config import resolveConflicts + return resolveConflicts(actions) + + def test_it_success_tuples(self): + from pyramid.tests.test_config import dummyfactory as f + result = self._callFUT([ + (None, f), + (1, f, (1,), {}, (), 'first'), + (1, f, (2,), {}, ('x',), 'second'), + (1, f, (3,), {}, ('y',), 'third'), + (4, f, (4,), {}, ('y',), 'should be last', 99999), + (3, f, (3,), {}, ('y',)), + (None, f, (5,), {}, ('y',)), + ]) + result = list(result) + self.assertEqual( + result, + [{'info': None, + 'args': (), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': None, + 'includepath': (), + 'order': 0}, + + {'info': 'first', + 'args': (1,), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': 1, + 'includepath': (), + 'order': 0}, + + {'info': None, + 'args': (3,), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': 3, + 'includepath': ('y',), + 'order': 0}, + + {'info': None, + 'args': (5,), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': None, + 'includepath': ('y',), + 'order': 0}, + + {'info': 'should be last', + 'args': (4,), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': 4, + 'includepath': ('y',), + 'order': 99999} + ] + ) + + def test_it_success_dicts(self): + from pyramid.tests.test_config import dummyfactory as f + result = self._callFUT([ + (None, f), + (1, f, (1,), {}, (), 'first'), + (1, f, (2,), {}, ('x',), 'second'), + (1, f, (3,), {}, ('y',), 'third'), + (4, f, (4,), {}, ('y',), 'should be last', 99999), + (3, f, (3,), {}, ('y',)), + (None, f, (5,), {}, ('y',)), + ]) + result = list(result) + self.assertEqual( + result, + [{'info': None, + 'args': (), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': None, + 'includepath': (), + 'order': 0}, + + {'info': 'first', + 'args': (1,), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': 1, + 'includepath': (), + 'order': 0}, + + {'info': None, + 'args': (3,), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': 3, + 'includepath': ('y',), + 'order': 0}, + + {'info': None, + 'args': (5,), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': None, + 'includepath': ('y',), + 'order': 0}, + + {'info': 'should be last', + 'args': (4,), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': 4, + 'includepath': ('y',), + 'order': 99999} + ] + ) + + def test_it_conflict(self): + from pyramid.tests.test_config import dummyfactory as f + result = self._callFUT([ + (None, f), + (1, f, (2,), {}, ('x',), 'eek'), # will conflict + (1, f, (3,), {}, ('y',), 'ack'), # will conflict + (4, f, (4,), {}, ('y',)), + (3, f, (3,), {}, ('y',)), + (None, f, (5,), {}, ('y',)), + ]) + self.assertRaises(ConfigurationConflictError, list, result) + + def test_it_with_actions_grouped_by_order(self): + from pyramid.tests.test_config import dummyfactory as f + result = self._callFUT([ + (None, f), # X + (1, f, (1,), {}, (), 'third', 10), # X + (1, f, (2,), {}, ('x',), 'fourth', 10), + (1, f, (3,), {}, ('y',), 'fifth', 10), + (2, f, (1,), {}, (), 'sixth', 10), # X + (3, f, (1,), {}, (), 'seventh', 10), # X + (5, f, (4,), {}, ('y',), 'eighth', 99999), # X + (4, f, (3,), {}, (), 'first', 5), # X + (4, f, (5,), {}, ('y',), 'second', 5), + ]) + result = list(result) + self.assertEqual(len(result), 6) + # resolved actions should be grouped by (order, i) + self.assertEqual( + result, + [{'info': None, + 'args': (), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': None, + 'includepath': (), + 'order': 0}, + + {'info': 'first', + 'args': (3,), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': 4, + 'includepath': (), + 'order': 5}, + + {'info': 'third', + 'args': (1,), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': 1, + 'includepath': (), + 'order': 10}, + + {'info': 'sixth', + 'args': (1,), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': 2, + 'includepath': (), + 'order': 10}, + + {'info': 'seventh', + 'args': (1,), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': 3, + 'includepath': (), + 'order': 10}, + + {'info': 'eighth', + 'args': (4,), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': 5, + 'includepath': ('y',), + 'order': 99999} + ] + ) + + def test_override_success_across_orders(self): + from pyramid.tests.test_config import dummyfactory as f + result = self._callFUT([ + (1, f, (2,), {}, ('x',), 'eek', 0), + (1, f, (3,), {}, ('x', 'y'), 'ack', 10), + ]) + result = list(result) + self.assertEqual(result, [ + {'info': 'eek', + 'args': (2,), + 'callable': f, + 'introspectables': (), + 'kw': {}, + 'discriminator': 1, + 'includepath': ('x',), + 'order': 0}, + ]) + + def test_conflicts_across_orders(self): + from pyramid.tests.test_config import dummyfactory as f + result = self._callFUT([ + (1, f, (2,), {}, ('x', 'y'), 'eek', 0), + (1, f, (3,), {}, ('x'), 'ack', 10), + ]) + self.assertRaises(ConfigurationConflictError, list, result) + +class TestGlobalRegistriesIntegration(unittest.TestCase): + def setUp(self): + from pyramid.config import global_registries + global_registries.empty() + + tearDown = setUp + + def _makeConfigurator(self, *arg, **kw): + from pyramid.config import Configurator + config = Configurator(*arg, **kw) + return config + + def test_global_registries_empty(self): + from pyramid.config import global_registries + self.assertEqual(global_registries.last, None) + + def test_global_registries(self): + from pyramid.config import global_registries + config1 = self._makeConfigurator() + config1.make_wsgi_app() + self.assertEqual(global_registries.last, config1.registry) + config2 = self._makeConfigurator() + config2.make_wsgi_app() + self.assertEqual(global_registries.last, config2.registry) + self.assertEqual(list(global_registries), + [config1.registry, config2.registry]) + global_registries.remove(config2.registry) + self.assertEqual(global_registries.last, config1.registry) + +class DummyRequest: + subpath = () + matchdict = None + request_iface = IRequest + def __init__(self, environ=None): + if environ is None: + environ = {} + self.environ = environ + self.params = {} + self.cookies = {} + +class DummyThreadLocalManager(object): + def __init__(self): + self.pushed = {'registry': None, 'request': None} + self.popped = False + def push(self, d): + self.pushed = d + def get(self): + return self.pushed + def pop(self): + self.popped = True + +from zope.interface import implementer +@implementer(IDummy) +class DummyEvent: + pass + +class DummyRegistry(object): + def __init__(self, adaptation=None, util=None): + self.utilities = [] + self.adapters = [] + self.adaptation = adaptation + self.util = util + def subscribers(self, events, name): + self.events = events + return events + def registerUtility(self, *arg, **kw): + self.utilities.append((arg, kw)) + def registerAdapter(self, *arg, **kw): + self.adapters.append((arg, kw)) + def queryAdapter(self, *arg, **kw): + return self.adaptation + def queryUtility(self, *arg, **kw): + return self.util + +from zope.interface import Interface +class IOther(Interface): + pass + +def _conflictFunctions(e): + conflicts = e._conflicts.values() + for conflict in conflicts: + for confinst in conflict: + yield confinst.function + +class DummyActionState(object): + autocommit = False + info = '' + def __init__(self): + self.actions = [] + def action(self, *arg, **kw): + self.actions.append((arg, kw)) + +class DummyIntrospectable(object): + def __init__(self): + self.registered = [] + def register(self, introspector, action_info): + self.registered.append((introspector, action_info)) + +class DummyPredicate(object): + pass diff --git a/src/pyramid/tests/test_config/test_rendering.py b/src/pyramid/tests/test_config/test_rendering.py new file mode 100644 index 000000000..cede64d3a --- /dev/null +++ b/src/pyramid/tests/test_config/test_rendering.py @@ -0,0 +1,34 @@ +import unittest + +class TestRenderingConfiguratorMixin(unittest.TestCase): + def _makeOne(self, *arg, **kw): + from pyramid.config import Configurator + config = Configurator(*arg, **kw) + return config + + def test_add_default_renderers(self): + from pyramid.config.rendering import DEFAULT_RENDERERS + from pyramid.interfaces import IRendererFactory + config = self._makeOne(autocommit=True) + config.add_default_renderers() + for name, impl in DEFAULT_RENDERERS: + self.assertTrue( + config.registry.queryUtility(IRendererFactory, name) is not None + ) + + def test_add_renderer(self): + from pyramid.interfaces import IRendererFactory + config = self._makeOne(autocommit=True) + renderer = object() + config.add_renderer('name', renderer) + self.assertEqual(config.registry.getUtility(IRendererFactory, 'name'), + renderer) + + def test_add_renderer_dottedname_factory(self): + from pyramid.interfaces import IRendererFactory + config = self._makeOne(autocommit=True) + import pyramid.tests.test_config + config.add_renderer('name', 'pyramid.tests.test_config') + self.assertEqual(config.registry.getUtility(IRendererFactory, 'name'), + pyramid.tests.test_config) + diff --git a/src/pyramid/tests/test_config/test_routes.py b/src/pyramid/tests/test_config/test_routes.py new file mode 100644 index 000000000..9f4ce9bc6 --- /dev/null +++ b/src/pyramid/tests/test_config/test_routes.py @@ -0,0 +1,297 @@ +import unittest + +from pyramid.tests.test_config import dummyfactory +from pyramid.tests.test_config import DummyContext +from pyramid.compat import text_ + +class RoutesConfiguratorMixinTests(unittest.TestCase): + def _makeOne(self, *arg, **kw): + from pyramid.config import Configurator + config = Configurator(*arg, **kw) + return config + + def _assertRoute(self, config, name, path, num_predicates=0): + from pyramid.interfaces import IRoutesMapper + mapper = config.registry.getUtility(IRoutesMapper) + routes = mapper.get_routes() + route = routes[0] + self.assertEqual(len(routes), 1) + self.assertEqual(route.name, name) + self.assertEqual(route.path, path) + self.assertEqual(len(routes[0].predicates), num_predicates) + return route + + def _makeRequest(self, config): + request = DummyRequest() + request.registry = config.registry + return request + + def test_get_routes_mapper_not_yet_registered(self): + config = self._makeOne() + mapper = config.get_routes_mapper() + self.assertEqual(mapper.routelist, []) + + def test_get_routes_mapper_already_registered(self): + from pyramid.interfaces import IRoutesMapper + config = self._makeOne() + mapper = object() + config.registry.registerUtility(mapper, IRoutesMapper) + result = config.get_routes_mapper() + self.assertEqual(result, mapper) + + def test_add_route_defaults(self): + config = self._makeOne(autocommit=True) + config.add_route('name', 'path') + self._assertRoute(config, 'name', 'path') + + def test_add_route_with_route_prefix(self): + config = self._makeOne(autocommit=True) + config.route_prefix = 'root' + config.add_route('name', 'path') + self._assertRoute(config, 'name', 'root/path') + + def test_add_route_discriminator(self): + config = self._makeOne() + config.add_route('name', 'path') + self.assertEqual(config.action_state.actions[-1]['discriminator'], + ('route', 'name')) + + def test_add_route_with_factory(self): + config = self._makeOne(autocommit=True) + factory = object() + config.add_route('name', 'path', factory=factory) + route = self._assertRoute(config, 'name', 'path') + self.assertEqual(route.factory, factory) + + def test_add_route_with_static(self): + config = self._makeOne(autocommit=True) + config.add_route('name', 'path/{foo}', static=True) + mapper = config.get_routes_mapper() + self.assertEqual(len(mapper.get_routes()), 0) + self.assertEqual(mapper.generate('name', {"foo":"a"}), '/path/a') + + def test_add_route_with_factory_dottedname(self): + config = self._makeOne(autocommit=True) + config.add_route( + 'name', 'path', + factory='pyramid.tests.test_config.dummyfactory') + route = self._assertRoute(config, 'name', 'path') + self.assertEqual(route.factory, dummyfactory) + + def test_add_route_with_xhr(self): + config = self._makeOne(autocommit=True) + config.add_route('name', 'path', xhr=True) + route = self._assertRoute(config, 'name', 'path', 1) + predicate = route.predicates[0] + request = self._makeRequest(config) + request.is_xhr = True + self.assertEqual(predicate(None, request), True) + request = self._makeRequest(config) + request.is_xhr = False + self.assertEqual(predicate(None, request), False) + + def test_add_route_with_request_method(self): + config = self._makeOne(autocommit=True) + config.add_route('name', 'path', request_method='GET') + route = self._assertRoute(config, 'name', 'path', 1) + predicate = route.predicates[0] + request = self._makeRequest(config) + request.method = 'GET' + self.assertEqual(predicate(None, request), True) + request = self._makeRequest(config) + request.method = 'POST' + self.assertEqual(predicate(None, request), False) + + def test_add_route_with_path_info(self): + config = self._makeOne(autocommit=True) + config.add_route('name', 'path', path_info='/foo') + route = self._assertRoute(config, 'name', 'path', 1) + predicate = route.predicates[0] + request = self._makeRequest(config) + request.upath_info = '/foo' + self.assertEqual(predicate(None, request), True) + request = self._makeRequest(config) + request.upath_info = '/' + self.assertEqual(predicate(None, request), False) + + def test_add_route_with_path_info_highorder(self): + config = self._makeOne(autocommit=True) + config.add_route('name', 'path', + path_info=text_(b'/La Pe\xc3\xb1a', 'utf-8')) + route = self._assertRoute(config, 'name', 'path', 1) + predicate = route.predicates[0] + request = self._makeRequest(config) + request.upath_info = text_(b'/La Pe\xc3\xb1a', 'utf-8') + self.assertEqual(predicate(None, request), True) + request = self._makeRequest(config) + request.upath_info = text_('/') + self.assertEqual(predicate(None, request), False) + + def test_add_route_with_path_info_regex(self): + config = self._makeOne(autocommit=True) + config.add_route('name', 'path', + path_info=text_(br'/La Pe\w*', 'utf-8')) + route = self._assertRoute(config, 'name', 'path', 1) + predicate = route.predicates[0] + request = self._makeRequest(config) + request.upath_info = text_(b'/La Pe\xc3\xb1a', 'utf-8') + self.assertEqual(predicate(None, request), True) + request = self._makeRequest(config) + request.upath_info = text_('/') + self.assertEqual(predicate(None, request), False) + + def test_add_route_with_request_param(self): + config = self._makeOne(autocommit=True) + config.add_route('name', 'path', request_param='abc') + route = self._assertRoute(config, 'name', 'path', 1) + predicate = route.predicates[0] + request = self._makeRequest(config) + request.params = {'abc':'123'} + self.assertEqual(predicate(None, request), True) + request = self._makeRequest(config) + request.params = {} + self.assertEqual(predicate(None, request), False) + + def test_add_route_with_custom_predicates(self): + import warnings + config = self._makeOne(autocommit=True) + def pred1(context, request): pass + def pred2(context, request): pass + with warnings.catch_warnings(record=True) as w: + warnings.filterwarnings('always') + config.add_route('name', 'path', custom_predicates=(pred1, pred2)) + self.assertEqual(len(w), 1) + route = self._assertRoute(config, 'name', 'path', 2) + self.assertEqual(len(route.predicates), 2) + + def test_add_route_with_header(self): + config = self._makeOne(autocommit=True) + config.add_route('name', 'path', header='Host') + route = self._assertRoute(config, 'name', 'path', 1) + predicate = route.predicates[0] + request = self._makeRequest(config) + request.headers = {'Host':'example.com'} + self.assertEqual(predicate(None, request), True) + request = self._makeRequest(config) + request.headers = {} + self.assertEqual(predicate(None, request), False) + + def test_add_route_with_accept(self): + config = self._makeOne(autocommit=True) + config.add_route('name', 'path', accept='text/xml') + route = self._assertRoute(config, 'name', 'path', 1) + predicate = route.predicates[0] + request = self._makeRequest(config) + request.accept = DummyAccept('text/xml') + self.assertEqual(predicate(None, request), True) + request = self._makeRequest(config) + request.accept = DummyAccept('text/html') + self.assertEqual(predicate(None, request), False) + + def test_add_route_with_accept_list(self): + config = self._makeOne(autocommit=True) + config.add_route('name', 'path', accept=['text/xml', 'text/plain']) + route = self._assertRoute(config, 'name', 'path', 1) + predicate = route.predicates[0] + request = self._makeRequest(config) + request.accept = DummyAccept('text/xml') + self.assertEqual(predicate(None, request), True) + request = self._makeRequest(config) + request.accept = DummyAccept('text/plain') + self.assertEqual(predicate(None, request), True) + request = self._makeRequest(config) + request.accept = DummyAccept('text/html') + self.assertEqual(predicate(None, request), False) + + def test_add_route_with_wildcard_accept(self): + config = self._makeOne(autocommit=True) + config.add_route('name', 'path', accept='text/*') + route = self._assertRoute(config, 'name', 'path', 1) + predicate = route.predicates[0] + request = self._makeRequest(config) + request.accept = DummyAccept('text/xml', contains=True) + self.assertEqual(predicate(None, request), True) + request = self._makeRequest(config) + request.accept = DummyAccept('application/json', contains=False) + self.assertEqual(predicate(None, request), False) + + def test_add_route_no_pattern_with_path(self): + config = self._makeOne(autocommit=True) + config.add_route('name', path='path') + self._assertRoute(config, 'name', 'path') + + def test_add_route_no_path_no_pattern(self): + from pyramid.exceptions import ConfigurationError + config = self._makeOne() + self.assertRaises(ConfigurationError, config.add_route, 'name') + + def test_add_route_with_pregenerator(self): + config = self._makeOne(autocommit=True) + config.add_route('name', 'pattern', pregenerator='123') + route = self._assertRoute(config, 'name', 'pattern') + self.assertEqual(route.pregenerator, '123') + + def test_add_route_no_view_with_view_attr(self): + config = self._makeOne(autocommit=True) + from pyramid.exceptions import ConfigurationError + try: + config.add_route('name', '/pattern', view_attr='abc') + except ConfigurationError: + pass + else: # pragma: no cover + raise AssertionError + + def test_add_route_no_view_with_view_context(self): + config = self._makeOne(autocommit=True) + from pyramid.exceptions import ConfigurationError + try: + config.add_route('name', '/pattern', view_context=DummyContext) + except ConfigurationError: + pass + else: # pragma: no cover + raise AssertionError + + def test_add_route_no_view_with_view_permission(self): + config = self._makeOne(autocommit=True) + from pyramid.exceptions import ConfigurationError + try: + config.add_route('name', '/pattern', view_permission='edit') + except ConfigurationError: + pass + else: # pragma: no cover + raise AssertionError + + def test_add_route_no_view_with_view_renderer(self): + config = self._makeOne(autocommit=True) + from pyramid.exceptions import ConfigurationError + try: + config.add_route('name', '/pattern', view_renderer='json') + except ConfigurationError: + pass + else: # pragma: no cover + raise AssertionError + +class DummyRequest: + subpath = () + matchdict = None + def __init__(self, environ=None): + if environ is None: + environ = {} + self.environ = environ + self.params = {} + self.cookies = {} + +class DummyAccept(object): + def __init__(self, *matches, **kw): + self.matches = list(matches) + self.contains = kw.pop('contains', False) + + def acceptable_offers(self, offers): + results = [] + for match in self.matches: + if match in offers: + results.append((match, 1.0)) + return results + + def __contains__(self, value): + return self.contains diff --git a/src/pyramid/tests/test_config/test_security.py b/src/pyramid/tests/test_config/test_security.py new file mode 100644 index 000000000..5db8e21fc --- /dev/null +++ b/src/pyramid/tests/test_config/test_security.py @@ -0,0 +1,125 @@ +import unittest + +from pyramid.exceptions import ConfigurationExecutionError +from pyramid.exceptions import ConfigurationError + +class ConfiguratorSecurityMethodsTests(unittest.TestCase): + def _makeOne(self, *arg, **kw): + from pyramid.config import Configurator + config = Configurator(*arg, **kw) + return config + + def test_set_authentication_policy_no_authz_policy(self): + config = self._makeOne() + policy = object() + config.set_authentication_policy(policy) + self.assertRaises(ConfigurationExecutionError, config.commit) + + def test_set_authentication_policy_no_authz_policy_autocommit(self): + config = self._makeOne(autocommit=True) + policy = object() + self.assertRaises(ConfigurationError, + config.set_authentication_policy, policy) + + def test_set_authentication_policy_with_authz_policy(self): + from pyramid.interfaces import IAuthenticationPolicy + from pyramid.interfaces import IAuthorizationPolicy + config = self._makeOne() + authn_policy = object() + authz_policy = object() + config.registry.registerUtility(authz_policy, IAuthorizationPolicy) + config.set_authentication_policy(authn_policy) + config.commit() + self.assertEqual( + config.registry.getUtility(IAuthenticationPolicy), authn_policy) + + def test_set_authentication_policy_with_authz_policy_autocommit(self): + from pyramid.interfaces import IAuthenticationPolicy + from pyramid.interfaces import IAuthorizationPolicy + config = self._makeOne(autocommit=True) + authn_policy = object() + authz_policy = object() + config.registry.registerUtility(authz_policy, IAuthorizationPolicy) + config.set_authentication_policy(authn_policy) + config.commit() + self.assertEqual( + config.registry.getUtility(IAuthenticationPolicy), authn_policy) + + def test_set_authorization_policy_no_authn_policy(self): + config = self._makeOne() + policy = object() + config.set_authorization_policy(policy) + self.assertRaises(ConfigurationExecutionError, config.commit) + + def test_set_authorization_policy_no_authn_policy_autocommit(self): + from pyramid.interfaces import IAuthorizationPolicy + config = self._makeOne(autocommit=True) + policy = object() + config.set_authorization_policy(policy) + self.assertEqual( + config.registry.getUtility(IAuthorizationPolicy), policy) + + def test_set_authorization_policy_with_authn_policy(self): + from pyramid.interfaces import IAuthorizationPolicy + from pyramid.interfaces import IAuthenticationPolicy + config = self._makeOne() + authn_policy = object() + authz_policy = object() + config.registry.registerUtility(authn_policy, IAuthenticationPolicy) + config.set_authorization_policy(authz_policy) + config.commit() + self.assertEqual( + config.registry.getUtility(IAuthorizationPolicy), authz_policy) + + def test_set_authorization_policy_with_authn_policy_autocommit(self): + from pyramid.interfaces import IAuthorizationPolicy + from pyramid.interfaces import IAuthenticationPolicy + config = self._makeOne(autocommit=True) + authn_policy = object() + authz_policy = object() + config.registry.registerUtility(authn_policy, IAuthenticationPolicy) + config.set_authorization_policy(authz_policy) + self.assertEqual( + config.registry.getUtility(IAuthorizationPolicy), authz_policy) + + def test_set_default_permission(self): + from pyramid.interfaces import IDefaultPermission + config = self._makeOne(autocommit=True) + config.set_default_permission('view') + self.assertEqual(config.registry.getUtility(IDefaultPermission), + 'view') + + def test_add_permission(self): + config = self._makeOne(autocommit=True) + config.add_permission('perm') + cat = config.registry.introspector.get_category('permissions') + self.assertEqual(len(cat), 1) + D = cat[0] + intr = D['introspectable'] + self.assertEqual(intr['value'], 'perm') + + def test_set_default_csrf_options(self): + from pyramid.interfaces import IDefaultCSRFOptions + config = self._makeOne(autocommit=True) + config.set_default_csrf_options() + result = config.registry.getUtility(IDefaultCSRFOptions) + self.assertEqual(result.require_csrf, True) + self.assertEqual(result.token, 'csrf_token') + self.assertEqual(result.header, 'X-CSRF-Token') + self.assertEqual(list(sorted(result.safe_methods)), + ['GET', 'HEAD', 'OPTIONS', 'TRACE']) + self.assertTrue(result.callback is None) + + def test_changing_set_default_csrf_options(self): + from pyramid.interfaces import IDefaultCSRFOptions + config = self._makeOne(autocommit=True) + def callback(request): return True + config.set_default_csrf_options( + require_csrf=False, token='DUMMY', header=None, + safe_methods=('PUT',), callback=callback) + result = config.registry.getUtility(IDefaultCSRFOptions) + self.assertEqual(result.require_csrf, False) + self.assertEqual(result.token, 'DUMMY') + self.assertEqual(result.header, None) + self.assertEqual(list(sorted(result.safe_methods)), ['PUT']) + self.assertTrue(result.callback is callback) diff --git a/src/pyramid/tests/test_config/test_settings.py b/src/pyramid/tests/test_config/test_settings.py new file mode 100644 index 000000000..a3afd24e7 --- /dev/null +++ b/src/pyramid/tests/test_config/test_settings.py @@ -0,0 +1,582 @@ +import unittest + + +class TestSettingsConfiguratorMixin(unittest.TestCase): + def _makeOne(self, *arg, **kw): + from pyramid.config import Configurator + config = Configurator(*arg, **kw) + return config + + def test__set_settings_as_None(self): + config = self._makeOne() + settings = config._set_settings(None) + self.assertTrue(settings) + + def test__set_settings_does_not_uses_original_dict(self): + config = self._makeOne() + dummy = {} + result = config._set_settings(dummy) + self.assertTrue(dummy is not result) + self.assertNotIn('pyramid.debug_all', dummy) + + def test__set_settings_as_dictwithvalues(self): + config = self._makeOne() + settings = config._set_settings({'a':'1'}) + self.assertEqual(settings['a'], '1') + + def test_get_settings_nosettings(self): + from pyramid.registry import Registry + reg = Registry() + config = self._makeOne(reg) + self.assertEqual(config.get_settings(), None) + + def test_get_settings_withsettings(self): + settings = {'a':1} + config = self._makeOne() + config.registry.settings = settings + self.assertEqual(config.get_settings(), settings) + + def test_add_settings_settings_already_registered(self): + from pyramid.registry import Registry + reg = Registry() + config = self._makeOne(reg) + config._set_settings({'a':1}) + config.add_settings({'b':2}) + settings = reg.settings + self.assertEqual(settings['a'], 1) + self.assertEqual(settings['b'], 2) + + def test_add_settings_settings_not_yet_registered(self): + from pyramid.registry import Registry + from pyramid.interfaces import ISettings + reg = Registry() + config = self._makeOne(reg) + config.add_settings({'a':1}) + settings = reg.getUtility(ISettings) + self.assertEqual(settings['a'], 1) + + def test_add_settings_settings_None(self): + from pyramid.registry import Registry + from pyramid.interfaces import ISettings + reg = Registry() + config = self._makeOne(reg) + config.add_settings(None, a=1) + settings = reg.getUtility(ISettings) + self.assertEqual(settings['a'], 1) + + def test_settings_parameter_dict_is_never_updated(self): + class ReadOnlyDict(dict): + def __readonly__(self, *args, **kwargs): # pragma: no cover + raise RuntimeError("Cannot modify ReadOnlyDict") + __setitem__ = __readonly__ + __delitem__ = __readonly__ + pop = __readonly__ + popitem = __readonly__ + clear = __readonly__ + update = __readonly__ + setdefault = __readonly__ + del __readonly__ + + initial = ReadOnlyDict() + config = self._makeOne(settings=initial) + config._set_settings({'a': '1'}) + + +class TestSettings(unittest.TestCase): + + def _getTargetClass(self): + from pyramid.config.settings import Settings + return Settings + + def _makeOne(self, d=None, environ=None): + if environ is None: + environ = {} + klass = self._getTargetClass() + return klass(d, _environ_=environ) + + def test_noargs(self): + settings = self._makeOne() + self.assertEqual(settings['debug_authorization'], False) + self.assertEqual(settings['debug_notfound'], False) + self.assertEqual(settings['debug_routematch'], False) + self.assertEqual(settings['reload_templates'], False) + self.assertEqual(settings['reload_resources'], False) + + self.assertEqual(settings['pyramid.debug_authorization'], False) + self.assertEqual(settings['pyramid.debug_notfound'], False) + self.assertEqual(settings['pyramid.debug_routematch'], False) + self.assertEqual(settings['pyramid.reload_templates'], False) + self.assertEqual(settings['pyramid.reload_resources'], False) + + def test_prevent_http_cache(self): + settings = self._makeOne({}) + self.assertEqual(settings['prevent_http_cache'], False) + self.assertEqual(settings['pyramid.prevent_http_cache'], False) + result = self._makeOne({'prevent_http_cache':'false'}) + self.assertEqual(result['prevent_http_cache'], False) + self.assertEqual(result['pyramid.prevent_http_cache'], False) + result = self._makeOne({'prevent_http_cache':'t'}) + self.assertEqual(result['prevent_http_cache'], True) + self.assertEqual(result['pyramid.prevent_http_cache'], True) + result = self._makeOne({'prevent_http_cache':'1'}) + self.assertEqual(result['prevent_http_cache'], True) + self.assertEqual(result['pyramid.prevent_http_cache'], True) + result = self._makeOne({'pyramid.prevent_http_cache':'t'}) + self.assertEqual(result['prevent_http_cache'], True) + self.assertEqual(result['pyramid.prevent_http_cache'], True) + result = self._makeOne({}, {'PYRAMID_PREVENT_HTTP_CACHE':'1'}) + self.assertEqual(result['prevent_http_cache'], True) + self.assertEqual(result['pyramid.prevent_http_cache'], True) + result = self._makeOne({'prevent_http_cache':'false', + 'pyramid.prevent_http_cache':'1'}) + self.assertEqual(result['prevent_http_cache'], True) + self.assertEqual(result['pyramid.prevent_http_cache'], True) + result = self._makeOne({'prevent_http_cache':'false', + 'pyramid.prevent_http_cache':'f'}, + {'PYRAMID_PREVENT_HTTP_CACHE':'1'}) + self.assertEqual(result['prevent_http_cache'], True) + self.assertEqual(result['pyramid.prevent_http_cache'], True) + + def test_prevent_cachebust(self): + settings = self._makeOne({}) + self.assertEqual(settings['prevent_cachebust'], False) + self.assertEqual(settings['pyramid.prevent_cachebust'], False) + result = self._makeOne({'prevent_cachebust':'false'}) + self.assertEqual(result['prevent_cachebust'], False) + self.assertEqual(result['pyramid.prevent_cachebust'], False) + result = self._makeOne({'prevent_cachebust':'t'}) + self.assertEqual(result['prevent_cachebust'], True) + self.assertEqual(result['pyramid.prevent_cachebust'], True) + result = self._makeOne({'prevent_cachebust':'1'}) + self.assertEqual(result['prevent_cachebust'], True) + self.assertEqual(result['pyramid.prevent_cachebust'], True) + result = self._makeOne({'pyramid.prevent_cachebust':'t'}) + self.assertEqual(result['prevent_cachebust'], True) + self.assertEqual(result['pyramid.prevent_cachebust'], True) + result = self._makeOne({}, {'PYRAMID_PREVENT_CACHEBUST':'1'}) + self.assertEqual(result['prevent_cachebust'], True) + self.assertEqual(result['pyramid.prevent_cachebust'], True) + result = self._makeOne({'prevent_cachebust':'false', + 'pyramid.prevent_cachebust':'1'}) + self.assertEqual(result['prevent_cachebust'], True) + self.assertEqual(result['pyramid.prevent_cachebust'], True) + result = self._makeOne({'prevent_cachebust':'false', + 'pyramid.prevent_cachebust':'f'}, + {'PYRAMID_PREVENT_CACHEBUST':'1'}) + self.assertEqual(result['prevent_cachebust'], True) + self.assertEqual(result['pyramid.prevent_cachebust'], True) + + def test_reload_templates(self): + settings = self._makeOne({}) + self.assertEqual(settings['reload_templates'], False) + self.assertEqual(settings['pyramid.reload_templates'], False) + result = self._makeOne({'reload_templates':'false'}) + self.assertEqual(result['reload_templates'], False) + self.assertEqual(result['pyramid.reload_templates'], False) + result = self._makeOne({'reload_templates':'t'}) + self.assertEqual(result['reload_templates'], True) + self.assertEqual(result['pyramid.reload_templates'], True) + result = self._makeOne({'reload_templates':'1'}) + self.assertEqual(result['reload_templates'], True) + self.assertEqual(result['pyramid.reload_templates'], True) + result = self._makeOne({'pyramid.reload_templates':'1'}) + self.assertEqual(result['reload_templates'], True) + self.assertEqual(result['pyramid.reload_templates'], True) + result = self._makeOne({}, {'PYRAMID_RELOAD_TEMPLATES':'1'}) + self.assertEqual(result['reload_templates'], True) + self.assertEqual(result['pyramid.reload_templates'], True) + result = self._makeOne({'reload_templates':'false', + 'pyramid.reload_templates':'1'}) + self.assertEqual(result['reload_templates'], True) + self.assertEqual(result['pyramid.reload_templates'], True) + result = self._makeOne({'reload_templates':'false'}, + {'PYRAMID_RELOAD_TEMPLATES':'1'}) + self.assertEqual(result['reload_templates'], True) + self.assertEqual(result['pyramid.reload_templates'], True) + + def test_reload_resources(self): + # alias for reload_assets + result = self._makeOne({}) + self.assertEqual(result['reload_resources'], False) + self.assertEqual(result['reload_assets'], False) + self.assertEqual(result['pyramid.reload_resources'], False) + self.assertEqual(result['pyramid.reload_assets'], False) + result = self._makeOne({'reload_resources':'false'}) + self.assertEqual(result['reload_resources'], False) + self.assertEqual(result['reload_assets'], False) + self.assertEqual(result['pyramid.reload_resources'], False) + self.assertEqual(result['pyramid.reload_assets'], False) + result = self._makeOne({'reload_resources':'t'}) + self.assertEqual(result['reload_resources'], True) + self.assertEqual(result['reload_assets'], True) + self.assertEqual(result['pyramid.reload_resources'], True) + self.assertEqual(result['pyramid.reload_assets'], True) + result = self._makeOne({'reload_resources':'1'}) + self.assertEqual(result['reload_resources'], True) + self.assertEqual(result['reload_assets'], True) + self.assertEqual(result['pyramid.reload_resources'], True) + self.assertEqual(result['pyramid.reload_assets'], True) + result = self._makeOne({'pyramid.reload_resources':'1'}) + self.assertEqual(result['reload_resources'], True) + self.assertEqual(result['reload_assets'], True) + self.assertEqual(result['pyramid.reload_resources'], True) + self.assertEqual(result['pyramid.reload_assets'], True) + result = self._makeOne({}, {'PYRAMID_RELOAD_RESOURCES':'1'}) + self.assertEqual(result['reload_resources'], True) + self.assertEqual(result['reload_assets'], True) + self.assertEqual(result['pyramid.reload_resources'], True) + self.assertEqual(result['pyramid.reload_assets'], True) + result = self._makeOne({'reload_resources':'false', + 'pyramid.reload_resources':'1'}) + self.assertEqual(result['reload_resources'], True) + self.assertEqual(result['reload_assets'], True) + self.assertEqual(result['pyramid.reload_resources'], True) + self.assertEqual(result['pyramid.reload_assets'], True) + result = self._makeOne({'reload_resources':'false', + 'pyramid.reload_resources':'false'}, + {'PYRAMID_RELOAD_RESOURCES':'1'}) + self.assertEqual(result['reload_resources'], True) + self.assertEqual(result['reload_assets'], True) + self.assertEqual(result['pyramid.reload_resources'], True) + self.assertEqual(result['pyramid.reload_assets'], True) + + def test_reload_assets(self): + # alias for reload_resources + result = self._makeOne({}) + self.assertEqual(result['reload_assets'], False) + self.assertEqual(result['reload_resources'], False) + self.assertEqual(result['pyramid.reload_assets'], False) + self.assertEqual(result['pyramid.reload_resources'], False) + result = self._makeOne({'reload_assets':'false'}) + self.assertEqual(result['reload_resources'], False) + self.assertEqual(result['reload_assets'], False) + self.assertEqual(result['pyramid.reload_assets'], False) + self.assertEqual(result['pyramid.reload_resources'], False) + result = self._makeOne({'reload_assets':'t'}) + self.assertEqual(result['reload_assets'], True) + self.assertEqual(result['reload_resources'], True) + self.assertEqual(result['pyramid.reload_assets'], True) + self.assertEqual(result['pyramid.reload_resources'], True) + result = self._makeOne({'reload_assets':'1'}) + self.assertEqual(result['reload_assets'], True) + self.assertEqual(result['reload_resources'], True) + self.assertEqual(result['pyramid.reload_assets'], True) + self.assertEqual(result['pyramid.reload_resources'], True) + result = self._makeOne({'pyramid.reload_assets':'1'}) + self.assertEqual(result['reload_assets'], True) + self.assertEqual(result['reload_resources'], True) + self.assertEqual(result['pyramid.reload_assets'], True) + self.assertEqual(result['pyramid.reload_resources'], True) + result = self._makeOne({}, {'PYRAMID_RELOAD_ASSETS':'1'}) + self.assertEqual(result['reload_assets'], True) + self.assertEqual(result['reload_resources'], True) + self.assertEqual(result['pyramid.reload_assets'], True) + self.assertEqual(result['pyramid.reload_resources'], True) + result = self._makeOne({'reload_assets':'false', + 'pyramid.reload_assets':'1'}) + self.assertEqual(result['reload_assets'], True) + self.assertEqual(result['reload_resources'], True) + self.assertEqual(result['pyramid.reload_assets'], True) + self.assertEqual(result['pyramid.reload_resources'], True) + result = self._makeOne({'reload_assets':'false', + 'pyramid.reload_assets':'false'}, + {'PYRAMID_RELOAD_ASSETS':'1'}) + self.assertEqual(result['reload_assets'], True) + self.assertEqual(result['reload_resources'], True) + self.assertEqual(result['pyramid.reload_assets'], True) + self.assertEqual(result['pyramid.reload_resources'], True) + + def test_reload_all(self): + result = self._makeOne({}) + self.assertEqual(result['reload_templates'], False) + self.assertEqual(result['reload_resources'], False) + self.assertEqual(result['reload_assets'], False) + self.assertEqual(result['pyramid.reload_templates'], False) + self.assertEqual(result['pyramid.reload_resources'], False) + self.assertEqual(result['pyramid.reload_assets'], False) + result = self._makeOne({'reload_all':'false'}) + self.assertEqual(result['reload_templates'], False) + self.assertEqual(result['reload_resources'], False) + self.assertEqual(result['reload_assets'], False) + self.assertEqual(result['pyramid.reload_templates'], False) + self.assertEqual(result['pyramid.reload_resources'], False) + self.assertEqual(result['pyramid.reload_assets'], False) + result = self._makeOne({'reload_all':'t'}) + self.assertEqual(result['reload_templates'], True) + self.assertEqual(result['reload_resources'], True) + self.assertEqual(result['reload_assets'], True) + self.assertEqual(result['pyramid.reload_templates'], True) + self.assertEqual(result['pyramid.reload_resources'], True) + self.assertEqual(result['pyramid.reload_assets'], True) + result = self._makeOne({'reload_all':'1'}) + self.assertEqual(result['reload_templates'], True) + self.assertEqual(result['reload_resources'], True) + self.assertEqual(result['reload_assets'], True) + self.assertEqual(result['pyramid.reload_templates'], True) + self.assertEqual(result['pyramid.reload_resources'], True) + self.assertEqual(result['pyramid.reload_assets'], True) + result = self._makeOne({'pyramid.reload_all':'1'}) + self.assertEqual(result['reload_templates'], True) + self.assertEqual(result['reload_resources'], True) + self.assertEqual(result['reload_assets'], True) + self.assertEqual(result['pyramid.reload_templates'], True) + self.assertEqual(result['pyramid.reload_resources'], True) + self.assertEqual(result['pyramid.reload_assets'], True) + result = self._makeOne({}, {'PYRAMID_RELOAD_ALL':'1'}) + self.assertEqual(result['reload_templates'], True) + self.assertEqual(result['reload_resources'], True) + self.assertEqual(result['reload_assets'], True) + self.assertEqual(result['pyramid.reload_templates'], True) + self.assertEqual(result['pyramid.reload_resources'], True) + self.assertEqual(result['pyramid.reload_assets'], True) + result = self._makeOne({'reload_all':'false', + 'pyramid.reload_all':'1'}) + self.assertEqual(result['reload_templates'], True) + self.assertEqual(result['reload_resources'], True) + self.assertEqual(result['reload_assets'], True) + self.assertEqual(result['pyramid.reload_templates'], True) + self.assertEqual(result['pyramid.reload_resources'], True) + self.assertEqual(result['pyramid.reload_assets'], True) + result = self._makeOne({'reload_all':'false', + 'pyramid.reload_all':'false'}, + {'PYRAMID_RELOAD_ALL':'1'}) + self.assertEqual(result['reload_templates'], True) + self.assertEqual(result['reload_resources'], True) + self.assertEqual(result['reload_assets'], True) + self.assertEqual(result['pyramid.reload_templates'], True) + self.assertEqual(result['pyramid.reload_resources'], True) + self.assertEqual(result['pyramid.reload_assets'], True) + + def test_debug_authorization(self): + result = self._makeOne({}) + self.assertEqual(result['debug_authorization'], False) + self.assertEqual(result['pyramid.debug_authorization'], False) + result = self._makeOne({'debug_authorization':'false'}) + self.assertEqual(result['debug_authorization'], False) + self.assertEqual(result['pyramid.debug_authorization'], False) + result = self._makeOne({'debug_authorization':'t'}) + self.assertEqual(result['debug_authorization'], True) + self.assertEqual(result['pyramid.debug_authorization'], True) + result = self._makeOne({'debug_authorization':'1'}) + self.assertEqual(result['debug_authorization'], True) + self.assertEqual(result['pyramid.debug_authorization'], True) + result = self._makeOne({'pyramid.debug_authorization':'1'}) + self.assertEqual(result['debug_authorization'], True) + self.assertEqual(result['pyramid.debug_authorization'], True) + result = self._makeOne({}, {'PYRAMID_DEBUG_AUTHORIZATION':'1'}) + self.assertEqual(result['debug_authorization'], True) + self.assertEqual(result['pyramid.debug_authorization'], True) + result = self._makeOne({'debug_authorization':'false', + 'pyramid.debug_authorization':'1'}) + self.assertEqual(result['debug_authorization'], True) + self.assertEqual(result['pyramid.debug_authorization'], True) + result = self._makeOne({'debug_authorization':'false', + 'pyramid.debug_authorization':'false'}, + {'PYRAMID_DEBUG_AUTHORIZATION':'1'}) + self.assertEqual(result['debug_authorization'], True) + self.assertEqual(result['pyramid.debug_authorization'], True) + + def test_debug_notfound(self): + result = self._makeOne({}) + self.assertEqual(result['debug_notfound'], False) + self.assertEqual(result['pyramid.debug_notfound'], False) + result = self._makeOne({'debug_notfound':'false'}) + self.assertEqual(result['debug_notfound'], False) + self.assertEqual(result['pyramid.debug_notfound'], False) + result = self._makeOne({'debug_notfound':'t'}) + self.assertEqual(result['debug_notfound'], True) + self.assertEqual(result['pyramid.debug_notfound'], True) + result = self._makeOne({'debug_notfound':'1'}) + self.assertEqual(result['debug_notfound'], True) + self.assertEqual(result['pyramid.debug_notfound'], True) + result = self._makeOne({'pyramid.debug_notfound':'1'}) + self.assertEqual(result['debug_notfound'], True) + self.assertEqual(result['pyramid.debug_notfound'], True) + result = self._makeOne({}, {'PYRAMID_DEBUG_NOTFOUND':'1'}) + self.assertEqual(result['debug_notfound'], True) + self.assertEqual(result['pyramid.debug_notfound'], True) + result = self._makeOne({'debug_notfound':'false', + 'pyramid.debug_notfound':'1'}) + self.assertEqual(result['debug_notfound'], True) + self.assertEqual(result['pyramid.debug_notfound'], True) + result = self._makeOne({'debug_notfound':'false', + 'pyramid.debug_notfound':'false'}, + {'PYRAMID_DEBUG_NOTFOUND':'1'}) + self.assertEqual(result['debug_notfound'], True) + self.assertEqual(result['pyramid.debug_notfound'], True) + + def test_debug_routematch(self): + result = self._makeOne({}) + self.assertEqual(result['debug_routematch'], False) + self.assertEqual(result['pyramid.debug_routematch'], False) + result = self._makeOne({'debug_routematch':'false'}) + self.assertEqual(result['debug_routematch'], False) + self.assertEqual(result['pyramid.debug_routematch'], False) + result = self._makeOne({'debug_routematch':'t'}) + self.assertEqual(result['debug_routematch'], True) + self.assertEqual(result['pyramid.debug_routematch'], True) + result = self._makeOne({'debug_routematch':'1'}) + self.assertEqual(result['debug_routematch'], True) + self.assertEqual(result['pyramid.debug_routematch'], True) + result = self._makeOne({'pyramid.debug_routematch':'1'}) + self.assertEqual(result['debug_routematch'], True) + self.assertEqual(result['pyramid.debug_routematch'], True) + result = self._makeOne({}, {'PYRAMID_DEBUG_ROUTEMATCH':'1'}) + self.assertEqual(result['debug_routematch'], True) + self.assertEqual(result['pyramid.debug_routematch'], True) + result = self._makeOne({'debug_routematch':'false', + 'pyramid.debug_routematch':'1'}) + self.assertEqual(result['debug_routematch'], True) + self.assertEqual(result['pyramid.debug_routematch'], True) + result = self._makeOne({'debug_routematch':'false', + 'pyramid.debug_routematch':'false'}, + {'PYRAMID_DEBUG_ROUTEMATCH':'1'}) + self.assertEqual(result['debug_routematch'], True) + self.assertEqual(result['pyramid.debug_routematch'], True) + + def test_debug_templates(self): + result = self._makeOne({}) + self.assertEqual(result['debug_templates'], False) + self.assertEqual(result['pyramid.debug_templates'], False) + result = self._makeOne({'debug_templates':'false'}) + self.assertEqual(result['debug_templates'], False) + self.assertEqual(result['pyramid.debug_templates'], False) + result = self._makeOne({'debug_templates':'t'}) + self.assertEqual(result['debug_templates'], True) + self.assertEqual(result['pyramid.debug_templates'], True) + result = self._makeOne({'debug_templates':'1'}) + self.assertEqual(result['debug_templates'], True) + self.assertEqual(result['pyramid.debug_templates'], True) + result = self._makeOne({'pyramid.debug_templates':'1'}) + self.assertEqual(result['debug_templates'], True) + self.assertEqual(result['pyramid.debug_templates'], True) + result = self._makeOne({}, {'PYRAMID_DEBUG_TEMPLATES':'1'}) + self.assertEqual(result['debug_templates'], True) + self.assertEqual(result['pyramid.debug_templates'], True) + result = self._makeOne({'debug_templates':'false', + 'pyramid.debug_templates':'1'}) + self.assertEqual(result['debug_templates'], True) + self.assertEqual(result['pyramid.debug_templates'], True) + result = self._makeOne({'debug_templates':'false', + 'pyramid.debug_templates':'false'}, + {'PYRAMID_DEBUG_TEMPLATES':'1'}) + self.assertEqual(result['debug_templates'], True) + self.assertEqual(result['pyramid.debug_templates'], True) + + def test_debug_all(self): + result = self._makeOne({}) + self.assertEqual(result['debug_notfound'], False) + self.assertEqual(result['debug_routematch'], False) + self.assertEqual(result['debug_authorization'], False) + self.assertEqual(result['debug_templates'], False) + self.assertEqual(result['pyramid.debug_notfound'], False) + self.assertEqual(result['pyramid.debug_routematch'], False) + self.assertEqual(result['pyramid.debug_authorization'], False) + self.assertEqual(result['pyramid.debug_templates'], False) + result = self._makeOne({'debug_all':'false'}) + self.assertEqual(result['debug_notfound'], False) + self.assertEqual(result['debug_routematch'], False) + self.assertEqual(result['debug_authorization'], False) + self.assertEqual(result['debug_templates'], False) + self.assertEqual(result['pyramid.debug_notfound'], False) + self.assertEqual(result['pyramid.debug_routematch'], False) + self.assertEqual(result['pyramid.debug_authorization'], False) + self.assertEqual(result['pyramid.debug_templates'], False) + result = self._makeOne({'debug_all':'t'}) + self.assertEqual(result['debug_notfound'], True) + self.assertEqual(result['debug_routematch'], True) + self.assertEqual(result['debug_authorization'], True) + self.assertEqual(result['debug_templates'], True) + self.assertEqual(result['pyramid.debug_notfound'], True) + self.assertEqual(result['pyramid.debug_routematch'], True) + self.assertEqual(result['pyramid.debug_authorization'], True) + self.assertEqual(result['pyramid.debug_templates'], True) + result = self._makeOne({'debug_all':'1'}) + self.assertEqual(result['debug_notfound'], True) + self.assertEqual(result['debug_routematch'], True) + self.assertEqual(result['debug_authorization'], True) + self.assertEqual(result['debug_templates'], True) + self.assertEqual(result['pyramid.debug_notfound'], True) + self.assertEqual(result['pyramid.debug_routematch'], True) + self.assertEqual(result['pyramid.debug_authorization'], True) + self.assertEqual(result['pyramid.debug_templates'], True) + result = self._makeOne({'pyramid.debug_all':'1'}) + self.assertEqual(result['debug_notfound'], True) + self.assertEqual(result['debug_routematch'], True) + self.assertEqual(result['debug_authorization'], True) + self.assertEqual(result['debug_templates'], True) + self.assertEqual(result['pyramid.debug_notfound'], True) + self.assertEqual(result['pyramid.debug_routematch'], True) + self.assertEqual(result['pyramid.debug_authorization'], True) + self.assertEqual(result['pyramid.debug_templates'], True) + result = self._makeOne({}, {'PYRAMID_DEBUG_ALL':'1'}) + self.assertEqual(result['debug_notfound'], True) + self.assertEqual(result['debug_routematch'], True) + self.assertEqual(result['debug_authorization'], True) + self.assertEqual(result['debug_templates'], True) + self.assertEqual(result['pyramid.debug_notfound'], True) + self.assertEqual(result['pyramid.debug_routematch'], True) + self.assertEqual(result['pyramid.debug_authorization'], True) + self.assertEqual(result['pyramid.debug_templates'], True) + result = self._makeOne({'debug_all':'false', + 'pyramid.debug_all':'1'}) + self.assertEqual(result['debug_notfound'], True) + self.assertEqual(result['debug_routematch'], True) + self.assertEqual(result['debug_authorization'], True) + self.assertEqual(result['debug_templates'], True) + self.assertEqual(result['pyramid.debug_notfound'], True) + self.assertEqual(result['pyramid.debug_routematch'], True) + self.assertEqual(result['pyramid.debug_authorization'], True) + self.assertEqual(result['pyramid.debug_templates'], True) + result = self._makeOne({'debug_all':'false', + 'pyramid.debug_all':'false'}, + {'PYRAMID_DEBUG_ALL':'1'}) + self.assertEqual(result['debug_notfound'], True) + self.assertEqual(result['debug_routematch'], True) + self.assertEqual(result['debug_authorization'], True) + self.assertEqual(result['debug_templates'], True) + self.assertEqual(result['pyramid.debug_notfound'], True) + self.assertEqual(result['pyramid.debug_routematch'], True) + self.assertEqual(result['pyramid.debug_authorization'], True) + self.assertEqual(result['pyramid.debug_templates'], True) + + def test_default_locale_name(self): + result = self._makeOne({}) + self.assertEqual(result['default_locale_name'], 'en') + self.assertEqual(result['pyramid.default_locale_name'], 'en') + result = self._makeOne({'default_locale_name':'abc'}) + self.assertEqual(result['default_locale_name'], 'abc') + self.assertEqual(result['pyramid.default_locale_name'], 'abc') + result = self._makeOne({'pyramid.default_locale_name':'abc'}) + self.assertEqual(result['default_locale_name'], 'abc') + self.assertEqual(result['pyramid.default_locale_name'], 'abc') + result = self._makeOne({}, {'PYRAMID_DEFAULT_LOCALE_NAME':'abc'}) + self.assertEqual(result['default_locale_name'], 'abc') + self.assertEqual(result['pyramid.default_locale_name'], 'abc') + result = self._makeOne({'default_locale_name':'def', + 'pyramid.default_locale_name':'abc'}) + self.assertEqual(result['default_locale_name'], 'abc') + self.assertEqual(result['pyramid.default_locale_name'], 'abc') + result = self._makeOne({'default_locale_name':'def', + 'pyramid.default_locale_name':'ghi'}, + {'PYRAMID_DEFAULT_LOCALE_NAME':'abc'}) + self.assertEqual(result['default_locale_name'], 'abc') + self.assertEqual(result['pyramid.default_locale_name'], 'abc') + + def test_csrf_trusted_origins(self): + result = self._makeOne({}) + self.assertEqual(result['pyramid.csrf_trusted_origins'], []) + result = self._makeOne({'pyramid.csrf_trusted_origins': 'example.com'}) + self.assertEqual(result['pyramid.csrf_trusted_origins'], ['example.com']) + result = self._makeOne({'pyramid.csrf_trusted_origins': ['example.com']}) + self.assertEqual(result['pyramid.csrf_trusted_origins'], ['example.com']) + result = self._makeOne({'pyramid.csrf_trusted_origins': ( + 'example.com foo.example.com\nasdf.example.com')}) + self.assertEqual(result['pyramid.csrf_trusted_origins'], [ + 'example.com', 'foo.example.com', 'asdf.example.com']) + + def test_originals_kept(self): + result = self._makeOne({'a':'i am so a'}) + self.assertEqual(result['a'], 'i am so a') + + diff --git a/src/pyramid/tests/test_config/test_testing.py b/src/pyramid/tests/test_config/test_testing.py new file mode 100644 index 000000000..05561bfe9 --- /dev/null +++ b/src/pyramid/tests/test_config/test_testing.py @@ -0,0 +1,205 @@ +import unittest + +from pyramid.compat import text_ +from pyramid.security import AuthenticationAPIMixin, AuthorizationAPIMixin +from pyramid.tests.test_config import IDummy + +class TestingConfiguratorMixinTests(unittest.TestCase): + def _makeOne(self, *arg, **kw): + from pyramid.config import Configurator + config = Configurator(*arg, **kw) + return config + + def test_testing_securitypolicy(self): + from pyramid.testing import DummySecurityPolicy + config = self._makeOne(autocommit=True) + config.testing_securitypolicy('user', ('group1', 'group2'), + permissive=False) + from pyramid.interfaces import IAuthenticationPolicy + from pyramid.interfaces import IAuthorizationPolicy + ut = config.registry.getUtility(IAuthenticationPolicy) + self.assertTrue(isinstance(ut, DummySecurityPolicy)) + ut = config.registry.getUtility(IAuthorizationPolicy) + self.assertEqual(ut.userid, 'user') + self.assertEqual(ut.groupids, ('group1', 'group2')) + self.assertEqual(ut.permissive, False) + + def test_testing_securitypolicy_remember_result(self): + from pyramid.security import remember + config = self._makeOne(autocommit=True) + pol = config.testing_securitypolicy( + 'user', ('group1', 'group2'), + permissive=False, remember_result=True) + request = DummyRequest() + request.registry = config.registry + val = remember(request, 'fred') + self.assertEqual(pol.remembered, 'fred') + self.assertEqual(val, True) + + def test_testing_securitypolicy_forget_result(self): + from pyramid.security import forget + config = self._makeOne(autocommit=True) + pol = config.testing_securitypolicy( + 'user', ('group1', 'group2'), + permissive=False, forget_result=True) + request = DummyRequest() + request.registry = config.registry + val = forget(request) + self.assertEqual(pol.forgotten, True) + self.assertEqual(val, True) + + def test_testing_resources(self): + from pyramid.traversal import find_resource + from pyramid.interfaces import ITraverser + ob1 = object() + ob2 = object() + resources = {'/ob1':ob1, '/ob2':ob2} + config = self._makeOne(autocommit=True) + config.testing_resources(resources) + adapter = config.registry.getAdapter(None, ITraverser) + result = adapter(DummyRequest({'PATH_INFO':'/ob1'})) + self.assertEqual(result['context'], ob1) + self.assertEqual(result['view_name'], '') + self.assertEqual(result['subpath'], ()) + self.assertEqual(result['traversed'], (text_('ob1'),)) + self.assertEqual(result['virtual_root'], ob1) + self.assertEqual(result['virtual_root_path'], ()) + result = adapter(DummyRequest({'PATH_INFO':'/ob2'})) + self.assertEqual(result['context'], ob2) + self.assertEqual(result['view_name'], '') + self.assertEqual(result['subpath'], ()) + self.assertEqual(result['traversed'], (text_('ob2'),)) + self.assertEqual(result['virtual_root'], ob2) + self.assertEqual(result['virtual_root_path'], ()) + self.assertRaises(KeyError, adapter, DummyRequest({'PATH_INFO':'/ob3'})) + try: + config.begin() + self.assertEqual(find_resource(None, '/ob1'), ob1) + finally: + config.end() + + def test_testing_add_subscriber_single(self): + config = self._makeOne(autocommit=True) + L = config.testing_add_subscriber(IDummy) + event = DummyEvent() + config.registry.notify(event) + self.assertEqual(len(L), 1) + self.assertEqual(L[0], event) + config.registry.notify(object()) + self.assertEqual(len(L), 1) + + def test_testing_add_subscriber_dottedname(self): + config = self._makeOne(autocommit=True) + L = config.testing_add_subscriber( + 'pyramid.tests.test_config.test_init.IDummy') + event = DummyEvent() + config.registry.notify(event) + self.assertEqual(len(L), 1) + self.assertEqual(L[0], event) + config.registry.notify(object()) + self.assertEqual(len(L), 1) + + def test_testing_add_subscriber_multiple(self): + from zope.interface import Interface + config = self._makeOne(autocommit=True) + L = config.testing_add_subscriber((Interface, IDummy)) + event = DummyEvent() + event.object = 'foo' + # the below is the equivalent of z.c.event.objectEventNotify(event) + config.registry.subscribers((event.object, event), None) + self.assertEqual(len(L), 2) + self.assertEqual(L[0], 'foo') + self.assertEqual(L[1], event) + + def test_testing_add_subscriber_defaults(self): + config = self._makeOne(autocommit=True) + L = config.testing_add_subscriber() + event = object() + config.registry.notify(event) + self.assertEqual(L[-1], event) + event2 = object() + config.registry.notify(event2) + self.assertEqual(L[-1], event2) + + def test_testing_add_renderer(self): + config = self._makeOne(autocommit=True) + renderer = config.testing_add_renderer('templates/foo.pt') + from pyramid.testing import DummyTemplateRenderer + self.assertTrue(isinstance(renderer, DummyTemplateRenderer)) + from pyramid.renderers import render_to_response + # must provide request to pass in registry (this is a functest) + request = DummyRequest() + request.registry = config.registry + render_to_response( + 'templates/foo.pt', {'foo':1, 'bar':2}, request=request) + renderer.assert_(foo=1) + renderer.assert_(bar=2) + renderer.assert_(request=request) + + def test_testing_add_renderer_twice(self): + config = self._makeOne(autocommit=True) + renderer1 = config.testing_add_renderer('templates/foo.pt') + renderer2 = config.testing_add_renderer('templates/bar.pt') + from pyramid.testing import DummyTemplateRenderer + self.assertTrue(isinstance(renderer1, DummyTemplateRenderer)) + self.assertTrue(isinstance(renderer2, DummyTemplateRenderer)) + from pyramid.renderers import render_to_response + # must provide request to pass in registry (this is a functest) + request = DummyRequest() + request.registry = config.registry + render_to_response( + 'templates/foo.pt', {'foo':1, 'bar':2}, request=request) + renderer1.assert_(foo=1) + renderer1.assert_(bar=2) + renderer1.assert_(request=request) + render_to_response( + 'templates/bar.pt', {'foo':1, 'bar':2}, request=request) + renderer2.assert_(foo=1) + renderer2.assert_(bar=2) + renderer2.assert_(request=request) + + def test_testing_add_renderer_explicitrenderer(self): + config = self._makeOne(autocommit=True) + class E(Exception): pass + def renderer(kw, system): + self.assertEqual(kw, {'foo':1, 'bar':2}) + raise E + renderer = config.testing_add_renderer('templates/foo.pt', renderer) + from pyramid.renderers import render_to_response + # must provide request to pass in registry (this is a functest) + request = DummyRequest() + request.registry = config.registry + try: + render_to_response( + 'templates/foo.pt', {'foo':1, 'bar':2}, request=request) + except E: + pass + else: # pragma: no cover + raise AssertionError + + def test_testing_add_template(self): + config = self._makeOne(autocommit=True) + renderer = config.testing_add_template('templates/foo.pt') + from pyramid.testing import DummyTemplateRenderer + self.assertTrue(isinstance(renderer, DummyTemplateRenderer)) + from pyramid.renderers import render_to_response + # must provide request to pass in registry (this is a functest) + request = DummyRequest() + request.registry = config.registry + render_to_response('templates/foo.pt', dict(foo=1, bar=2), + request=request) + renderer.assert_(foo=1) + renderer.assert_(bar=2) + renderer.assert_(request=request) + +from zope.interface import implementer +@implementer(IDummy) +class DummyEvent: + pass + +class DummyRequest(AuthenticationAPIMixin, AuthorizationAPIMixin): + def __init__(self, environ=None): + if environ is None: + environ = {} + self.environ = environ + diff --git a/src/pyramid/tests/test_config/test_tweens.py b/src/pyramid/tests/test_config/test_tweens.py new file mode 100644 index 000000000..9c3433468 --- /dev/null +++ b/src/pyramid/tests/test_config/test_tweens.py @@ -0,0 +1,410 @@ +import unittest + +from pyramid.tests.test_config import dummy_tween_factory +from pyramid.tests.test_config import dummy_tween_factory2 + +from pyramid.exceptions import ConfigurationConflictError + +class TestTweensConfiguratorMixin(unittest.TestCase): + def _makeOne(self, *arg, **kw): + from pyramid.config import Configurator + config = Configurator(*arg, **kw) + return config + + def test_add_tweens_names_distinct(self): + from pyramid.interfaces import ITweens + from pyramid.tweens import excview_tween_factory + def factory1(handler, registry): return handler + def factory2(handler, registry): return handler + config = self._makeOne() + config.add_tween( + 'pyramid.tests.test_config.dummy_tween_factory') + config.add_tween( + 'pyramid.tests.test_config.dummy_tween_factory2') + config.commit() + tweens = config.registry.queryUtility(ITweens) + implicit = tweens.implicit() + self.assertEqual( + implicit, + [ + ('pyramid.tests.test_config.dummy_tween_factory2', + dummy_tween_factory2), + ('pyramid.tests.test_config.dummy_tween_factory', + dummy_tween_factory), + ('pyramid.tweens.excview_tween_factory', + excview_tween_factory), + ] + ) + + def test_add_tweens_names_with_underover(self): + from pyramid.interfaces import ITweens + from pyramid.tweens import excview_tween_factory + from pyramid.tweens import MAIN + config = self._makeOne() + config.add_tween( + 'pyramid.tests.test_config.dummy_tween_factory', + over=MAIN) + config.add_tween( + 'pyramid.tests.test_config.dummy_tween_factory2', + over=MAIN, + under='pyramid.tests.test_config.dummy_tween_factory') + config.commit() + tweens = config.registry.queryUtility(ITweens) + implicit = tweens.implicit() + self.assertEqual( + implicit, + [ + ('pyramid.tweens.excview_tween_factory', excview_tween_factory), + ('pyramid.tests.test_config.dummy_tween_factory', + dummy_tween_factory), + ('pyramid.tests.test_config.dummy_tween_factory2', + dummy_tween_factory2), + ]) + + def test_add_tweens_names_with_under_nonstringoriter(self): + from pyramid.exceptions import ConfigurationError + config = self._makeOne() + self.assertRaises( + ConfigurationError, config.add_tween, + 'pyramid.tests.test_config.dummy_tween_factory', + under=False) + + def test_add_tweens_names_with_over_nonstringoriter(self): + from pyramid.exceptions import ConfigurationError + config = self._makeOne() + self.assertRaises( + ConfigurationError, config.add_tween, + 'pyramid.tests.test_config.dummy_tween_factory', + over=False) + + def test_add_tween_dottedname(self): + from pyramid.interfaces import ITweens + from pyramid.tweens import excview_tween_factory + config = self._makeOne() + config.add_tween('pyramid.tests.test_config.dummy_tween_factory') + config.commit() + tweens = config.registry.queryUtility(ITweens) + self.assertEqual( + tweens.implicit(), + [ + ('pyramid.tests.test_config.dummy_tween_factory', + dummy_tween_factory), + ('pyramid.tweens.excview_tween_factory', + excview_tween_factory), + ]) + + def test_add_tween_instance(self): + from pyramid.exceptions import ConfigurationError + class ATween(object): pass + atween = ATween() + config = self._makeOne() + self.assertRaises(ConfigurationError, config.add_tween, atween) + + def test_add_tween_unsuitable(self): + from pyramid.exceptions import ConfigurationError + import pyramid.tests.test_config + config = self._makeOne() + self.assertRaises(ConfigurationError, config.add_tween, + pyramid.tests.test_config) + + def test_add_tween_name_ingress(self): + from pyramid.exceptions import ConfigurationError + from pyramid.tweens import INGRESS + config = self._makeOne() + self.assertRaises(ConfigurationError, config.add_tween, INGRESS) + + def test_add_tween_name_main(self): + from pyramid.exceptions import ConfigurationError + from pyramid.tweens import MAIN + config = self._makeOne() + self.assertRaises(ConfigurationError, config.add_tween, MAIN) + + def test_add_tweens_conflict(self): + config = self._makeOne() + config.add_tween('pyramid.tests.test_config.dummy_tween_factory') + config.add_tween('pyramid.tests.test_config.dummy_tween_factory') + self.assertRaises(ConfigurationConflictError, config.commit) + + def test_add_tween_over_ingress(self): + from pyramid.exceptions import ConfigurationError + from pyramid.tweens import INGRESS + config = self._makeOne() + self.assertRaises( + ConfigurationError, + config.add_tween, + 'pyramid.tests.test_config.dummy_tween_factory', + over=INGRESS) + + def test_add_tween_over_ingress_iterable(self): + from pyramid.exceptions import ConfigurationError + from pyramid.tweens import INGRESS + config = self._makeOne() + self.assertRaises( + ConfigurationError, + config.add_tween, + 'pyramid.tests.test_config.dummy_tween_factory', + over=('a', INGRESS)) + + def test_add_tween_under_main(self): + from pyramid.exceptions import ConfigurationError + from pyramid.tweens import MAIN + config = self._makeOne() + self.assertRaises( + ConfigurationError, + config.add_tween, + 'pyramid.tests.test_config.dummy_tween_factory', + under=MAIN) + + def test_add_tween_under_main_iterable(self): + from pyramid.exceptions import ConfigurationError + from pyramid.tweens import MAIN + config = self._makeOne() + self.assertRaises( + ConfigurationError, + config.add_tween, + 'pyramid.tests.test_config.dummy_tween_factory', + under=('a', MAIN)) + +class TestTweens(unittest.TestCase): + def _makeOne(self): + from pyramid.config.tweens import Tweens + return Tweens() + + def test_add_explicit(self): + tweens = self._makeOne() + tweens.add_explicit('name', 'factory') + self.assertEqual(tweens.explicit, [('name', 'factory')]) + tweens.add_explicit('name2', 'factory2') + self.assertEqual(tweens.explicit, [('name', 'factory'), + ('name2', 'factory2')]) + + def test_add_implicit(self): + tweens = self._makeOne() + tweens.add_implicit('name', 'factory') + tweens.add_implicit('name2', 'factory2') + self.assertEqual(tweens.sorter.sorted(), + [('name2', 'factory2'), + ('name', 'factory')]) + + def test___call___explicit(self): + tweens = self._makeOne() + def factory1(handler, registry): + return handler + def factory2(handler, registry): + return '123' + tweens.explicit = [('name', factory1), ('name', factory2)] + self.assertEqual(tweens(None, None), '123') + + def test___call___implicit(self): + tweens = self._makeOne() + def factory1(handler, registry): + return handler + def factory2(handler, registry): + return '123' + tweens.add_implicit('name2', factory2) + tweens.add_implicit('name1', factory1) + self.assertEqual(tweens(None, None), '123') + + def test_implicit_ordering_1(self): + tweens = self._makeOne() + tweens.add_implicit('name1', 'factory1') + tweens.add_implicit('name2', 'factory2') + self.assertEqual(tweens.implicit(), + [ + ('name2', 'factory2'), + ('name1', 'factory1'), + ]) + + def test_implicit_ordering_2(self): + from pyramid.tweens import MAIN + tweens = self._makeOne() + tweens.add_implicit('name1', 'factory1') + tweens.add_implicit('name2', 'factory2', over=MAIN) + self.assertEqual(tweens.implicit(), + [ + ('name1', 'factory1'), + ('name2', 'factory2'), + ]) + + def test_implicit_ordering_3(self): + from pyramid.tweens import MAIN + tweens = self._makeOne() + add = tweens.add_implicit + add('auth', 'auth_factory', under='browserid') + add('dbt', 'dbt_factory') + add('retry', 'retry_factory', over='txnmgr', under='exceptionview') + add('browserid', 'browserid_factory') + add('txnmgr', 'txnmgr_factory', under='exceptionview') + add('exceptionview', 'excview_factory', over=MAIN) + self.assertEqual(tweens.implicit(), + [ + ('browserid', 'browserid_factory'), + ('auth', 'auth_factory'), + ('dbt', 'dbt_factory'), + ('exceptionview', 'excview_factory'), + ('retry', 'retry_factory'), + ('txnmgr', 'txnmgr_factory'), + ]) + + def test_implicit_ordering_4(self): + from pyramid.tweens import MAIN + tweens = self._makeOne() + add = tweens.add_implicit + add('exceptionview', 'excview_factory', over=MAIN) + add('auth', 'auth_factory', under='browserid') + add('retry', 'retry_factory', over='txnmgr', under='exceptionview') + add('browserid', 'browserid_factory') + add('txnmgr', 'txnmgr_factory', under='exceptionview') + add('dbt', 'dbt_factory') + self.assertEqual(tweens.implicit(), + [ + ('dbt', 'dbt_factory'), + ('browserid', 'browserid_factory'), + ('auth', 'auth_factory'), + ('exceptionview', 'excview_factory'), + ('retry', 'retry_factory'), + ('txnmgr', 'txnmgr_factory'), + ]) + + def test_implicit_ordering_5(self): + from pyramid.tweens import MAIN, INGRESS + tweens = self._makeOne() + add = tweens.add_implicit + add('exceptionview', 'excview_factory', over=MAIN) + add('auth', 'auth_factory', under=INGRESS) + add('retry', 'retry_factory', over='txnmgr', under='exceptionview') + add('browserid', 'browserid_factory', under=INGRESS) + add('txnmgr', 'txnmgr_factory', under='exceptionview', over=MAIN) + add('dbt', 'dbt_factory') + self.assertEqual(tweens.implicit(), + [ + ('dbt', 'dbt_factory'), + ('browserid', 'browserid_factory'), + ('auth', 'auth_factory'), + ('exceptionview', 'excview_factory'), + ('retry', 'retry_factory'), + ('txnmgr', 'txnmgr_factory'), + ]) + + def test_implicit_ordering_missing_over_partial(self): + from pyramid.exceptions import ConfigurationError + tweens = self._makeOne() + add = tweens.add_implicit + add('dbt', 'dbt_factory') + add('auth', 'auth_factory', under='browserid') + add('retry', 'retry_factory', over='txnmgr', under='exceptionview') + add('browserid', 'browserid_factory') + self.assertRaises(ConfigurationError, tweens.implicit) + + def test_implicit_ordering_missing_under_partial(self): + from pyramid.exceptions import ConfigurationError + tweens = self._makeOne() + add = tweens.add_implicit + add('dbt', 'dbt_factory') + add('auth', 'auth_factory', under='txnmgr') + add('retry', 'retry_factory', over='dbt', under='exceptionview') + add('browserid', 'browserid_factory') + self.assertRaises(ConfigurationError, tweens.implicit) + + def test_implicit_ordering_missing_over_and_under_partials(self): + from pyramid.exceptions import ConfigurationError + tweens = self._makeOne() + add = tweens.add_implicit + add('dbt', 'dbt_factory') + add('auth', 'auth_factory', under='browserid') + add('retry', 'retry_factory', over='foo', under='txnmgr') + add('browserid', 'browserid_factory') + self.assertRaises(ConfigurationError, tweens.implicit) + + def test_implicit_ordering_missing_over_partial_with_fallback(self): + from pyramid.tweens import MAIN + tweens = self._makeOne() + add = tweens.add_implicit + add('exceptionview', 'excview_factory', over=MAIN) + add('auth', 'auth_factory', under='browserid') + add('retry', 'retry_factory', over=('txnmgr',MAIN), + under='exceptionview') + add('browserid', 'browserid_factory') + add('dbt', 'dbt_factory') + self.assertEqual(tweens.implicit(), + [ + ('dbt', 'dbt_factory'), + ('browserid', 'browserid_factory'), + ('auth', 'auth_factory'), + ('exceptionview', 'excview_factory'), + ('retry', 'retry_factory'), + ]) + + def test_implicit_ordering_missing_under_partial_with_fallback(self): + from pyramid.tweens import MAIN + tweens = self._makeOne() + add = tweens.add_implicit + add('exceptionview', 'excview_factory', over=MAIN) + add('auth', 'auth_factory', under=('txnmgr','browserid')) + add('retry', 'retry_factory', under='exceptionview') + add('browserid', 'browserid_factory') + add('dbt', 'dbt_factory') + self.assertEqual(tweens.implicit(), + [ + ('dbt', 'dbt_factory'), + ('browserid', 'browserid_factory'), + ('auth', 'auth_factory'), + ('exceptionview', 'excview_factory'), + ('retry', 'retry_factory'), + ]) + + def test_implicit_ordering_with_partial_fallbacks(self): + from pyramid.tweens import MAIN + tweens = self._makeOne() + add = tweens.add_implicit + add('exceptionview', 'excview_factory', over=('wontbethere', MAIN)) + add('retry', 'retry_factory', under='exceptionview') + add('browserid', 'browserid_factory', over=('wont2', 'exceptionview')) + self.assertEqual(tweens.implicit(), + [ + ('browserid', 'browserid_factory'), + ('exceptionview', 'excview_factory'), + ('retry', 'retry_factory'), + ]) + + def test_implicit_ordering_with_multiple_matching_fallbacks(self): + from pyramid.tweens import MAIN + tweens = self._makeOne() + add = tweens.add_implicit + add('exceptionview', 'excview_factory', over=MAIN) + add('retry', 'retry_factory', under='exceptionview') + add('browserid', 'browserid_factory', over=('retry', 'exceptionview')) + self.assertEqual(tweens.implicit(), + [ + ('browserid', 'browserid_factory'), + ('exceptionview', 'excview_factory'), + ('retry', 'retry_factory'), + ]) + + def test_implicit_ordering_with_missing_fallbacks(self): + from pyramid.exceptions import ConfigurationError + from pyramid.tweens import MAIN + tweens = self._makeOne() + add = tweens.add_implicit + add('exceptionview', 'excview_factory', over=MAIN) + add('retry', 'retry_factory', under='exceptionview') + add('browserid', 'browserid_factory', over=('txnmgr', 'auth')) + self.assertRaises(ConfigurationError, tweens.implicit) + + def test_implicit_ordering_conflict_direct(self): + from pyramid.exceptions import CyclicDependencyError + tweens = self._makeOne() + add = tweens.add_implicit + add('browserid', 'browserid_factory') + add('auth', 'auth_factory', over='browserid', under='browserid') + self.assertRaises(CyclicDependencyError, tweens.implicit) + + def test_implicit_ordering_conflict_indirect(self): + from pyramid.exceptions import CyclicDependencyError + tweens = self._makeOne() + add = tweens.add_implicit + add('browserid', 'browserid_factory') + add('auth', 'auth_factory', over='browserid') + add('dbt', 'dbt_factory', under='browserid', over='auth') + self.assertRaises(CyclicDependencyError, tweens.implicit) + diff --git a/src/pyramid/tests/test_config/test_util.py b/src/pyramid/tests/test_config/test_util.py new file mode 100644 index 000000000..540f3d14c --- /dev/null +++ b/src/pyramid/tests/test_config/test_util.py @@ -0,0 +1,497 @@ +import unittest + +from pyramid.compat import text_ + +class TestActionInfo(unittest.TestCase): + def _getTargetClass(self): + from pyramid.config.util import ActionInfo + return ActionInfo + + def _makeOne(self, filename, lineno, function, linerepr): + return self._getTargetClass()(filename, lineno, function, linerepr) + + def test_class_conforms(self): + from zope.interface.verify import verifyClass + from pyramid.interfaces import IActionInfo + verifyClass(IActionInfo, self._getTargetClass()) + + def test_instance_conforms(self): + from zope.interface.verify import verifyObject + from pyramid.interfaces import IActionInfo + verifyObject(IActionInfo, self._makeOne('f', 0, 'f', 'f')) + + def test_ctor(self): + inst = self._makeOne('filename', 10, 'function', 'src') + self.assertEqual(inst.file, 'filename') + self.assertEqual(inst.line, 10) + self.assertEqual(inst.function, 'function') + self.assertEqual(inst.src, 'src') + + def test___str__(self): + inst = self._makeOne('filename', 0, 'function', ' linerepr ') + self.assertEqual(str(inst), + "Line 0 of file filename:\n linerepr ") + + +class TestPredicateList(unittest.TestCase): + + def _makeOne(self): + from pyramid.config.util import PredicateList + from pyramid import predicates + inst = PredicateList() + for name, factory in ( + ('xhr', predicates.XHRPredicate), + ('request_method', predicates.RequestMethodPredicate), + ('path_info', predicates.PathInfoPredicate), + ('request_param', predicates.RequestParamPredicate), + ('header', predicates.HeaderPredicate), + ('accept', predicates.AcceptPredicate), + ('containment', predicates.ContainmentPredicate), + ('request_type', predicates.RequestTypePredicate), + ('match_param', predicates.MatchParamPredicate), + ('custom', predicates.CustomPredicate), + ('traverse', predicates.TraversePredicate), + ): + inst.add(name, factory) + return inst + + def _callFUT(self, **kw): + inst = self._makeOne() + config = DummyConfigurator() + return inst.make(config, **kw) + + def test_ordering_xhr_and_request_method_trump_only_containment(self): + order1, _, _ = self._callFUT(xhr=True, request_method='GET') + order2, _, _ = self._callFUT(containment=True) + self.assertTrue(order1 < order2) + + def test_ordering_number_of_predicates(self): + from pyramid.config.util import predvalseq + order1, _, _ = self._callFUT( + xhr='xhr', + request_method='request_method', + path_info='path_info', + request_param='param', + match_param='foo=bar', + header='header', + accept='accept', + containment='containment', + request_type='request_type', + custom=predvalseq([DummyCustomPredicate()]), + ) + order2, _, _ = self._callFUT( + xhr='xhr', + request_method='request_method', + path_info='path_info', + request_param='param', + match_param='foo=bar', + header='header', + accept='accept', + containment='containment', + request_type='request_type', + custom=predvalseq([DummyCustomPredicate()]), + ) + order3, _, _ = self._callFUT( + xhr='xhr', + request_method='request_method', + path_info='path_info', + request_param='param', + match_param='foo=bar', + header='header', + accept='accept', + containment='containment', + request_type='request_type', + ) + order4, _, _ = self._callFUT( + xhr='xhr', + request_method='request_method', + path_info='path_info', + request_param='param', + match_param='foo=bar', + header='header', + accept='accept', + containment='containment', + ) + order5, _, _ = self._callFUT( + xhr='xhr', + request_method='request_method', + path_info='path_info', + request_param='param', + match_param='foo=bar', + header='header', + accept='accept', + ) + order6, _, _ = self._callFUT( + xhr='xhr', + request_method='request_method', + path_info='path_info', + request_param='param', + match_param='foo=bar', + header='header', + ) + order7, _, _ = self._callFUT( + xhr='xhr', + request_method='request_method', + path_info='path_info', + request_param='param', + match_param='foo=bar', + ) + order8, _, _ = self._callFUT( + xhr='xhr', + request_method='request_method', + path_info='path_info', + request_param='param', + ) + order9, _, _ = self._callFUT( + xhr='xhr', + request_method='request_method', + path_info='path_info', + ) + order10, _, _ = self._callFUT( + xhr='xhr', + request_method='request_method', + ) + order11, _, _ = self._callFUT( + xhr='xhr', + ) + order12, _, _ = self._callFUT( + ) + self.assertEqual(order1, order2) + self.assertTrue(order3 > order2) + self.assertTrue(order4 > order3) + self.assertTrue(order5 > order4) + self.assertTrue(order6 > order5) + self.assertTrue(order7 > order6) + self.assertTrue(order8 > order7) + self.assertTrue(order9 > order8) + self.assertTrue(order10 > order9) + self.assertTrue(order11 > order10) + self.assertTrue(order12 > order10) + + def test_ordering_importance_of_predicates(self): + from pyramid.config.util import predvalseq + order1, _, _ = self._callFUT( + xhr='xhr', + ) + order2, _, _ = self._callFUT( + request_method='request_method', + ) + order3, _, _ = self._callFUT( + path_info='path_info', + ) + order4, _, _ = self._callFUT( + request_param='param', + ) + order5, _, _ = self._callFUT( + header='header', + ) + order6, _, _ = self._callFUT( + accept='accept', + ) + order7, _, _ = self._callFUT( + containment='containment', + ) + order8, _, _ = self._callFUT( + request_type='request_type', + ) + order9, _, _ = self._callFUT( + match_param='foo=bar', + ) + order10, _, _ = self._callFUT( + custom=predvalseq([DummyCustomPredicate()]), + ) + self.assertTrue(order1 > order2) + self.assertTrue(order2 > order3) + self.assertTrue(order3 > order4) + self.assertTrue(order4 > order5) + self.assertTrue(order5 > order6) + self.assertTrue(order6 > order7) + self.assertTrue(order7 > order8) + self.assertTrue(order8 > order9) + self.assertTrue(order9 > order10) + + def test_ordering_importance_and_number(self): + from pyramid.config.util import predvalseq + order1, _, _ = self._callFUT( + xhr='xhr', + request_method='request_method', + ) + order2, _, _ = self._callFUT( + custom=predvalseq([DummyCustomPredicate()]), + ) + self.assertTrue(order1 < order2) + + order1, _, _ = self._callFUT( + xhr='xhr', + request_method='request_method', + ) + order2, _, _ = self._callFUT( + request_method='request_method', + custom=predvalseq([DummyCustomPredicate()]), + ) + self.assertTrue(order1 > order2) + + order1, _, _ = self._callFUT( + xhr='xhr', + request_method='request_method', + path_info='path_info', + ) + order2, _, _ = self._callFUT( + request_method='request_method', + custom=predvalseq([DummyCustomPredicate()]), + ) + self.assertTrue(order1 < order2) + + order1, _, _ = self._callFUT( + xhr='xhr', + request_method='request_method', + path_info='path_info', + ) + order2, _, _ = self._callFUT( + xhr='xhr', + request_method='request_method', + custom=predvalseq([DummyCustomPredicate()]), + ) + self.assertTrue(order1 > order2) + + def test_different_custom_predicates_with_same_hash(self): + from pyramid.config.util import predvalseq + class PredicateWithHash(object): + def __hash__(self): + return 1 + a = PredicateWithHash() + b = PredicateWithHash() + _, _, a_phash = self._callFUT(custom=predvalseq([a])) + _, _, b_phash = self._callFUT(custom=predvalseq([b])) + self.assertEqual(a_phash, b_phash) + + def test_traverse_has_remainder_already(self): + order, predicates, phash = self._callFUT(traverse='/1/:a/:b') + self.assertEqual(len(predicates), 1) + pred = predicates[0] + info = {'traverse':'abc'} + request = DummyRequest() + result = pred(info, request) + self.assertEqual(result, True) + self.assertEqual(info, {'traverse':'abc'}) + + def test_traverse_matches(self): + order, predicates, phash = self._callFUT(traverse='/1/:a/:b') + self.assertEqual(len(predicates), 1) + pred = predicates[0] + info = {'match':{'a':'a', 'b':'b'}} + request = DummyRequest() + result = pred(info, request) + self.assertEqual(result, True) + self.assertEqual(info, {'match': + {'a':'a', 'b':'b', 'traverse':('1', 'a', 'b')}}) + + def test_traverse_matches_with_highorder_chars(self): + order, predicates, phash = self._callFUT( + traverse=text_(b'/La Pe\xc3\xb1a/{x}', 'utf-8')) + self.assertEqual(len(predicates), 1) + pred = predicates[0] + info = {'match':{'x':text_(b'Qu\xc3\xa9bec', 'utf-8')}} + request = DummyRequest() + result = pred(info, request) + self.assertEqual(result, True) + self.assertEqual( + info['match']['traverse'], + (text_(b'La Pe\xc3\xb1a', 'utf-8'), + text_(b'Qu\xc3\xa9bec', 'utf-8')) + ) + + def test_custom_predicates_can_affect_traversal(self): + from pyramid.config.util import predvalseq + def custom(info, request): + m = info['match'] + m['dummy'] = 'foo' + return True + _, predicates, _ = self._callFUT( + custom=predvalseq([custom]), + traverse='/1/:dummy/:a') + self.assertEqual(len(predicates), 2) + info = {'match':{'a':'a'}} + request = DummyRequest() + self.assertTrue(all([p(info, request) for p in predicates])) + self.assertEqual(info, {'match': + {'a':'a', 'dummy':'foo', + 'traverse':('1', 'foo', 'a')}}) + + def test_predicate_text_is_correct(self): + from pyramid.config.util import predvalseq + _, predicates, _ = self._callFUT( + xhr='xhr', + request_method='request_method', + path_info='path_info', + request_param='param', + header='header', + accept='accept', + containment='containment', + request_type='request_type', + custom=predvalseq( + [ + DummyCustomPredicate(), + DummyCustomPredicate.classmethod_predicate, + DummyCustomPredicate.classmethod_predicate_no_text, + ] + ), + match_param='foo=bar') + self.assertEqual(predicates[0].text(), 'xhr = True') + self.assertEqual(predicates[1].text(), + "request_method = request_method") + self.assertEqual(predicates[2].text(), 'path_info = path_info') + self.assertEqual(predicates[3].text(), 'request_param param') + self.assertEqual(predicates[4].text(), 'header header') + self.assertEqual(predicates[5].text(), 'accept = accept') + self.assertEqual(predicates[6].text(), 'containment = containment') + self.assertEqual(predicates[7].text(), 'request_type = request_type') + self.assertEqual(predicates[8].text(), "match_param foo=bar") + self.assertEqual(predicates[9].text(), 'custom predicate') + self.assertEqual(predicates[10].text(), 'classmethod predicate') + self.assertTrue(predicates[11].text().startswith('custom predicate')) + + def test_match_param_from_string(self): + _, predicates, _ = self._callFUT(match_param='foo=bar') + request = DummyRequest() + request.matchdict = {'foo':'bar', 'baz':'bum'} + self.assertTrue(predicates[0](Dummy(), request)) + + def test_match_param_from_string_fails(self): + _, predicates, _ = self._callFUT(match_param='foo=bar') + request = DummyRequest() + request.matchdict = {'foo':'bum', 'baz':'bum'} + self.assertFalse(predicates[0](Dummy(), request)) + + def test_match_param_from_dict(self): + _, predicates, _ = self._callFUT(match_param=('foo=bar','baz=bum')) + request = DummyRequest() + request.matchdict = {'foo':'bar', 'baz':'bum'} + self.assertTrue(predicates[0](Dummy(), request)) + + def test_match_param_from_dict_fails(self): + _, predicates, _ = self._callFUT(match_param=('foo=bar','baz=bum')) + request = DummyRequest() + request.matchdict = {'foo':'bar', 'baz':'foo'} + self.assertFalse(predicates[0](Dummy(), request)) + + def test_request_method_sequence(self): + _, predicates, _ = self._callFUT(request_method=('GET', 'HEAD')) + request = DummyRequest() + request.method = 'HEAD' + self.assertTrue(predicates[0](Dummy(), request)) + request.method = 'GET' + self.assertTrue(predicates[0](Dummy(), request)) + request.method = 'POST' + self.assertFalse(predicates[0](Dummy(), request)) + + def test_request_method_ordering_hashes_same(self): + hash1, _, __= self._callFUT(request_method=('GET', 'HEAD')) + hash2, _, __= self._callFUT(request_method=('HEAD', 'GET')) + self.assertEqual(hash1, hash2) + hash1, _, __= self._callFUT(request_method=('GET',)) + hash2, _, __= self._callFUT(request_method='GET') + self.assertEqual(hash1, hash2) + + def test_unknown_predicate(self): + from pyramid.exceptions import ConfigurationError + self.assertRaises(ConfigurationError, self._callFUT, unknown=1) + + def test_predicate_close_matches(self): + from pyramid.exceptions import ConfigurationError + with self.assertRaises(ConfigurationError) as context: + self._callFUT(method='GET') + expected_msg = ( + "Unknown predicate values: {'method': 'GET'} " + "(did you mean request_method)" + ) + self.assertEqual(context.exception.args[0], expected_msg) + + def test_notted(self): + from pyramid.config import not_ + from pyramid.testing import DummyRequest + request = DummyRequest() + _, predicates, _ = self._callFUT( + xhr='xhr', + request_method=not_('POST'), + header=not_('header'), + ) + self.assertEqual(predicates[0].text(), 'xhr = True') + self.assertEqual(predicates[1].text(), + "!request_method = POST") + self.assertEqual(predicates[2].text(), '!header header') + self.assertEqual(predicates[1](None, request), True) + self.assertEqual(predicates[2](None, request), True) + +class TestDeprecatedPredicates(unittest.TestCase): + def test_it(self): + import warnings + with warnings.catch_warnings(record=True) as w: + warnings.filterwarnings('always') + from pyramid.config.predicates import XHRPredicate + self.assertEqual(len(w), 1) + +class Test_sort_accept_offers(unittest.TestCase): + def _callFUT(self, offers, order=None): + from pyramid.config.util import sort_accept_offers + return sort_accept_offers(offers, order) + + def test_default_specificities(self): + result = self._callFUT(['text/html', 'text/html;charset=utf8']) + self.assertEqual(result, [ + 'text/html;charset=utf8', 'text/html', + ]) + + def test_specific_type_order(self): + result = self._callFUT( + ['text/html', 'application/json', 'text/html;charset=utf8', 'text/plain'], + ['application/json', 'text/html'], + ) + self.assertEqual(result, [ + 'application/json', 'text/html;charset=utf8', 'text/html', 'text/plain', + ]) + + def test_params_order(self): + result = self._callFUT( + ['text/html;charset=utf8', 'text/html;charset=latin1', 'text/html;foo=bar'], + ['text/html;charset=latin1', 'text/html;charset=utf8'], + ) + self.assertEqual(result, [ + 'text/html;charset=latin1', 'text/html;charset=utf8', 'text/html;foo=bar', + ]) + + def test_params_inherit_type_prefs(self): + result = self._callFUT( + ['text/html;charset=utf8', 'text/plain;charset=latin1'], + ['text/plain', 'text/html'], + ) + self.assertEqual(result, ['text/plain;charset=latin1', 'text/html;charset=utf8']) + +class DummyCustomPredicate(object): + def __init__(self): + self.__text__ = 'custom predicate' + + def classmethod_predicate(*args): pass + classmethod_predicate.__text__ = 'classmethod predicate' + classmethod_predicate = classmethod(classmethod_predicate) + + @classmethod + def classmethod_predicate_no_text(*args): pass # pragma: no cover + +class Dummy(object): + def __init__(self, **kw): + self.__dict__.update(**kw) + +class DummyRequest: + subpath = () + matchdict = None + def __init__(self, environ=None): + if environ is None: + environ = {} + self.environ = environ + self.params = {} + self.cookies = {} + +class DummyConfigurator(object): + def maybe_dotted(self, thing): + return thing diff --git a/src/pyramid/tests/test_config/test_views.py b/src/pyramid/tests/test_config/test_views.py new file mode 100644 index 000000000..6565a35d5 --- /dev/null +++ b/src/pyramid/tests/test_config/test_views.py @@ -0,0 +1,3632 @@ +import os +import unittest +from pyramid import testing + +from pyramid.tests.test_config import IDummy + +from pyramid.tests.test_config import dummy_view + +from pyramid.compat import ( + im_func, + text_, + ) +from pyramid.exceptions import ConfigurationError +from pyramid.exceptions import ConfigurationExecutionError +from pyramid.exceptions import ConfigurationConflictError + +class TestViewsConfigurationMixin(unittest.TestCase): + def _makeOne(self, *arg, **kw): + from pyramid.config import Configurator + config = Configurator(*arg, **kw) + config.set_default_csrf_options(require_csrf=False) + return config + + def _getViewCallable(self, config, ctx_iface=None, exc_iface=None, + request_iface=None, name=''): + from zope.interface import Interface + from pyramid.interfaces import IRequest + from pyramid.interfaces import IView + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IExceptionViewClassifier + if exc_iface: + classifier = IExceptionViewClassifier + ctx_iface = exc_iface + else: + classifier = IViewClassifier + if ctx_iface is None: + ctx_iface = Interface + if request_iface is None: + request_iface = IRequest + return config.registry.adapters.lookup( + (classifier, request_iface, ctx_iface), IView, name=name, + default=None) + + def _registerRenderer(self, config, name='.txt'): + from pyramid.interfaces import IRendererFactory + class Renderer: + def __init__(self, info): + self.__class__.info = info + def __call__(self, *arg): + return b'Hello!' + config.registry.registerUtility(Renderer, IRendererFactory, name=name) + return Renderer + + def _makeRequest(self, config): + request = DummyRequest() + request.registry = config.registry + return request + + def _assertNotFound(self, wrapper, *arg): + from pyramid.httpexceptions import HTTPNotFound + self.assertRaises(HTTPNotFound, wrapper, *arg) + + def _getRouteRequestIface(self, config, name): + from pyramid.interfaces import IRouteRequest + iface = config.registry.getUtility(IRouteRequest, name) + return iface + + def _assertRoute(self, config, name, path, num_predicates=0): + from pyramid.interfaces import IRoutesMapper + mapper = config.registry.getUtility(IRoutesMapper) + routes = mapper.get_routes() + route = routes[0] + self.assertEqual(len(routes), 1) + self.assertEqual(route.name, name) + self.assertEqual(route.path, path) + self.assertEqual(len(routes[0].predicates), num_predicates) + return route + + def test_add_view_view_callable_None_no_renderer(self): + config = self._makeOne(autocommit=True) + self.assertRaises(ConfigurationError, config.add_view) + + def test_add_view_with_request_type_and_route_name(self): + config = self._makeOne(autocommit=True) + view = lambda *arg: 'OK' + self.assertRaises(ConfigurationError, config.add_view, view, '', None, + None, True, True) + + def test_add_view_with_request_type(self): + from pyramid.renderers import null_renderer + from zope.interface import directlyProvides + from pyramid.interfaces import IRequest + view = lambda *arg: 'OK' + config = self._makeOne(autocommit=True) + config.add_view(view=view, + request_type='pyramid.interfaces.IRequest', + renderer=null_renderer) + wrapper = self._getViewCallable(config) + request = DummyRequest() + self._assertNotFound(wrapper, None, request) + directlyProvides(request, IRequest) + result = wrapper(None, request) + self.assertEqual(result, 'OK') + + def test_add_view_view_callable_None_with_renderer(self): + config = self._makeOne(autocommit=True) + self._registerRenderer(config, name='dummy') + config.add_view(renderer='dummy') + view = self._getViewCallable(config) + self.assertTrue(b'Hello!' in view(None, None).body) + + def test_add_view_with_tmpl_renderer_factory_introspector_missing(self): + config = self._makeOne(autocommit=True) + config.introspection = False + config.introspector = None + config.add_view(renderer='dummy.pt') + view = self._getViewCallable(config) + self.assertRaises(ValueError, view, None, None) + + def test_add_view_with_tmpl_renderer_factory_no_renderer_factory(self): + config = self._makeOne(autocommit=True) + introspector = DummyIntrospector() + config.introspector = introspector + config.add_view(renderer='dummy.pt') + self.assertFalse(('renderer factories', '.pt') in + introspector.related[-1]) + view = self._getViewCallable(config) + self.assertRaises(ValueError, view, None, None) + + def test_add_view_with_tmpl_renderer_factory_with_renderer_factory(self): + config = self._makeOne(autocommit=True) + introspector = DummyIntrospector(True) + config.introspector = introspector + def dummy_factory(helper): + return lambda val, system_vals: 'Hello!' + config.add_renderer('.pt', dummy_factory) + config.add_view(renderer='dummy.pt') + self.assertTrue( + ('renderer factories', '.pt') in introspector.related[-1]) + view = self._getViewCallable(config) + self.assertTrue(b'Hello!' in view(None, None).body) + + def test_add_view_wrapped_view_is_decorated(self): + def view(request): # request-only wrapper + """ """ + config = self._makeOne(autocommit=True) + config.add_view(view=view) + wrapper = self._getViewCallable(config) + self.assertEqual(wrapper.__module__, view.__module__) + self.assertEqual(wrapper.__name__, view.__name__) + self.assertEqual(wrapper.__doc__, view.__doc__) + self.assertEqual(wrapper.__discriminator__(None, None).resolve()[0], + 'view') + + def test_add_view_view_callable_dottedname(self): + from pyramid.renderers import null_renderer + config = self._makeOne(autocommit=True) + config.add_view(view='pyramid.tests.test_config.dummy_view', + renderer=null_renderer) + wrapper = self._getViewCallable(config) + self.assertEqual(wrapper(None, None), 'OK') + + def test_add_view_with_function_callable(self): + from pyramid.renderers import null_renderer + view = lambda *arg: 'OK' + config = self._makeOne(autocommit=True) + config.add_view(view=view, renderer=null_renderer) + wrapper = self._getViewCallable(config) + result = wrapper(None, None) + self.assertEqual(result, 'OK') + + def test_add_view_with_function_callable_requestonly(self): + from pyramid.renderers import null_renderer + def view(request): + return 'OK' + config = self._makeOne(autocommit=True) + config.add_view(view=view, renderer=null_renderer) + wrapper = self._getViewCallable(config) + result = wrapper(None, None) + self.assertEqual(result, 'OK') + + def test_add_view_with_name(self): + from pyramid.renderers import null_renderer + view = lambda *arg: 'OK' + config = self._makeOne(autocommit=True) + config.add_view(view=view, name='abc', renderer=null_renderer) + wrapper = self._getViewCallable(config, name='abc') + result = wrapper(None, None) + self.assertEqual(result, 'OK') + + def test_add_view_with_name_unicode(self): + from pyramid.renderers import null_renderer + view = lambda *arg: 'OK' + config = self._makeOne(autocommit=True) + name = text_(b'La Pe\xc3\xb1a', 'utf-8') + config.add_view(view=view, name=name, renderer=null_renderer) + wrapper = self._getViewCallable(config, name=name) + result = wrapper(None, None) + self.assertEqual(result, 'OK') + + def test_add_view_with_decorator(self): + from pyramid.renderers import null_renderer + def view(request): + """ ABC """ + return 'OK' + def view_wrapper(fn): + def inner(context, request): + return fn(context, request) + return inner + config = self._makeOne(autocommit=True) + config.add_view(view=view, decorator=view_wrapper, + renderer=null_renderer) + wrapper = self._getViewCallable(config) + self.assertFalse(wrapper is view) + self.assertEqual(wrapper.__doc__, view.__doc__) + result = wrapper(None, None) + self.assertEqual(result, 'OK') + + def test_add_view_with_decorator_tuple(self): + from pyramid.renderers import null_renderer + def view(request): + """ ABC """ + return 'OK' + def view_wrapper1(fn): + def inner(context, request): + return 'wrapped1' + fn(context, request) + return inner + def view_wrapper2(fn): + def inner(context, request): + return 'wrapped2' + fn(context, request) + return inner + config = self._makeOne(autocommit=True) + config.add_view(view=view, decorator=(view_wrapper2, view_wrapper1), + renderer=null_renderer) + wrapper = self._getViewCallable(config) + self.assertFalse(wrapper is view) + self.assertEqual(wrapper.__doc__, view.__doc__) + result = wrapper(None, None) + self.assertEqual(result, 'wrapped2wrapped1OK') + + def test_add_view_with_http_cache(self): + import datetime + from pyramid.response import Response + response = Response('OK') + def view(request): + """ ABC """ + return response + config = self._makeOne(autocommit=True) + config.add_view(view=view, http_cache=(86400, {'public':True})) + wrapper = self._getViewCallable(config) + self.assertFalse(wrapper is view) + self.assertEqual(wrapper.__doc__, view.__doc__) + request = testing.DummyRequest() + when = datetime.datetime.utcnow() + datetime.timedelta(days=1) + result = wrapper(None, request) + self.assertEqual(result, response) + headers = dict(response.headerlist) + self.assertEqual(headers['Cache-Control'], 'max-age=86400, public') + expires = parse_httpdate(headers['Expires']) + assert_similar_datetime(expires, when) + + def test_add_view_as_instance(self): + from pyramid.renderers import null_renderer + class AView: + def __call__(self, context, request): + """ """ + return 'OK' + view = AView() + config = self._makeOne(autocommit=True) + config.add_view(view=view, renderer=null_renderer) + wrapper = self._getViewCallable(config) + result = wrapper(None, None) + self.assertEqual(result, 'OK') + + def test_add_view_as_instancemethod(self): + from pyramid.renderers import null_renderer + class View: + def index(self, context, request): + return 'OK' + view = View() + config=self._makeOne(autocommit=True) + config.add_view(view=view.index, renderer=null_renderer) + wrapper = self._getViewCallable(config) + result = wrapper(None, None) + self.assertEqual(result, 'OK') + + def test_add_view_as_instancemethod_requestonly(self): + from pyramid.renderers import null_renderer + class View: + def index(self, request): + return 'OK' + view = View() + config=self._makeOne(autocommit=True) + config.add_view(view=view.index, renderer=null_renderer) + wrapper = self._getViewCallable(config) + result = wrapper(None, None) + self.assertEqual(result, 'OK') + + def test_add_view_as_instance_requestonly(self): + from pyramid.renderers import null_renderer + class AView: + def __call__(self, request): + """ """ + return 'OK' + view = AView() + config = self._makeOne(autocommit=True) + config.add_view(view=view, renderer=null_renderer) + wrapper = self._getViewCallable(config) + result = wrapper(None, None) + self.assertEqual(result, 'OK') + + def test_add_view_as_oldstyle_class(self): + from pyramid.renderers import null_renderer + class view: + def __init__(self, context, request): + self.context = context + self.request = request + + def __call__(self): + return 'OK' + config = self._makeOne(autocommit=True) + config.add_view(view=view, renderer=null_renderer) + wrapper = self._getViewCallable(config) + request = self._makeRequest(config) + result = wrapper(None, request) + self.assertEqual(result, 'OK') + self.assertEqual(request.__view__.__class__, view) + + def test_add_view_as_oldstyle_class_requestonly(self): + from pyramid.renderers import null_renderer + class view: + def __init__(self, request): + self.request = request + + def __call__(self): + return 'OK' + config = self._makeOne(autocommit=True) + config.add_view(view=view, renderer=null_renderer) + wrapper = self._getViewCallable(config) + + request = self._makeRequest(config) + result = wrapper(None, request) + self.assertEqual(result, 'OK') + self.assertEqual(request.__view__.__class__, view) + + def test_add_view_context_as_class(self): + from pyramid.renderers import null_renderer + from zope.interface import implementedBy + view = lambda *arg: 'OK' + class Foo: + pass + config = self._makeOne(autocommit=True) + config.add_view(context=Foo, view=view, renderer=null_renderer) + foo = implementedBy(Foo) + wrapper = self._getViewCallable(config, foo) + self.assertEqual(wrapper, view) + + def test_add_view_context_as_iface(self): + from pyramid.renderers import null_renderer + view = lambda *arg: 'OK' + config = self._makeOne(autocommit=True) + config.add_view(context=IDummy, view=view, renderer=null_renderer) + wrapper = self._getViewCallable(config, IDummy) + self.assertEqual(wrapper, view) + + def test_add_view_context_as_dottedname(self): + from pyramid.renderers import null_renderer + view = lambda *arg: 'OK' + config = self._makeOne(autocommit=True) + config.add_view(context='pyramid.tests.test_config.IDummy', + view=view, renderer=null_renderer) + wrapper = self._getViewCallable(config, IDummy) + self.assertEqual(wrapper, view) + + def test_add_view_for__as_dottedname(self): + from pyramid.renderers import null_renderer + view = lambda *arg: 'OK' + config = self._makeOne(autocommit=True) + config.add_view(for_='pyramid.tests.test_config.IDummy', + view=view, renderer=null_renderer) + wrapper = self._getViewCallable(config, IDummy) + self.assertEqual(wrapper, view) + + def test_add_view_for_as_class(self): + # ``for_`` is older spelling for ``context`` + from pyramid.renderers import null_renderer + from zope.interface import implementedBy + view = lambda *arg: 'OK' + class Foo: + pass + config = self._makeOne(autocommit=True) + config.add_view(for_=Foo, view=view, renderer=null_renderer) + foo = implementedBy(Foo) + wrapper = self._getViewCallable(config, foo) + self.assertEqual(wrapper, view) + + def test_add_view_for_as_iface(self): + # ``for_`` is older spelling for ``context`` + from pyramid.renderers import null_renderer + view = lambda *arg: 'OK' + config = self._makeOne(autocommit=True) + config.add_view(for_=IDummy, view=view, renderer=null_renderer) + wrapper = self._getViewCallable(config, IDummy) + self.assertEqual(wrapper, view) + + def test_add_view_context_trumps_for(self): + # ``for_`` is older spelling for ``context`` + from pyramid.renderers import null_renderer + view = lambda *arg: 'OK' + config = self._makeOne(autocommit=True) + class Foo: + pass + config.add_view(context=IDummy, for_=Foo, view=view, + renderer=null_renderer) + wrapper = self._getViewCallable(config, IDummy) + self.assertEqual(wrapper, view) + + def test_add_view_register_secured_view(self): + from pyramid.renderers import null_renderer + from zope.interface import Interface + from pyramid.interfaces import IRequest + from pyramid.interfaces import ISecuredView + from pyramid.interfaces import IViewClassifier + view = lambda *arg: 'OK' + view.__call_permissive__ = view + config = self._makeOne(autocommit=True) + config.add_view(view=view, renderer=null_renderer) + wrapper = config.registry.adapters.lookup( + (IViewClassifier, IRequest, Interface), + ISecuredView, name='', default=None) + self.assertEqual(wrapper, view) + + def test_add_view_exception_register_secured_view(self): + from pyramid.renderers import null_renderer + from zope.interface import implementedBy + from pyramid.interfaces import IRequest + from pyramid.interfaces import IView + from pyramid.interfaces import IExceptionViewClassifier + view = lambda *arg: 'OK' + view.__call_permissive__ = view + config = self._makeOne(autocommit=True) + config.add_view(view=view, context=RuntimeError, renderer=null_renderer) + wrapper = config.registry.adapters.lookup( + (IExceptionViewClassifier, IRequest, implementedBy(RuntimeError)), + IView, name='', default=None) + self.assertEqual(wrapper, view) + + def test_add_view_same_phash_overrides_existing_single_view(self): + from pyramid.renderers import null_renderer + from hashlib import md5 + from zope.interface import Interface + from pyramid.interfaces import IRequest + from pyramid.interfaces import IView + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IMultiView + phash = md5() + phash.update(b'xhr = True') + view = lambda *arg: 'NOT OK' + view.__phash__ = phash.hexdigest() + config = self._makeOne(autocommit=True) + config.registry.registerAdapter( + view, (IViewClassifier, IRequest, Interface), IView, name='') + def newview(context, request): + return 'OK' + config.add_view(view=newview, xhr=True, renderer=null_renderer) + wrapper = self._getViewCallable(config) + self.assertFalse(IMultiView.providedBy(wrapper)) + request = DummyRequest() + request.is_xhr = True + self.assertEqual(wrapper(None, request), 'OK') + + def test_add_view_exc_same_phash_overrides_existing_single_view(self): + from pyramid.renderers import null_renderer + from hashlib import md5 + from zope.interface import implementedBy + from pyramid.interfaces import IRequest + from pyramid.interfaces import IView + from pyramid.interfaces import IExceptionViewClassifier + from pyramid.interfaces import IMultiView + phash = md5() + phash.update(b'xhr = True') + view = lambda *arg: 'NOT OK' + view.__phash__ = phash.hexdigest() + config = self._makeOne(autocommit=True) + config.registry.registerAdapter( + view, + (IExceptionViewClassifier, IRequest, implementedBy(RuntimeError)), + IView, name='') + def newview(context, request): + return 'OK' + config.add_view(view=newview, xhr=True, context=RuntimeError, + renderer=null_renderer) + wrapper = self._getViewCallable( + config, exc_iface=implementedBy(RuntimeError)) + self.assertFalse(IMultiView.providedBy(wrapper)) + request = DummyRequest() + request.is_xhr = True + self.assertEqual(wrapper(None, request), 'OK') + + def test_add_view_default_phash_overrides_no_phash(self): + from pyramid.renderers import null_renderer + from zope.interface import Interface + from pyramid.interfaces import IRequest + from pyramid.interfaces import IView + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IMultiView + view = lambda *arg: 'NOT OK' + config = self._makeOne(autocommit=True) + config.registry.registerAdapter( + view, (IViewClassifier, IRequest, Interface), IView, name='') + def newview(context, request): + return 'OK' + config.add_view(view=newview, renderer=null_renderer) + wrapper = self._getViewCallable(config) + self.assertFalse(IMultiView.providedBy(wrapper)) + request = DummyRequest() + request.is_xhr = True + self.assertEqual(wrapper(None, request), 'OK') + + def test_add_view_exc_default_phash_overrides_no_phash(self): + from pyramid.renderers import null_renderer + from zope.interface import implementedBy + from pyramid.interfaces import IRequest + from pyramid.interfaces import IView + from pyramid.interfaces import IExceptionViewClassifier + from pyramid.interfaces import IMultiView + view = lambda *arg: 'NOT OK' + config = self._makeOne(autocommit=True) + config.registry.registerAdapter( + view, + (IExceptionViewClassifier, IRequest, implementedBy(RuntimeError)), + IView, name='') + def newview(context, request): + return 'OK' + config.add_view(view=newview, context=RuntimeError, + renderer=null_renderer) + wrapper = self._getViewCallable( + config, exc_iface=implementedBy(RuntimeError)) + self.assertFalse(IMultiView.providedBy(wrapper)) + request = DummyRequest() + request.is_xhr = True + self.assertEqual(wrapper(None, request), 'OK') + + def test_add_view_default_phash_overrides_default_phash(self): + from pyramid.config.util import DEFAULT_PHASH + from pyramid.renderers import null_renderer + from zope.interface import Interface + from pyramid.interfaces import IRequest + from pyramid.interfaces import IView + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IMultiView + view = lambda *arg: 'NOT OK' + view.__phash__ = DEFAULT_PHASH + config = self._makeOne(autocommit=True) + config.registry.registerAdapter( + view, (IViewClassifier, IRequest, Interface), IView, name='') + def newview(context, request): + return 'OK' + config.add_view(view=newview, renderer=null_renderer) + wrapper = self._getViewCallable(config) + self.assertFalse(IMultiView.providedBy(wrapper)) + request = DummyRequest() + request.is_xhr = True + self.assertEqual(wrapper(None, request), 'OK') + + def test_add_view_exc_default_phash_overrides_default_phash(self): + from pyramid.config.util import DEFAULT_PHASH + from pyramid.renderers import null_renderer + from zope.interface import implementedBy + from pyramid.interfaces import IRequest + from pyramid.interfaces import IView + from pyramid.interfaces import IExceptionViewClassifier + from pyramid.interfaces import IMultiView + view = lambda *arg: 'NOT OK' + view.__phash__ = DEFAULT_PHASH + config = self._makeOne(autocommit=True) + config.registry.registerAdapter( + view, + (IExceptionViewClassifier, IRequest, implementedBy(RuntimeError)), + IView, name='') + def newview(context, request): + return 'OK' + config.add_view(view=newview, context=RuntimeError, + renderer=null_renderer) + wrapper = self._getViewCallable( + config, exc_iface=implementedBy(RuntimeError)) + self.assertFalse(IMultiView.providedBy(wrapper)) + request = DummyRequest() + request.is_xhr = True + self.assertEqual(wrapper(None, request), 'OK') + + def test_add_view_multiview_replaces_existing_view(self): + from pyramid.renderers import null_renderer + from zope.interface import Interface + from pyramid.interfaces import IRequest + from pyramid.interfaces import IView + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IMultiView + view = lambda *arg: 'OK' + view.__phash__ = 'abc' + config = self._makeOne(autocommit=True) + config.registry.registerAdapter( + view, (IViewClassifier, IRequest, Interface), IView, name='') + config.add_view(view=view, renderer=null_renderer) + wrapper = self._getViewCallable(config) + self.assertTrue(IMultiView.providedBy(wrapper)) + self.assertEqual(wrapper(None, None), 'OK') + + def test_add_view_exc_multiview_replaces_existing_view(self): + from pyramid.renderers import null_renderer + from zope.interface import implementedBy + from pyramid.interfaces import IRequest + from pyramid.interfaces import IView + from pyramid.interfaces import IExceptionViewClassifier + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IMultiView + view = lambda *arg: 'OK' + view.__phash__ = 'abc' + config = self._makeOne(autocommit=True) + config.registry.registerAdapter( + view, + (IViewClassifier, IRequest, implementedBy(RuntimeError)), + IView, name='') + config.registry.registerAdapter( + view, + (IExceptionViewClassifier, IRequest, implementedBy(RuntimeError)), + IView, name='') + config.add_view(view=view, context=RuntimeError, + renderer=null_renderer) + wrapper = self._getViewCallable( + config, exc_iface=implementedBy(RuntimeError)) + self.assertTrue(IMultiView.providedBy(wrapper)) + self.assertEqual(wrapper(None, None), 'OK') + + def test_add_view_multiview_replaces_existing_securedview(self): + from pyramid.renderers import null_renderer + from zope.interface import Interface + from pyramid.interfaces import IRequest + from pyramid.interfaces import ISecuredView + from pyramid.interfaces import IMultiView + from pyramid.interfaces import IViewClassifier + view = lambda *arg: 'OK' + view.__phash__ = 'abc' + config = self._makeOne(autocommit=True) + config.registry.registerAdapter( + view, (IViewClassifier, IRequest, Interface), + ISecuredView, name='') + config.add_view(view=view, renderer=null_renderer) + wrapper = self._getViewCallable(config) + self.assertTrue(IMultiView.providedBy(wrapper)) + self.assertEqual(wrapper(None, None), 'OK') + + def test_add_view_exc_multiview_replaces_existing_securedview(self): + from pyramid.renderers import null_renderer + from zope.interface import implementedBy + from pyramid.interfaces import IRequest + from pyramid.interfaces import ISecuredView + from pyramid.interfaces import IMultiView + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IExceptionViewClassifier + view = lambda *arg: 'OK' + view.__phash__ = 'abc' + config = self._makeOne(autocommit=True) + config.registry.registerAdapter( + view, + (IViewClassifier, IRequest, implementedBy(RuntimeError)), + ISecuredView, name='') + config.registry.registerAdapter( + view, + (IExceptionViewClassifier, IRequest, implementedBy(RuntimeError)), + ISecuredView, name='') + config.add_view(view=view, context=RuntimeError, renderer=null_renderer) + wrapper = self._getViewCallable( + config, exc_iface=implementedBy(RuntimeError)) + self.assertTrue(IMultiView.providedBy(wrapper)) + self.assertEqual(wrapper(None, None), 'OK') + + def test_add_view_with_accept_multiview_replaces_existing_view(self): + from pyramid.renderers import null_renderer + from zope.interface import Interface + from pyramid.interfaces import IRequest + from pyramid.interfaces import IView + from pyramid.interfaces import IMultiView + from pyramid.interfaces import IViewClassifier + def view(context, request): + return 'OK' + def view2(context, request): + return 'OK2' + config = self._makeOne(autocommit=True) + config.registry.registerAdapter( + view, (IViewClassifier, IRequest, Interface), IView, name='') + config.add_view(view=view2, accept='text/html', renderer=null_renderer) + wrapper = self._getViewCallable(config) + self.assertTrue(IMultiView.providedBy(wrapper)) + self.assertEqual(len(wrapper.views), 1) + self.assertEqual(len(wrapper.media_views), 1) + self.assertEqual(wrapper(None, None), 'OK') + request = DummyRequest() + request.accept = DummyAccept('text/html', 'text/html') + self.assertEqual(wrapper(None, request), 'OK2') + + def test_add_view_mixed_case_replaces_existing_view(self): + from pyramid.renderers import null_renderer + def view(context, request): return 'OK' + def view2(context, request): return 'OK2' + def view3(context, request): return 'OK3' + config = self._makeOne(autocommit=True) + config.add_view(view=view, renderer=null_renderer) + config.add_view(view=view2, accept='text/html', renderer=null_renderer) + config.add_view(view=view3, accept='text/HTML', renderer=null_renderer) + wrapper = self._getViewCallable(config) + self.assertTrue(IMultiView.providedBy(wrapper)) + self.assertEqual(len(wrapper.media_views.items()),1) + self.assertFalse('text/HTML' in wrapper.media_views) + self.assertEqual(wrapper(None, None), 'OK') + request = DummyRequest() + request.accept = DummyAccept('text/html', 'text/html') + self.assertEqual(wrapper(None, request), 'OK3') + + def test_add_views_with_accept_multiview_replaces_existing(self): + from pyramid.renderers import null_renderer + def view(context, request): return 'OK' + def view2(context, request): return 'OK2' + def view3(context, request): return 'OK3' + config = self._makeOne(autocommit=True) + config.add_view(view=view, renderer=null_renderer) + config.add_view(view=view2, accept='text/html', renderer=null_renderer) + config.add_view(view=view3, accept='text/html', renderer=null_renderer) + wrapper = self._getViewCallable(config) + self.assertEqual(len(wrapper.media_views['text/html']), 1) + self.assertEqual(wrapper(None, None), 'OK') + request = DummyRequest() + request.accept = DummyAccept('text/html', 'text/html') + self.assertEqual(wrapper(None, request), 'OK3') + + def test_add_view_exc_with_accept_multiview_replaces_existing_view(self): + from pyramid.renderers import null_renderer + from zope.interface import implementedBy + from pyramid.interfaces import IRequest + from pyramid.interfaces import IView + from pyramid.interfaces import IMultiView + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IExceptionViewClassifier + def view(context, request): + return 'OK' + def view2(context, request): + return 'OK2' + config = self._makeOne(autocommit=True) + config.registry.registerAdapter( + view, + (IViewClassifier, IRequest, implementedBy(RuntimeError)), + IView, name='') + config.registry.registerAdapter( + view, + (IExceptionViewClassifier, IRequest, implementedBy(RuntimeError)), + IView, name='') + config.add_view(view=view2, accept='text/html', context=RuntimeError, + renderer=null_renderer) + wrapper = self._getViewCallable( + config, exc_iface=implementedBy(RuntimeError)) + self.assertTrue(IMultiView.providedBy(wrapper)) + self.assertEqual(len(wrapper.views), 1) + self.assertEqual(len(wrapper.media_views), 1) + self.assertEqual(wrapper(None, None), 'OK') + request = DummyRequest() + request.accept = DummyAccept('text/html', 'text/html') + self.assertEqual(wrapper(None, request), 'OK2') + + def test_add_view_multiview_replaces_existing_view_with___accept__(self): + from pyramid.renderers import null_renderer + from zope.interface import Interface + from pyramid.interfaces import IRequest + from pyramid.interfaces import IView + from pyramid.interfaces import IMultiView + from pyramid.interfaces import IViewClassifier + def view(context, request): + return 'OK' + def view2(context, request): + return 'OK2' + view.__accept__ = 'text/html' + view.__phash__ = 'abc' + config = self._makeOne(autocommit=True) + config.registry.registerAdapter( + view, (IViewClassifier, IRequest, Interface), IView, name='') + config.add_view(view=view2, renderer=null_renderer) + wrapper = self._getViewCallable(config) + self.assertTrue(IMultiView.providedBy(wrapper)) + self.assertEqual(len(wrapper.views), 1) + self.assertEqual(len(wrapper.media_views), 1) + self.assertEqual(wrapper(None, None), 'OK2') + request = DummyRequest() + request.accept = DummyAccept('text/html') + self.assertEqual(wrapper(None, request), 'OK') + + def test_add_view_exc_mulview_replaces_existing_view_with___accept__(self): + from pyramid.renderers import null_renderer + from zope.interface import implementedBy + from pyramid.interfaces import IRequest + from pyramid.interfaces import IView + from pyramid.interfaces import IMultiView + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IExceptionViewClassifier + def view(context, request): + return 'OK' + def view2(context, request): + return 'OK2' + view.__accept__ = 'text/html' + view.__phash__ = 'abc' + config = self._makeOne(autocommit=True) + config.registry.registerAdapter( + view, + (IViewClassifier, IRequest, implementedBy(RuntimeError)), + IView, name='') + config.registry.registerAdapter( + view, + (IExceptionViewClassifier, IRequest, implementedBy(RuntimeError)), + IView, name='') + config.add_view(view=view2, context=RuntimeError, + renderer=null_renderer) + wrapper = self._getViewCallable( + config, exc_iface=implementedBy(RuntimeError)) + self.assertTrue(IMultiView.providedBy(wrapper)) + self.assertEqual(len(wrapper.views), 1) + self.assertEqual(len(wrapper.media_views), 1) + self.assertEqual(wrapper(None, None), 'OK2') + request = DummyRequest() + request.accept = DummyAccept('text/html') + self.assertEqual(wrapper(None, request), 'OK') + + def test_add_view_multiview_replaces_multiview(self): + from pyramid.renderers import null_renderer + from zope.interface import Interface + from pyramid.interfaces import IRequest + from pyramid.interfaces import IMultiView + from pyramid.interfaces import IViewClassifier + view = DummyMultiView() + config = self._makeOne(autocommit=True) + config.registry.registerAdapter( + view, (IViewClassifier, IRequest, Interface), + IMultiView, name='') + view2 = lambda *arg: 'OK2' + config.add_view(view=view2, renderer=null_renderer) + wrapper = self._getViewCallable(config) + self.assertTrue(IMultiView.providedBy(wrapper)) + self.assertEqual([(x[0], x[2]) for x in wrapper.views], [(view2, None)]) + self.assertEqual(wrapper(None, None), 'OK1') + + def test_add_view_exc_multiview_replaces_multiviews(self): + from pyramid.renderers import null_renderer + from zope.interface import implementedBy + from pyramid.interfaces import IRequest + from pyramid.interfaces import IMultiView + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IExceptionViewClassifier + hot_view = DummyMultiView() + exc_view = DummyMultiView() + config = self._makeOne(autocommit=True) + config.registry.registerAdapter( + hot_view, + (IViewClassifier, IRequest, implementedBy(RuntimeError)), + IMultiView, name='') + config.registry.registerAdapter( + exc_view, + (IExceptionViewClassifier, IRequest, implementedBy(RuntimeError)), + IMultiView, name='') + view2 = lambda *arg: 'OK2' + config.add_view(view=view2, context=RuntimeError, + renderer=null_renderer) + hot_wrapper = self._getViewCallable( + config, ctx_iface=implementedBy(RuntimeError)) + self.assertTrue(IMultiView.providedBy(hot_wrapper)) + self.assertEqual([(x[0], x[2]) for x in hot_wrapper.views], [(view2, None)]) + self.assertEqual(hot_wrapper(None, None), 'OK1') + + exc_wrapper = self._getViewCallable( + config, exc_iface=implementedBy(RuntimeError)) + self.assertTrue(IMultiView.providedBy(exc_wrapper)) + self.assertEqual([(x[0], x[2]) for x in exc_wrapper.views], [(view2, None)]) + self.assertEqual(exc_wrapper(None, None), 'OK1') + + def test_add_view_exc_multiview_replaces_only_exc_multiview(self): + from pyramid.renderers import null_renderer + from zope.interface import implementedBy + from pyramid.interfaces import IRequest + from pyramid.interfaces import IMultiView + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IExceptionViewClassifier + hot_view = DummyMultiView() + exc_view = DummyMultiView() + config = self._makeOne(autocommit=True) + config.registry.registerAdapter( + hot_view, + (IViewClassifier, IRequest, implementedBy(RuntimeError)), + IMultiView, name='') + config.registry.registerAdapter( + exc_view, + (IExceptionViewClassifier, IRequest, implementedBy(RuntimeError)), + IMultiView, name='') + view2 = lambda *arg: 'OK2' + config.add_view(view=view2, context=RuntimeError, exception_only=True, + renderer=null_renderer) + hot_wrapper = self._getViewCallable( + config, ctx_iface=implementedBy(RuntimeError)) + self.assertTrue(IMultiView.providedBy(hot_wrapper)) + self.assertEqual(len(hot_wrapper.views), 0) + self.assertEqual(hot_wrapper(None, None), 'OK1') + + exc_wrapper = self._getViewCallable( + config, exc_iface=implementedBy(RuntimeError)) + self.assertTrue(IMultiView.providedBy(exc_wrapper)) + self.assertEqual([(x[0], x[2]) for x in exc_wrapper.views], [(view2, None)]) + self.assertEqual(exc_wrapper(None, None), 'OK1') + + def test_add_view_multiview_context_superclass_then_subclass(self): + from pyramid.renderers import null_renderer + from zope.interface import Interface + from pyramid.interfaces import IRequest + from pyramid.interfaces import IView + from pyramid.interfaces import IMultiView + from pyramid.interfaces import IViewClassifier + class ISuper(Interface): + pass + class ISub(ISuper): + pass + view = lambda *arg: 'OK' + view2 = lambda *arg: 'OK2' + config = self._makeOne(autocommit=True) + config.registry.registerAdapter( + view, (IViewClassifier, IRequest, ISuper), IView, name='') + config.add_view(view=view2, for_=ISub, renderer=null_renderer) + wrapper = self._getViewCallable(config, ctx_iface=ISuper, + request_iface=IRequest) + self.assertFalse(IMultiView.providedBy(wrapper)) + self.assertEqual(wrapper(None, None), 'OK') + wrapper = self._getViewCallable(config, ctx_iface=ISub, + request_iface=IRequest) + self.assertFalse(IMultiView.providedBy(wrapper)) + self.assertEqual(wrapper(None, None), 'OK2') + + def test_add_view_multiview_exception_superclass_then_subclass(self): + from pyramid.renderers import null_renderer + from zope.interface import implementedBy + from pyramid.interfaces import IRequest + from pyramid.interfaces import IView + from pyramid.interfaces import IMultiView + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IExceptionViewClassifier + class Super(Exception): + pass + class Sub(Super): + pass + view = lambda *arg: 'OK' + view2 = lambda *arg: 'OK2' + config = self._makeOne(autocommit=True) + config.registry.registerAdapter( + view, (IViewClassifier, IRequest, Super), IView, name='') + config.registry.registerAdapter( + view, (IExceptionViewClassifier, IRequest, Super), IView, name='') + config.add_view(view=view2, for_=Sub, renderer=null_renderer) + wrapper = self._getViewCallable( + config, ctx_iface=implementedBy(Super), request_iface=IRequest) + wrapper_exc_view = self._getViewCallable( + config, exc_iface=implementedBy(Super), request_iface=IRequest) + self.assertEqual(wrapper_exc_view, wrapper) + self.assertFalse(IMultiView.providedBy(wrapper_exc_view)) + self.assertEqual(wrapper_exc_view(None, None), 'OK') + wrapper = self._getViewCallable( + config, ctx_iface=implementedBy(Sub), request_iface=IRequest) + wrapper_exc_view = self._getViewCallable( + config, exc_iface=implementedBy(Sub), request_iface=IRequest) + self.assertEqual(wrapper_exc_view, wrapper) + self.assertFalse(IMultiView.providedBy(wrapper_exc_view)) + self.assertEqual(wrapper_exc_view(None, None), 'OK2') + + def test_add_view_multiview_call_ordering(self): + from pyramid.renderers import null_renderer as nr + from zope.interface import directlyProvides + def view1(context, request): return 'view1' + def view2(context, request): return 'view2' + def view3(context, request): return 'view3' + def view4(context, request): return 'view4' + def view5(context, request): return 'view5' + def view6(context, request): return 'view6' + def view7(context, request): return 'view7' + def view8(context, request): return 'view8' + config = self._makeOne(autocommit=True) + config.add_view(view=view1, renderer=nr) + config.add_view(view=view2, request_method='POST', renderer=nr) + config.add_view(view=view3,request_param='param', renderer=nr) + config.add_view(view=view4, containment=IDummy, renderer=nr) + config.add_view(view=view5, request_method='POST', + request_param='param', renderer=nr) + config.add_view(view=view6, request_method='POST', containment=IDummy, + renderer=nr) + config.add_view(view=view7, request_param='param', containment=IDummy, + renderer=nr) + config.add_view(view=view8, request_method='POST',request_param='param', + containment=IDummy, renderer=nr) + + + wrapper = self._getViewCallable(config) + + ctx = DummyContext() + request = self._makeRequest(config) + request.method = 'GET' + request.params = {} + self.assertEqual(wrapper(ctx, request), 'view1') + + ctx = DummyContext() + request = self._makeRequest(config) + request.params = {} + request.method = 'POST' + self.assertEqual(wrapper(ctx, request), 'view2') + + ctx = DummyContext() + request = self._makeRequest(config) + request.params = {'param':'1'} + request.method = 'GET' + self.assertEqual(wrapper(ctx, request), 'view3') + + ctx = DummyContext() + directlyProvides(ctx, IDummy) + request = self._makeRequest(config) + request.method = 'GET' + request.params = {} + self.assertEqual(wrapper(ctx, request), 'view4') + + ctx = DummyContext() + request = self._makeRequest(config) + request.method = 'POST' + request.params = {'param':'1'} + self.assertEqual(wrapper(ctx, request), 'view5') + + ctx = DummyContext() + directlyProvides(ctx, IDummy) + request = self._makeRequest(config) + request.params = {} + request.method = 'POST' + self.assertEqual(wrapper(ctx, request), 'view6') + + ctx = DummyContext() + directlyProvides(ctx, IDummy) + request = self._makeRequest(config) + request.method = 'GET' + request.params = {'param':'1'} + self.assertEqual(wrapper(ctx, request), 'view7') + + ctx = DummyContext() + directlyProvides(ctx, IDummy) + request = self._makeRequest(config) + request.method = 'POST' + request.params = {'param':'1'} + self.assertEqual(wrapper(ctx, request), 'view8') + + def test_view_with_most_specific_predicate(self): + from pyramid.renderers import null_renderer as nr + from pyramid.router import Router + + class OtherBase(object): pass + class Int1(object): pass + class Int2(object): pass + + class Resource(OtherBase, Int1, Int2): + def __init__(self, request): pass + + def unknown(context, request): return 'unknown' + def view(context, request): return 'hello' + + config = self._makeOne(autocommit=True) + config.add_route('root', '/', factory=Resource) + config.add_view(unknown, route_name='root', renderer=nr) + config.add_view( + view, renderer=nr, route_name='root', + context=Int1, request_method='GET' + ) + config.add_view( + view=view, renderer=nr, route_name='root', + context=Int2, request_method='POST' + ) + request = self._makeRequest(config) + request.method = 'POST' + request.params = {} + router = Router(config.registry) + response = router.handle_request(request) + self.assertEqual(response, 'hello') + + def test_view_with_most_specific_predicate_with_mismatch(self): + from pyramid.renderers import null_renderer as nr + from pyramid.router import Router + + class OtherBase(object): pass + class Int1(object): pass + class Int2(object): pass + + class Resource(OtherBase, Int1, Int2): + def __init__(self, request): pass + + def unknown(context, request): return 'unknown' + def view(context, request): return 'hello' + + config = self._makeOne(autocommit=True) + config.add_route('root', '/', factory=Resource) + + config.add_view( + unknown, + route_name='root', + renderer=nr, + request_method=('POST',), + xhr=True, + ) + + config.add_view( + view, renderer=nr, route_name='root', + context=Int1, request_method='GET' + ) + config.add_view( + view=view, renderer=nr, route_name='root', + context=Int2, request_method='POST' + ) + request = self._makeRequest(config) + request.method = 'POST' + request.params = {} + router = Router(config.registry) + response = router.handle_request(request) + self.assertEqual(response, 'hello') + + def test_add_view_multiview___discriminator__(self): + from pyramid.renderers import null_renderer + from zope.interface import Interface + class IFoo(Interface): + pass + class IBar(Interface): + pass + @implementer(IFoo) + class Foo(object): + pass + @implementer(IBar) + class Bar(object): + pass + foo = Foo() + bar = Bar() + + from pyramid.interfaces import IRequest + from pyramid.interfaces import IView + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IMultiView + view = lambda *arg: 'OK' + view.__phash__ = 'abc' + config = self._makeOne(autocommit=True) + config.registry.registerAdapter( + view, (IViewClassifier, IRequest, Interface), IView, name='') + config.add_view(view=view, renderer=null_renderer, + containment=IFoo) + config.add_view(view=view, renderer=null_renderer, + containment=IBar) + wrapper = self._getViewCallable(config) + self.assertTrue(IMultiView.providedBy(wrapper)) + request = self._makeRequest(config) + self.assertNotEqual( + wrapper.__discriminator__(foo, request), + wrapper.__discriminator__(bar, request), + ) + + def test_add_view_with_template_renderer(self): + from pyramid.tests import test_config + from pyramid.interfaces import ISettings + class view(object): + def __init__(self, context, request): + self.request = request + self.context = context + + def __call__(self): + return {'a':'1'} + config = self._makeOne(autocommit=True) + renderer = self._registerRenderer(config) + fixture = 'pyramid.tests.test_config:files/minimal.txt' + config.introspection = False + config.add_view(view=view, renderer=fixture) + wrapper = self._getViewCallable(config) + request = self._makeRequest(config) + result = wrapper(None, request) + self.assertEqual(result.body, b'Hello!') + settings = config.registry.queryUtility(ISettings) + result = renderer.info + self.assertEqual(result.registry, config.registry) + self.assertEqual(result.type, '.txt') + self.assertEqual(result.package, test_config) + self.assertEqual(result.name, fixture) + self.assertEqual(result.settings, settings) + + def test_add_view_with_default_renderer(self): + class view(object): + def __init__(self, context, request): + self.request = request + self.context = context + + def __call__(self): + return {'a':'1'} + config = self._makeOne(autocommit=True) + class moo(object): + def __init__(self, *arg, **kw): + pass + def __call__(self, *arg, **kw): + return b'moo' + config.add_renderer(None, moo) + config.add_view(view=view) + wrapper = self._getViewCallable(config) + request = self._makeRequest(config) + result = wrapper(None, request) + self.assertEqual(result.body, b'moo') + + def test_add_view_with_template_renderer_no_callable(self): + from pyramid.tests import test_config + from pyramid.interfaces import ISettings + config = self._makeOne(autocommit=True) + renderer = self._registerRenderer(config) + fixture = 'pyramid.tests.test_config:files/minimal.txt' + config.introspection = False + config.add_view(view=None, renderer=fixture) + wrapper = self._getViewCallable(config) + request = self._makeRequest(config) + result = wrapper(None, request) + self.assertEqual(result.body, b'Hello!') + settings = config.registry.queryUtility(ISettings) + result = renderer.info + self.assertEqual(result.registry, config.registry) + self.assertEqual(result.type, '.txt') + self.assertEqual(result.package, test_config) + self.assertEqual(result.name, fixture) + self.assertEqual(result.settings, settings) + + def test_add_view_with_request_type_as_iface(self): + from pyramid.renderers import null_renderer + from zope.interface import directlyProvides + def view(context, request): + return 'OK' + config = self._makeOne(autocommit=True) + config.add_view(request_type=IDummy, view=view, renderer=null_renderer) + wrapper = self._getViewCallable(config, None) + request = self._makeRequest(config) + directlyProvides(request, IDummy) + result = wrapper(None, request) + self.assertEqual(result, 'OK') + + def test_add_view_with_request_type_as_noniface(self): + view = lambda *arg: 'OK' + config = self._makeOne() + self.assertRaises(ConfigurationError, + config.add_view, view, '', None, None, object) + + def test_add_view_with_route_name(self): + from pyramid.renderers import null_renderer + view = lambda *arg: 'OK' + config = self._makeOne(autocommit=True) + config.add_route('foo', '/a/b') + config.add_view(view=view, route_name='foo', renderer=null_renderer) + request_iface = self._getRouteRequestIface(config, 'foo') + self.assertNotEqual(request_iface, None) + wrapper = self._getViewCallable(config, request_iface=request_iface) + self.assertNotEqual(wrapper, None) + self.assertEqual(wrapper(None, None), 'OK') + + def test_add_view_with_nonexistant_route_name(self): + from pyramid.renderers import null_renderer + view = lambda *arg: 'OK' + config = self._makeOne() + config.add_view(view=view, route_name='foo', renderer=null_renderer) + self.assertRaises(ConfigurationExecutionError, config.commit) + + def test_add_view_with_route_name_exception(self): + from pyramid.renderers import null_renderer + from zope.interface import implementedBy + view = lambda *arg: 'OK' + config = self._makeOne(autocommit=True) + config.add_route('foo', '/a/b') + config.add_view(view=view, route_name='foo', context=RuntimeError, + renderer=null_renderer) + request_iface = self._getRouteRequestIface(config, 'foo') + wrapper_exc_view = self._getViewCallable( + config, exc_iface=implementedBy(RuntimeError), + request_iface=request_iface) + self.assertNotEqual(wrapper_exc_view, None) + wrapper = self._getViewCallable( + config, ctx_iface=implementedBy(RuntimeError), + request_iface=request_iface) + self.assertEqual(wrapper_exc_view, wrapper) + self.assertEqual(wrapper_exc_view(None, None), 'OK') + + def test_add_view_with_request_method_true(self): + from pyramid.renderers import null_renderer + view = lambda *arg: 'OK' + config = self._makeOne(autocommit=True) + config.add_view(view=view, request_method='POST', + renderer=null_renderer) + wrapper = self._getViewCallable(config) + request = self._makeRequest(config) + request.method = 'POST' + self.assertEqual(wrapper(None, request), 'OK') + + def test_add_view_with_request_method_false(self): + view = lambda *arg: 'OK' + config = self._makeOne(autocommit=True) + config.add_view(view=view, request_method='POST') + wrapper = self._getViewCallable(config) + request = self._makeRequest(config) + request.method = 'GET' + self._assertNotFound(wrapper, None, request) + + def test_add_view_with_request_method_sequence_true(self): + from pyramid.renderers import null_renderer + view = lambda *arg: 'OK' + config = self._makeOne(autocommit=True) + config.add_view(view=view, request_method=('POST', 'GET'), + renderer=null_renderer) + wrapper = self._getViewCallable(config) + request = self._makeRequest(config) + request.method = 'POST' + self.assertEqual(wrapper(None, request), 'OK') + + def test_add_view_with_request_method_sequence_conflict(self): + from pyramid.renderers import null_renderer + view = lambda *arg: 'OK' + config = self._makeOne() + config.add_view(view=view, request_method=('POST', 'GET'), + renderer=null_renderer) + config.add_view(view=view, request_method=('GET', 'POST'), + renderer=null_renderer) + self.assertRaises(ConfigurationConflictError, config.commit) + + def test_add_view_with_request_method_sequence_false(self): + view = lambda *arg: 'OK' + config = self._makeOne(autocommit=True) + config.add_view(view=view, request_method=('POST', 'HEAD')) + wrapper = self._getViewCallable(config) + request = self._makeRequest(config) + request.method = 'GET' + self._assertNotFound(wrapper, None, request) + + def test_add_view_with_request_method_get_implies_head(self): + from pyramid.renderers import null_renderer + view = lambda *arg: 'OK' + config = self._makeOne(autocommit=True) + config.add_view(view=view, request_method='GET', renderer=null_renderer) + wrapper = self._getViewCallable(config) + request = self._makeRequest(config) + request.method = 'HEAD' + self.assertEqual(wrapper(None, request), 'OK') + + def test_add_view_with_request_param_noval_true(self): + from pyramid.renderers import null_renderer + view = lambda *arg: 'OK' + config = self._makeOne(autocommit=True) + config.add_view(view=view, request_param='abc', renderer=null_renderer) + wrapper = self._getViewCallable(config) + request = self._makeRequest(config) + request.params = {'abc':''} + self.assertEqual(wrapper(None, request), 'OK') + + def test_add_view_with_request_param_noval_false(self): + view = lambda *arg: 'OK' + config = self._makeOne(autocommit=True) + config.add_view(view=view, request_param='abc') + wrapper = self._getViewCallable(config) + request = self._makeRequest(config) + request.params = {} + self._assertNotFound(wrapper, None, request) + + def test_add_view_with_request_param_val_true(self): + from pyramid.renderers import null_renderer + view = lambda *arg: 'OK' + config = self._makeOne(autocommit=True) + config.add_view(view=view, request_param='abc=123', + renderer=null_renderer) + wrapper = self._getViewCallable(config) + request = self._makeRequest(config) + request.params = {'abc':'123'} + self.assertEqual(wrapper(None, request), 'OK') + + def test_add_view_with_request_param_val_false(self): + view = lambda *arg: 'OK' + config = self._makeOne(autocommit=True) + config.add_view(view=view, request_param='abc=123') + wrapper = self._getViewCallable(config) + request = self._makeRequest(config) + request.params = {'abc':''} + self._assertNotFound(wrapper, None, request) + + def test_add_view_with_xhr_true(self): + from pyramid.renderers import null_renderer + view = lambda *arg: 'OK' + config = self._makeOne(autocommit=True) + config.add_view(view=view, xhr=True, renderer=null_renderer) + wrapper = self._getViewCallable(config) + request = self._makeRequest(config) + request.is_xhr = True + self.assertEqual(wrapper(None, request), 'OK') + + def test_add_view_with_xhr_false(self): + view = lambda *arg: 'OK' + config = self._makeOne(autocommit=True) + config.add_view(view=view, xhr=True) + wrapper = self._getViewCallable(config) + request = self._makeRequest(config) + request.is_xhr = False + self._assertNotFound(wrapper, None, request) + + def test_add_view_with_header_badregex(self): + view = lambda *arg: 'OK' + config = self._makeOne() + config.add_view(view, header='Host:a\\') + self.assertRaises(ConfigurationError, config.commit) + + def test_add_view_with_header_noval_match(self): + from pyramid.renderers import null_renderer + view = lambda *arg: 'OK' + config = self._makeOne(autocommit=True) + config.add_view(view=view, header='Host', renderer=null_renderer) + wrapper = self._getViewCallable(config) + request = self._makeRequest(config) + request.headers = {'Host':'whatever'} + self.assertEqual(wrapper(None, request), 'OK') + + def test_add_view_with_header_noval_nomatch(self): + view = lambda *arg: 'OK' + config = self._makeOne(autocommit=True) + config.add_view(view=view, header='Host') + wrapper = self._getViewCallable(config) + request = self._makeRequest(config) + request.headers = {'NotHost':'whatever'} + self._assertNotFound(wrapper, None, request) + + def test_add_view_with_header_val_match(self): + from pyramid.renderers import null_renderer + view = lambda *arg: 'OK' + config = self._makeOne(autocommit=True) + config.add_view(view=view, header=r'Host:\d', renderer=null_renderer) + wrapper = self._getViewCallable(config) + request = self._makeRequest(config) + request.headers = {'Host':'1'} + self.assertEqual(wrapper(None, request), 'OK') + + def test_add_view_with_header_val_nomatch(self): + view = lambda *arg: 'OK' + config = self._makeOne(autocommit=True) + config.add_view(view=view, header=r'Host:\d') + wrapper = self._getViewCallable(config) + request = self._makeRequest(config) + request.headers = {'Host':'abc'} + self._assertNotFound(wrapper, None, request) + + def test_add_view_with_header_val_missing(self): + from pyramid.httpexceptions import HTTPNotFound + view = lambda *arg: 'OK' + config = self._makeOne(autocommit=True) + config.add_view(view=view, header=r'Host:\d') + wrapper = self._getViewCallable(config) + request = self._makeRequest(config) + request.headers = {'NoHost':'1'} + self.assertRaises(HTTPNotFound, wrapper, None, request) + + def test_add_view_with_accept_match(self): + from pyramid.renderers import null_renderer + view = lambda *arg: 'OK' + config = self._makeOne(autocommit=True) + config.add_view(view=view, accept='text/xml', renderer=null_renderer) + wrapper = self._getViewCallable(config) + request = self._makeRequest(config) + request.accept = DummyAccept('text/xml') + self.assertEqual(wrapper(None, request), 'OK') + + def test_add_view_with_accept_nomatch(self): + view = lambda *arg: 'OK' + config = self._makeOne(autocommit=True) + config.add_view(view=view, accept='text/xml') + wrapper = self._getViewCallable(config) + request = self._makeRequest(config) + request.accept = DummyAccept('text/html') + self._assertNotFound(wrapper, None, request) + + def test_add_view_with_range_accept_match(self): + from pyramid.renderers import null_renderer + view = lambda *arg: 'OK' + config = self._makeOne(autocommit=True) + config.add_view(view=view, accept='text/*', renderer=null_renderer) + wrapper = self._getViewCallable(config) + request = self._makeRequest(config) + request.accept = DummyAccept('text/html', contains=True) + self.assertEqual(wrapper(None, request), 'OK') + + def test_add_view_with_range_accept_nomatch(self): + view = lambda *arg: 'OK' + config = self._makeOne(autocommit=True) + config.add_view(view=view, accept='text/*') + wrapper = self._getViewCallable(config) + request = self._makeRequest(config) + request.accept = DummyAccept('application/json', contains=False) + self._assertNotFound(wrapper, None, request) + + def test_add_view_with_containment_true(self): + from pyramid.renderers import null_renderer + from zope.interface import directlyProvides + view = lambda *arg: 'OK' + config = self._makeOne(autocommit=True) + config.add_view(view=view, containment=IDummy, renderer=null_renderer) + wrapper = self._getViewCallable(config) + context = DummyContext() + directlyProvides(context, IDummy) + self.assertEqual(wrapper(context, None), 'OK') + + def test_add_view_with_containment_false(self): + view = lambda *arg: 'OK' + config = self._makeOne(autocommit=True) + config.add_view(view=view, containment=IDummy) + wrapper = self._getViewCallable(config) + context = DummyContext() + self._assertNotFound(wrapper, context, None) + + def test_add_view_with_containment_dottedname(self): + from pyramid.renderers import null_renderer + from zope.interface import directlyProvides + view = lambda *arg: 'OK' + config = self._makeOne(autocommit=True) + config.add_view( + view=view, + containment='pyramid.tests.test_config.IDummy', + renderer=null_renderer) + wrapper = self._getViewCallable(config) + context = DummyContext() + directlyProvides(context, IDummy) + self.assertEqual(wrapper(context, None), 'OK') + + def test_add_view_with_path_info_badregex(self): + view = lambda *arg: 'OK' + config = self._makeOne() + config.add_view(view, path_info='\\') + self.assertRaises(ConfigurationError, config.commit) + + def test_add_view_with_path_info_match(self): + from pyramid.renderers import null_renderer + view = lambda *arg: 'OK' + config = self._makeOne(autocommit=True) + config.add_view(view=view, path_info='/foo', renderer=null_renderer) + wrapper = self._getViewCallable(config) + request = self._makeRequest(config) + request.upath_info = text_(b'/foo') + self.assertEqual(wrapper(None, request), 'OK') + + def test_add_view_with_path_info_nomatch(self): + view = lambda *arg: 'OK' + config = self._makeOne(autocommit=True) + config.add_view(view=view, path_info='/foo') + wrapper = self._getViewCallable(config) + request = self._makeRequest(config) + request.upath_info = text_('/') + self._assertNotFound(wrapper, None, request) + + def test_add_view_with_check_csrf_predicates_match(self): + import warnings + from pyramid.renderers import null_renderer + view = lambda *arg: 'OK' + config = self._makeOne(autocommit=True) + with warnings.catch_warnings(record=True) as w: + warnings.filterwarnings('always') + config.add_view(view=view, check_csrf=True, renderer=null_renderer) + self.assertEqual(len(w), 1) + wrapper = self._getViewCallable(config) + request = self._makeRequest(config) + request.method = "POST" + request.session = DummySession({'csrf_token': 'foo'}) + request.POST = {'csrf_token': 'foo'} + request.headers = {} + self.assertEqual(wrapper(None, request), 'OK') + + def test_add_view_with_custom_predicates_match(self): + import warnings + from pyramid.renderers import null_renderer + view = lambda *arg: 'OK' + config = self._makeOne(autocommit=True) + def pred1(context, request): + return True + def pred2(context, request): + return True + predicates = (pred1, pred2) + with warnings.catch_warnings(record=True) as w: + warnings.filterwarnings('always') + config.add_view(view=view, custom_predicates=predicates, + renderer=null_renderer) + self.assertEqual(len(w), 1) + wrapper = self._getViewCallable(config) + request = self._makeRequest(config) + self.assertEqual(wrapper(None, request), 'OK') + + def test_add_view_with_custom_predicates_nomatch(self): + import warnings + view = lambda *arg: 'OK' + config = self._makeOne(autocommit=True) + def pred1(context, request): + return True + def pred2(context, request): + return False + predicates = (pred1, pred2) + with warnings.catch_warnings(record=True) as w: + warnings.filterwarnings('always') + config.add_view(view=view, custom_predicates=predicates) + self.assertEqual(len(w), 1) + wrapper = self._getViewCallable(config) + request = self._makeRequest(config) + self._assertNotFound(wrapper, None, request) + + def test_add_view_custom_predicate_bests_standard_predicate(self): + import warnings + from pyramid.renderers import null_renderer + view = lambda *arg: 'OK' + view2 = lambda *arg: 'NOT OK' + config = self._makeOne(autocommit=True) + def pred1(context, request): + return True + with warnings.catch_warnings(record=True) as w: + warnings.filterwarnings('always') + config.add_view(view=view, custom_predicates=(pred1,), + renderer=null_renderer) + config.add_view(view=view2, request_method='GET', + renderer=null_renderer) + self.assertEqual(len(w), 1) + wrapper = self._getViewCallable(config) + request = self._makeRequest(config) + request.method = 'GET' + self.assertEqual(wrapper(None, request), 'OK') + + def test_add_view_custom_more_preds_first_bests_fewer_preds_last(self): + from pyramid.renderers import null_renderer + view = lambda *arg: 'OK' + view2 = lambda *arg: 'NOT OK' + config = self._makeOne(autocommit=True) + config.add_view(view=view, request_method='GET', xhr=True, + renderer=null_renderer) + config.add_view(view=view2, request_method='GET', + renderer=null_renderer) + wrapper = self._getViewCallable(config) + request = self._makeRequest(config) + request.method = 'GET' + request.is_xhr = True + self.assertEqual(wrapper(None, request), 'OK') + + def test_add_view_same_predicates(self): + view2 = lambda *arg: 'second' + view1 = lambda *arg: 'first' + config = self._makeOne() + config.add_view(view=view1) + config.add_view(view=view2) + self.assertRaises(ConfigurationConflictError, config.commit) + + def test_add_view_with_csrf_param(self): + from pyramid.renderers import null_renderer + def view(request): + return 'OK' + config = self._makeOne(autocommit=True) + config.add_view(view, require_csrf='st', renderer=null_renderer) + view = self._getViewCallable(config) + request = self._makeRequest(config) + request.scheme = "http" + request.method = 'POST' + request.POST = {'st': 'foo'} + request.headers = {} + request.session = DummySession({'csrf_token': 'foo'}) + self.assertEqual(view(None, request), 'OK') + + def test_add_view_with_csrf_header(self): + from pyramid.renderers import null_renderer + def view(request): + return 'OK' + config = self._makeOne(autocommit=True) + config.add_view(view, require_csrf=True, renderer=null_renderer) + view = self._getViewCallable(config) + request = self._makeRequest(config) + request.scheme = "http" + request.method = 'POST' + request.POST = {} + request.headers = {'X-CSRF-Token': 'foo'} + request.session = DummySession({'csrf_token': 'foo'}) + self.assertEqual(view(None, request), 'OK') + + def test_add_view_with_missing_csrf_header(self): + from pyramid.exceptions import BadCSRFToken + from pyramid.renderers import null_renderer + def view(request): return 'OK' + config = self._makeOne(autocommit=True) + config.add_view(view, require_csrf=True, renderer=null_renderer) + view = self._getViewCallable(config) + request = self._makeRequest(config) + request.scheme = "http" + request.method = 'POST' + request.POST = {} + request.headers = {} + request.session = DummySession({'csrf_token': 'foo'}) + self.assertRaises(BadCSRFToken, lambda: view(None, request)) + + def test_add_view_with_permission(self): + from pyramid.renderers import null_renderer + view1 = lambda *arg: 'OK' + outerself = self + class DummyPolicy(object): + def effective_principals(self, r): + outerself.assertEqual(r, request) + return ['abc'] + def permits(self, context, principals, permission): + outerself.assertEqual(context, None) + outerself.assertEqual(principals, ['abc']) + outerself.assertEqual(permission, 'view') + return True + policy = DummyPolicy() + config = self._makeOne(authorization_policy=policy, + authentication_policy=policy, + autocommit=True) + config.add_view(view=view1, permission='view', renderer=null_renderer) + view = self._getViewCallable(config) + request = self._makeRequest(config) + self.assertEqual(view(None, request), 'OK') + + def test_add_view_with_default_permission_no_explicit_permission(self): + from pyramid.renderers import null_renderer + view1 = lambda *arg: 'OK' + outerself = self + class DummyPolicy(object): + def effective_principals(self, r): + outerself.assertEqual(r, request) + return ['abc'] + def permits(self, context, principals, permission): + outerself.assertEqual(context, None) + outerself.assertEqual(principals, ['abc']) + outerself.assertEqual(permission, 'view') + return True + policy = DummyPolicy() + config = self._makeOne(authorization_policy=policy, + authentication_policy=policy, + default_permission='view', + autocommit=True) + config.add_view(view=view1, renderer=null_renderer) + view = self._getViewCallable(config) + request = self._makeRequest(config) + self.assertEqual(view(None, request), 'OK') + + def test_add_view_with_no_default_permission_no_explicit_permission(self): + from pyramid.renderers import null_renderer + view1 = lambda *arg: 'OK' + class DummyPolicy(object): pass # wont be called + policy = DummyPolicy() + config = self._makeOne(authorization_policy=policy, + authentication_policy=policy, + autocommit=True) + config.add_view(view=view1, renderer=null_renderer) + view = self._getViewCallable(config) + request = self._makeRequest(config) + self.assertEqual(view(None, request), 'OK') + + def test_add_view_with_mapper(self): + from pyramid.renderers import null_renderer + class Mapper(object): + def __init__(self, **kw): + self.__class__.kw = kw + def __call__(self, view): + return view + config = self._makeOne(autocommit=True) + def view(context, request): return 'OK' + config.add_view(view=view, mapper=Mapper, renderer=null_renderer) + view = self._getViewCallable(config) + self.assertEqual(view(None, None), 'OK') + self.assertEqual(Mapper.kw['mapper'], Mapper) + + def test_add_view_with_view_defaults(self): + from pyramid.renderers import null_renderer + from pyramid.exceptions import PredicateMismatch + from zope.interface import directlyProvides + class view(object): + __view_defaults__ = { + 'containment':'pyramid.tests.test_config.IDummy' + } + def __init__(self, request): + pass + def __call__(self): + return 'OK' + config = self._makeOne(autocommit=True) + config.add_view( + view=view, + renderer=null_renderer) + wrapper = self._getViewCallable(config) + context = DummyContext() + directlyProvides(context, IDummy) + request = self._makeRequest(config) + self.assertEqual(wrapper(context, request), 'OK') + context = DummyContext() + request = self._makeRequest(config) + self.assertRaises(PredicateMismatch, wrapper, context, request) + + def test_add_view_with_view_defaults_viewname_is_dottedname_kwarg(self): + from pyramid.renderers import null_renderer + from pyramid.exceptions import PredicateMismatch + from zope.interface import directlyProvides + config = self._makeOne(autocommit=True) + config.add_view( + view='pyramid.tests.test_config.test_views.DummyViewDefaultsClass', + renderer=null_renderer) + wrapper = self._getViewCallable(config) + context = DummyContext() + directlyProvides(context, IDummy) + request = self._makeRequest(config) + self.assertEqual(wrapper(context, request), 'OK') + context = DummyContext() + request = self._makeRequest(config) + self.assertRaises(PredicateMismatch, wrapper, context, request) + + def test_add_view_with_view_defaults_viewname_is_dottedname_nonkwarg(self): + from pyramid.renderers import null_renderer + from pyramid.exceptions import PredicateMismatch + from zope.interface import directlyProvides + config = self._makeOne(autocommit=True) + config.add_view( + 'pyramid.tests.test_config.test_views.DummyViewDefaultsClass', + renderer=null_renderer) + wrapper = self._getViewCallable(config) + context = DummyContext() + directlyProvides(context, IDummy) + request = self._makeRequest(config) + self.assertEqual(wrapper(context, request), 'OK') + context = DummyContext() + request = self._makeRequest(config) + self.assertRaises(PredicateMismatch, wrapper, context, request) + + def test_add_view_with_view_config_and_view_defaults_doesnt_conflict(self): + from pyramid.renderers import null_renderer + class view(object): + __view_defaults__ = { + 'containment':'pyramid.tests.test_config.IDummy' + } + class view2(object): + __view_defaults__ = { + 'containment':'pyramid.tests.test_config.IFactory' + } + config = self._makeOne(autocommit=False) + config.add_view( + view=view, + renderer=null_renderer) + config.add_view( + view=view2, + renderer=null_renderer) + config.commit() # does not raise + + def test_add_view_with_view_config_and_view_defaults_conflicts(self): + from pyramid.renderers import null_renderer + class view(object): + __view_defaults__ = { + 'containment':'pyramid.tests.test_config.IDummy' + } + class view2(object): + __view_defaults__ = { + 'containment':'pyramid.tests.test_config.IDummy' + } + config = self._makeOne(autocommit=False) + config.add_view( + view=view, + renderer=null_renderer) + config.add_view( + view=view2, + renderer=null_renderer) + self.assertRaises(ConfigurationConflictError, config.commit) + + def test_add_view_class_method_no_attr(self): + from pyramid.renderers import null_renderer + from zope.interface import directlyProvides + from pyramid.exceptions import ConfigurationError + + config = self._makeOne(autocommit=True) + class DummyViewClass(object): + def run(self): pass + + def configure_view(): + config.add_view(view=DummyViewClass.run, renderer=null_renderer) + + self.assertRaises(ConfigurationError, configure_view) + + def test_add_view_exception_only_no_regular_view(self): + from zope.interface import implementedBy + from pyramid.renderers import null_renderer + view1 = lambda *arg: 'OK' + config = self._makeOne(autocommit=True) + config.add_view(view=view1, context=Exception, exception_only=True, + renderer=null_renderer) + view = self._getViewCallable(config, ctx_iface=implementedBy(Exception)) + self.assertTrue(view is None) + + def test_add_view_exception_only(self): + from zope.interface import implementedBy + from pyramid.renderers import null_renderer + view1 = lambda *arg: 'OK' + config = self._makeOne(autocommit=True) + config.add_view(view=view1, context=Exception, exception_only=True, + renderer=null_renderer) + view = self._getViewCallable( + config, exc_iface=implementedBy(Exception)) + self.assertEqual(view1, view) + + def test_add_view_exception_only_misconfiguration(self): + view = lambda *arg: 'OK' + config = self._makeOne(autocommit=True) + class NotAnException(object): + pass + self.assertRaises( + ConfigurationError, + config.add_view, view, context=NotAnException, exception_only=True) + + def test_add_exception_view(self): + from zope.interface import implementedBy + from pyramid.renderers import null_renderer + view1 = lambda *arg: 'OK' + config = self._makeOne(autocommit=True) + config.add_exception_view(view=view1, renderer=null_renderer) + wrapper = self._getViewCallable( + config, exc_iface=implementedBy(Exception)) + context = Exception() + request = self._makeRequest(config) + self.assertEqual(wrapper(context, request), 'OK') + + def test_add_exception_view_with_subclass(self): + from zope.interface import implementedBy + from pyramid.renderers import null_renderer + view1 = lambda *arg: 'OK' + config = self._makeOne(autocommit=True) + config.add_exception_view(view=view1, context=ValueError, + renderer=null_renderer) + wrapper = self._getViewCallable( + config, exc_iface=implementedBy(ValueError)) + context = ValueError() + request = self._makeRequest(config) + self.assertEqual(wrapper(context, request), 'OK') + + def test_add_exception_view_disallows_name(self): + config = self._makeOne(autocommit=True) + self.assertRaises(ConfigurationError, + config.add_exception_view, + context=Exception(), + name='foo') + + def test_add_exception_view_disallows_permission(self): + config = self._makeOne(autocommit=True) + self.assertRaises(ConfigurationError, + config.add_exception_view, + context=Exception(), + permission='foo') + + def test_add_exception_view_disallows_require_csrf(self): + config = self._makeOne(autocommit=True) + self.assertRaises(ConfigurationError, + config.add_exception_view, + context=Exception(), + require_csrf=True) + + def test_add_exception_view_disallows_for_(self): + config = self._makeOne(autocommit=True) + self.assertRaises(ConfigurationError, + config.add_exception_view, + context=Exception(), + for_='foo') + + def test_add_exception_view_disallows_exception_only(self): + config = self._makeOne(autocommit=True) + self.assertRaises(ConfigurationError, + config.add_exception_view, + context=Exception(), + exception_only=True) + + def test_add_exception_view_with_view_defaults(self): + from pyramid.renderers import null_renderer + from pyramid.exceptions import PredicateMismatch + from zope.interface import directlyProvides + from zope.interface import implementedBy + class view(object): + __view_defaults__ = { + 'containment': 'pyramid.tests.test_config.IDummy' + } + def __init__(self, request): + pass + def __call__(self): + return 'OK' + config = self._makeOne(autocommit=True) + config.add_exception_view( + view=view, + context=Exception, + renderer=null_renderer) + wrapper = self._getViewCallable( + config, exc_iface=implementedBy(Exception)) + context = DummyContext() + directlyProvides(context, IDummy) + request = self._makeRequest(config) + self.assertEqual(wrapper(context, request), 'OK') + context = DummyContext() + request = self._makeRequest(config) + self.assertRaises(PredicateMismatch, wrapper, context, request) + + def test_derive_view_function(self): + from pyramid.renderers import null_renderer + def view(request): + return 'OK' + config = self._makeOne() + result = config.derive_view(view, renderer=null_renderer) + self.assertFalse(result is view) + self.assertEqual(result(None, None), 'OK') + + def test_derive_view_dottedname(self): + from pyramid.renderers import null_renderer + config = self._makeOne() + result = config.derive_view( + 'pyramid.tests.test_config.dummy_view', + renderer=null_renderer) + self.assertFalse(result is dummy_view) + self.assertEqual(result(None, None), 'OK') + + def test_derive_view_with_default_renderer_no_explicit_renderer(self): + config = self._makeOne() + class moo(object): + def __init__(self, view): + pass + def __call__(self, *arg, **kw): + return 'moo' + config.add_renderer(None, moo) + config.commit() + def view(request): + return 'OK' + result = config.derive_view(view) + self.assertFalse(result is view) + self.assertEqual(result(None, None).body, b'moo') + + def test_derive_view_with_default_renderer_with_explicit_renderer(self): + class moo(object): pass + class foo(object): + def __init__(self, view): + pass + def __call__(self, *arg, **kw): + return b'foo' + def view(request): + return 'OK' + config = self._makeOne() + config.add_renderer(None, moo) + config.add_renderer('foo', foo) + config.commit() + result = config.derive_view(view, renderer='foo') + self.assertFalse(result is view) + request = self._makeRequest(config) + self.assertEqual(result(None, request).body, b'foo') + + def test_add_static_view_here_no_utility_registered(self): + from pyramid.renderers import null_renderer + from zope.interface import Interface + from pyramid.interfaces import IView + from pyramid.interfaces import IViewClassifier + config = self._makeOne(autocommit=True) + config.add_static_view('static', 'files', renderer=null_renderer) + request_type = self._getRouteRequestIface(config, '__static/') + self._assertRoute(config, '__static/', 'static/*subpath') + wrapped = config.registry.adapters.lookup( + (IViewClassifier, request_type, Interface), IView, name='') + from pyramid.request import Request + request = Request.blank('/static/minimal.txt') + request.subpath = ('minimal.txt', ) + result = wrapped(None, request) + self.assertEqual(result.status, '200 OK') + self.assertTrue(result.body.startswith(b'' in body) + + def test__default_app_iter_with_comment_html(self): + cls = self._getTargetSubclass() + exc = cls(comment='comment & comment') + environ = _makeEnviron() + environ['HTTP_ACCEPT'] = 'text/html' + start_response = DummyStartResponse() + body = list(exc(environ, start_response))[0] + self.assertTrue(b'' in body) + + def test__default_app_iter_with_comment_json(self): + cls = self._getTargetSubclass() + exc = cls(comment='comment & comment') + environ = _makeEnviron() + environ['HTTP_ACCEPT'] = 'application/json' + start_response = DummyStartResponse() + body = list(exc(environ, start_response))[0] + import json + retval = json.loads(body.decode('UTF-8')) + self.assertEqual(retval['code'], '200 OK') + self.assertEqual(retval['title'], 'OK') + + def test__default_app_iter_with_custom_json(self): + def json_formatter(status, body, title, environ): + return {'message': body, + 'code': status, + 'title': title, + 'custom': environ['CUSTOM_VARIABLE'] + } + cls = self._getTargetSubclass() + exc = cls(comment='comment', json_formatter=json_formatter) + environ = _makeEnviron() + environ['HTTP_ACCEPT'] = 'application/json' + environ['CUSTOM_VARIABLE'] = 'custom!' + start_response = DummyStartResponse() + body = list(exc(environ, start_response))[0] + import json + retval = json.loads(body.decode('UTF-8')) + self.assertEqual(retval['code'], '200 OK') + self.assertEqual(retval['title'], 'OK') + self.assertEqual(retval['custom'], 'custom!') + + def test_custom_body_template(self): + cls = self._getTargetSubclass() + exc = cls(body_template='${REQUEST_METHOD}') + environ = _makeEnviron() + start_response = DummyStartResponse() + body = list(exc(environ, start_response))[0] + self.assertEqual(body, b'200 OK\n\nGET') + + def test_custom_body_template_with_custom_variable_doesnt_choke(self): + cls = self._getTargetSubclass() + exc = cls(body_template='${REQUEST_METHOD}') + environ = _makeEnviron() + class Choke(object): + def __str__(self): # pragma no cover + raise ValueError + environ['gardentheory.user'] = Choke() + start_response = DummyStartResponse() + body = list(exc(environ, start_response))[0] + self.assertEqual(body, b'200 OK\n\nGET') + + def test_body_template_unicode(self): + cls = self._getTargetSubclass() + la = text_(b'/La Pe\xc3\xb1a', 'utf-8') + environ = _makeEnviron(unicodeval=la) + exc = cls(body_template='${unicodeval}') + start_response = DummyStartResponse() + body = list(exc(environ, start_response))[0] + self.assertEqual(body, b'200 OK\n\n/La Pe\xc3\xb1a') + + def test_allow_detail_non_str(self): + exc = self._makeOne(detail={'error': 'This is a test'}) + self.assertIsInstance(exc.__str__(), string_types) + + +class TestRenderAllExceptionsWithoutArguments(unittest.TestCase): + def _doit(self, content_type): + from pyramid.httpexceptions import status_map + L = [] + self.assertTrue(status_map) + for v in status_map.values(): + environ = _makeEnviron() + start_response = DummyStartResponse() + exc = v() + exc.content_type = content_type + result = list(exc(environ, start_response))[0] + if exc.empty_body: + self.assertEqual(result, b'') + else: + self.assertTrue(bytes_(exc.status) in result) + L.append(result) + self.assertEqual(len(L), len(status_map)) + + def test_it_plain(self): + self._doit('text/plain') + + def test_it_html(self): + self._doit('text/html') + +class Test_HTTPMove(unittest.TestCase): + def _makeOne(self, *arg, **kw): + from pyramid.httpexceptions import _HTTPMove + return _HTTPMove(*arg, **kw) + + def test_it_location_none_valueerrors(self): + # Constructing a HTTPMove instance with location=None should + # throw a ValueError from __init__ so that a more-confusing + # exception won't be thrown later from .prepare(environ) + self.assertRaises(ValueError, self._makeOne, location=None) + + def test_it_location_not_passed(self): + exc = self._makeOne() + self.assertEqual(exc.location, '') + + def test_it_location_passed(self): + exc = self._makeOne(location='foo') + self.assertEqual(exc.location, 'foo') + + def test_it_location_firstarg(self): + exc = self._makeOne('foo') + self.assertEqual(exc.location, 'foo') + + def test_it_call_with_default_body_tmpl(self): + exc = self._makeOne(location='foo') + environ = _makeEnviron() + start_response = DummyStartResponse() + app_iter = exc(environ, start_response) + self.assertEqual(app_iter[0], + (b'520 Unknown Error\n\nThe resource has been moved to foo; ' + b'you should be redirected automatically.\n\n')) + +class TestHTTPForbidden(unittest.TestCase): + def _makeOne(self, *arg, **kw): + from pyramid.httpexceptions import HTTPForbidden + return HTTPForbidden(*arg, **kw) + + def test_it_result_not_passed(self): + exc = self._makeOne() + self.assertEqual(exc.result, None) + + def test_it_result_passed(self): + exc = self._makeOne(result='foo') + self.assertEqual(exc.result, 'foo') + +class TestHTTPMethodNotAllowed(unittest.TestCase): + def _makeOne(self, *arg, **kw): + from pyramid.httpexceptions import HTTPMethodNotAllowed + return HTTPMethodNotAllowed(*arg, **kw) + + def test_it_with_default_body_tmpl(self): + exc = self._makeOne() + environ = _makeEnviron() + start_response = DummyStartResponse() + app_iter = exc(environ, start_response) + self.assertEqual(app_iter[0], + (b'405 Method Not Allowed\n\nThe method GET is not ' + b'allowed for this resource. \n\n\n')) + + +class DummyRequest(object): + exception = None + +class DummyStartResponse(object): + def __call__(self, status, headerlist): + self.status = status + self.headerlist = headerlist + +def _makeEnviron(**kw): + environ = {'REQUEST_METHOD': 'GET', + 'wsgi.url_scheme': 'http', + 'SERVER_NAME': 'localhost', + 'SERVER_PORT': '80'} + environ.update(kw) + return environ diff --git a/src/pyramid/tests/test_i18n.py b/src/pyramid/tests/test_i18n.py new file mode 100644 index 000000000..d72d0d480 --- /dev/null +++ b/src/pyramid/tests/test_i18n.py @@ -0,0 +1,508 @@ +# -*- coding: utf-8 -*- +# +import os + +here = os.path.dirname(__file__) +localedir = os.path.join(here, 'pkgs', 'localeapp', 'locale') + +import unittest +from pyramid import testing + +class TestTranslationString(unittest.TestCase): + def _makeOne(self, *arg, **kw): + from pyramid.i18n import TranslationString + return TranslationString(*arg, **kw) + + def test_it(self): + # this is part of the API, we don't actually need to test much more + # than that it's importable + ts = self._makeOne('a') + self.assertEqual(ts, 'a') + +class TestTranslationStringFactory(unittest.TestCase): + def _makeOne(self, *arg, **kw): + from pyramid.i18n import TranslationStringFactory + return TranslationStringFactory(*arg, **kw) + + def test_it(self): + # this is part of the API, we don't actually need to test much more + # than that it's importable + factory = self._makeOne('a') + self.assertEqual(factory('').domain, 'a') + +class TestLocalizer(unittest.TestCase): + def _makeOne(self, *arg, **kw): + from pyramid.i18n import Localizer + return Localizer(*arg, **kw) + + def test_ctor(self): + localizer = self._makeOne('en_US', None) + self.assertEqual(localizer.locale_name, 'en_US') + self.assertEqual(localizer.translations, None) + + def test_translate(self): + translations = DummyTranslations() + localizer = self._makeOne(None, translations) + self.assertEqual(localizer.translate('123', domain='1', + mapping={}), '123') + self.assertTrue(localizer.translator) + + def test_pluralize(self): + translations = DummyTranslations() + localizer = self._makeOne(None, translations) + result = localizer.pluralize('singular', 'plural', 1, + domain='1', mapping={}) + self.assertEqual(result, 'singular') + self.assertTrue(localizer.pluralizer) + + def test_pluralize_pluralizer_already_added(self): + translations = DummyTranslations() + localizer = self._makeOne(None, translations) + def pluralizer(*arg, **kw): + return arg, kw + localizer.pluralizer = pluralizer + result = localizer.pluralize('singular', 'plural', 1, domain='1', + mapping={}) + self.assertEqual( + result, + (('singular', 'plural', 1), {'domain': '1', 'mapping': {}}) + ) + self.assertTrue(localizer.pluralizer is pluralizer) + + def test_pluralize_default_translations(self): + # test that even without message ids loaded that + # "localizer.pluralize" "works" instead of raising an inscrutable + # "translations object has no attr 'plural' error; see + # see https://github.com/Pylons/pyramid/issues/235 + from pyramid.i18n import Translations + translations = Translations() + translations._catalog = {} + localizer = self._makeOne(None, translations) + result = localizer.pluralize('singular', 'plural', 2, domain='1', + mapping={}) + self.assertEqual(result, 'plural') + +class Test_negotiate_locale_name(unittest.TestCase): + def setUp(self): + testing.setUp() + + def tearDown(self): + testing.tearDown() + + def _callFUT(self, request): + from pyramid.i18n import negotiate_locale_name + return negotiate_locale_name(request) + + def _registerImpl(self, impl): + from pyramid.threadlocal import get_current_registry + registry = get_current_registry() + from pyramid.interfaces import ILocaleNegotiator + registry.registerUtility(impl, ILocaleNegotiator) + + def test_no_registry_on_request(self): + self._registerImpl(dummy_negotiator) + request = DummyRequest() + result = self._callFUT(request) + self.assertEqual(result, 'bogus') + + def test_with_registry_on_request(self): + from pyramid.threadlocal import get_current_registry + registry = get_current_registry() + self._registerImpl(dummy_negotiator) + request = DummyRequest() + request.registry = registry + result = self._callFUT(request) + self.assertEqual(result, 'bogus') + + def test_default_from_settings(self): + from pyramid.threadlocal import get_current_registry + registry = get_current_registry() + settings = {'default_locale_name':'settings'} + registry.settings = settings + request = DummyRequest() + request.registry = registry + result = self._callFUT(request) + self.assertEqual(result, 'settings') + + def test_use_default_locale_negotiator(self): + from pyramid.threadlocal import get_current_registry + registry = get_current_registry() + request = DummyRequest() + request.registry = registry + request._LOCALE_ = 'locale' + result = self._callFUT(request) + self.assertEqual(result, 'locale') + + def test_default_default(self): + request = DummyRequest() + result = self._callFUT(request) + self.assertEqual(result, 'en') + +class Test_get_locale_name(unittest.TestCase): + def setUp(self): + testing.setUp() + + def tearDown(self): + testing.tearDown() + + def _callFUT(self, request): + from pyramid.i18n import get_locale_name + return get_locale_name(request) + + def test_name_on_request(self): + request = DummyRequest() + request.locale_name = 'ie' + result = self._callFUT(request) + self.assertEqual(result, 'ie') + +class Test_make_localizer(unittest.TestCase): + def setUp(self): + testing.setUp() + + def tearDown(self): + testing.tearDown() + + def _callFUT(self, locale, tdirs): + from pyramid.i18n import make_localizer + return make_localizer(locale, tdirs) + + def test_locale_from_mo(self): + from pyramid.i18n import Localizer + localedirs = [localedir] + locale_name = 'de' + result = self._callFUT(locale_name, localedirs) + self.assertEqual(result.__class__, Localizer) + self.assertEqual(result.translate('Approve', 'deformsite'), + 'Genehmigen') + self.assertEqual(result.translate('Approve'), 'Approve') + self.assertTrue(hasattr(result, 'pluralize')) + + def test_locale_from_mo_bad_mo(self): + from pyramid.i18n import Localizer + localedirs = [localedir] + locale_name = 'be' + result = self._callFUT(locale_name, localedirs) + self.assertEqual(result.__class__, Localizer) + self.assertEqual(result.translate('Approve', 'deformsite'), + 'Approve') + + def test_locale_from_mo_mo_isdir(self): + from pyramid.i18n import Localizer + localedirs = [localedir] + locale_name = 'gb' + result = self._callFUT(locale_name, localedirs) + self.assertEqual(result.__class__, Localizer) + self.assertEqual(result.translate('Approve', 'deformsite'), + 'Approve') + + def test_territory_fallback(self): + from pyramid.i18n import Localizer + localedirs = [localedir] + locale_name = 'de_DE' + result = self._callFUT(locale_name, localedirs) + self.assertEqual(result.__class__, Localizer) + self.assertEqual(result.translate('Submit', 'deformsite'), + 'different') # prefer translations from de_DE locale + self.assertEqual(result.translate('Approve', 'deformsite'), + 'Genehmigen') # missing from de_DE locale, but in de + +class Test_get_localizer(unittest.TestCase): + def setUp(self): + testing.setUp() + + def tearDown(self): + testing.tearDown() + + def _callFUT(self, request): + from pyramid.i18n import get_localizer + return get_localizer(request) + + def test_it(self): + request = DummyRequest() + request.localizer = 'localizer' + self.assertEqual(self._callFUT(request), 'localizer') + +class Test_default_locale_negotiator(unittest.TestCase): + def setUp(self): + testing.setUp() + + def tearDown(self): + testing.tearDown() + + def _callFUT(self, request): + from pyramid.i18n import default_locale_negotiator + return default_locale_negotiator(request) + + def test_from_none(self): + request = DummyRequest() + result = self._callFUT(request) + self.assertEqual(result, None) + + def test_from_request_attr(self): + request = DummyRequest() + request._LOCALE_ = 'foo' + result = self._callFUT(request) + self.assertEqual(result, 'foo') + + def test_from_params(self): + request = DummyRequest() + request.params['_LOCALE_'] = 'foo' + result = self._callFUT(request) + self.assertEqual(result, 'foo') + + def test_from_cookies(self): + request = DummyRequest() + request.cookies['_LOCALE_'] = 'foo' + result = self._callFUT(request) + self.assertEqual(result, 'foo') + +class TestTranslations(unittest.TestCase): + def _getTargetClass(self): + from pyramid.i18n import Translations + return Translations + + def _makeOne(self): + messages1 = [ + ('foo', 'Voh'), + (('foo1', 1), 'Voh1'), + ] + messages2 = [ + ('foo', 'VohD'), + (('foo1', 1), 'VohD1'), + ] + + klass = self._getTargetClass() + + translations1 = klass(None, domain='messages') + translations1._catalog = dict(messages1) + translations1.plural = lambda *arg: 1 + translations2 = klass(None, domain='messages1') + translations2._catalog = dict(messages2) + translations2.plural = lambda *arg: 1 + translations = translations1.add(translations2, merge=False) + return translations + + def test_load_locales_None(self): + import gettext + klass = self._getTargetClass() + result = klass.load(localedir, None, domain=None) + self.assertEqual(result.__class__, gettext.NullTranslations) + + def test_load_domain_None(self): + import gettext + locales = ['de', 'en'] + klass = self._getTargetClass() + result = klass.load(localedir, locales, domain=None) + self.assertEqual(result.__class__, gettext.NullTranslations) + + def test_load_found_locale_and_domain(self): + locales = ['de', 'en'] + klass = self._getTargetClass() + result = klass.load(localedir, locales, domain='deformsite') + self.assertEqual(result.__class__, klass) + + def test_load_found_locale_and_domain_locale_is_string(self): + locales = 'de' + klass = self._getTargetClass() + result = klass.load(localedir, locales, domain='deformsite') + self.assertEqual(result.__class__, klass) + + def test___repr__(self): + inst = self._makeOne() + result = repr(inst) + self.assertEqual(result, '') + + def test_merge_not_gnutranslations(self): + inst = self._makeOne() + self.assertEqual(inst.merge(None), inst) + + def test_merge_gnutranslations(self): + inst = self._makeOne() + inst2 = self._makeOne() + inst2._catalog['a'] = 'b' + inst.merge(inst2) + self.assertEqual(inst._catalog['a'], 'b') + + def test_merge_gnutranslations_not_translations(self): + import gettext + t = gettext.GNUTranslations() + t._catalog = {'a':'b'} + inst = self._makeOne() + inst.merge(t) + self.assertEqual(inst._catalog['a'], 'b') + + def test_add_different_domain_merge_true_notexisting(self): + inst = self._makeOne() + inst2 = self._makeOne() + inst2.domain = 'domain2' + inst.add(inst2) + self.assertEqual(inst._domains['domain2'], inst2) + + def test_add_different_domain_merge_true_existing(self): + inst = self._makeOne() + inst2 = self._makeOne() + inst3 = self._makeOne() + inst2.domain = 'domain2' + inst2._catalog['a'] = 'b' + inst3.domain = 'domain2' + inst._domains['domain2'] = inst3 + inst.add(inst2) + self.assertEqual(inst._domains['domain2'], inst3) + self.assertEqual(inst3._catalog['a'], 'b') + + def test_add_same_domain_merge_true(self): + inst = self._makeOne() + inst2 = self._makeOne() + inst2._catalog['a'] = 'b' + 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') + self.assertEqual(t.dgettext('messages1', 'foo'), 'VohD') + + def test_ldgettext(self): + t = self._makeOne() + self.assertEqual(t.ldgettext('messages', 'foo'), b'Voh') + self.assertEqual(t.ldgettext('messages1', 'foo'), b'VohD') + + def test_dugettext(self): + t = self._makeOne() + self.assertEqual(t.dugettext('messages', 'foo'), 'Voh') + self.assertEqual(t.dugettext('messages1', 'foo'), 'VohD') + + def test_dngettext(self): + t = self._makeOne() + self.assertEqual(t.dngettext('messages', 'foo1', 'foos1', 1), 'Voh1') + self.assertEqual(t.dngettext('messages1', 'foo1', 'foos1', 1), 'VohD1') + + def test_ldngettext(self): + t = self._makeOne() + self.assertEqual(t.ldngettext('messages', 'foo1', 'foos1', 1), b'Voh1') + self.assertEqual(t.ldngettext('messages1', 'foo1', 'foos1', 1),b'VohD1') + + def test_dungettext(self): + t = self._makeOne() + self.assertEqual(t.dungettext('messages', 'foo1', 'foos1', 1), 'Voh1') + self.assertEqual(t.dungettext('messages1', 'foo1', 'foos1', 1), 'VohD1') + + def test_default_germanic_pluralization(self): + t = self._getTargetClass()() + t._catalog = {} + result = t.dungettext('messages', 'foo1', 'foos1', 2) + self.assertEqual(result, 'foos1') + +class TestLocalizerRequestMixin(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def _makeOne(self): + from pyramid.i18n import LocalizerRequestMixin + request = LocalizerRequestMixin() + request.registry = self.config.registry + request.cookies = {} + request.params = {} + return request + + def test_default_localizer(self): + # `localizer` returns a default localizer for `en` + from pyramid.i18n import Localizer + request = self._makeOne() + self.assertEqual(request.localizer.__class__, Localizer) + self.assertEqual(request.locale_name, 'en') + + def test_custom_localizer_for_default_locale(self): + from pyramid.interfaces import ILocalizer + dummy = object() + self.config.registry.registerUtility(dummy, ILocalizer, name='en') + request = self._makeOne() + self.assertEqual(request.localizer, dummy) + + def test_custom_localizer_for_custom_locale(self): + from pyramid.interfaces import ILocalizer + dummy = object() + self.config.registry.registerUtility(dummy, ILocalizer, name='ie') + request = self._makeOne() + request._LOCALE_ = 'ie' + self.assertEqual(request.localizer, dummy) + + def test_localizer_from_mo(self): + from pyramid.interfaces import ITranslationDirectories + from pyramid.i18n import Localizer + localedirs = [localedir] + self.config.registry.registerUtility( + localedirs, ITranslationDirectories) + request = self._makeOne() + request._LOCALE_ = 'de' + result = request.localizer + self.assertEqual(result.__class__, Localizer) + self.assertEqual(result.translate('Approve', 'deformsite'), + 'Genehmigen') + self.assertEqual(result.translate('Approve'), 'Approve') + self.assertTrue(hasattr(result, 'pluralize')) + + def test_localizer_from_mo_bad_mo(self): + from pyramid.interfaces import ITranslationDirectories + from pyramid.i18n import Localizer + localedirs = [localedir] + self.config.registry.registerUtility( + localedirs, ITranslationDirectories) + request = self._makeOne() + request._LOCALE_ = 'be' + result = request.localizer + self.assertEqual(result.__class__, Localizer) + self.assertEqual(result.translate('Approve', 'deformsite'), + 'Approve') + +class DummyRequest(object): + def __init__(self): + self.params = {} + self.cookies = {} + +def dummy_negotiator(request): + return 'bogus' + +class DummyTranslations(object): + def ugettext(self, text): + return text + + gettext = ugettext + + def ungettext(self, singular, plural, n): + return singular + + ngettext = ungettext diff --git a/src/pyramid/tests/test_integration.py b/src/pyramid/tests/test_integration.py new file mode 100644 index 000000000..eedc145ad --- /dev/null +++ b/src/pyramid/tests/test_integration.py @@ -0,0 +1,848 @@ +# -*- coding: utf-8 -*- + +import datetime +import gc +import locale +import os +import unittest + +from pyramid.wsgi import wsgiapp +from pyramid.view import view_config +from pyramid.static import static_view +from pyramid.testing import skip_on +from pyramid.compat import ( + text_, + url_quote, + ) + +from zope.interface import Interface +from webtest import TestApp + +# 5 years from now (more or less) +fiveyrsfuture = datetime.datetime.utcnow() + datetime.timedelta(5*365) + +defaultlocale = locale.getdefaultlocale()[1] + +class INothing(Interface): + pass + +@view_config(for_=INothing) +@wsgiapp +def wsgiapptest(environ, start_response): + """ """ + return '123' + +class WGSIAppPlusViewConfigTests(unittest.TestCase): + def test_it(self): + from venusian import ATTACH_ATTR + import types + self.assertTrue(getattr(wsgiapptest, ATTACH_ATTR)) + self.assertTrue(type(wsgiapptest) is types.FunctionType) + context = DummyContext() + request = DummyRequest() + result = wsgiapptest(context, request) + self.assertEqual(result, '123') + + def test_scanned(self): + from pyramid.interfaces import IRequest + from pyramid.interfaces import IView + from pyramid.interfaces import IViewClassifier + from pyramid.config import Configurator + from pyramid.tests import test_integration + config = Configurator() + config.scan(test_integration) + config.commit() + reg = config.registry + view = reg.adapters.lookup( + (IViewClassifier, IRequest, INothing), IView, name='') + self.assertEqual(view.__original_view__, wsgiapptest) + +class IntegrationBase(object): + root_factory = None + package = None + def setUp(self): + from pyramid.config import Configurator + config = Configurator(root_factory=self.root_factory, + package=self.package) + config.include(self.package) + app = config.make_wsgi_app() + self.testapp = TestApp(app) + self.config = config + + def tearDown(self): + self.config.end() + +here = os.path.dirname(__file__) + +class StaticAppBase(IntegrationBase): + def test_basic(self): + res = self.testapp.get('/minimal.txt', status=200) + _assertBody(res.body, os.path.join(here, 'fixtures/minimal.txt')) + + def test_hidden(self): + res = self.testapp.get('/static/.hiddenfile', status=200) + _assertBody(res.body, os.path.join(here, 'fixtures/static/.hiddenfile')) + + if defaultlocale is not None: # pragma: no cover + # These tests are expected to fail on LANG=C systems due to decode + # errors and on non-Linux systems due to git highchar handling + # vagaries + def test_highchars_in_pathelement(self): + path = os.path.join( + here, + text_('fixtures/static/héhé/index.html', 'utf-8')) + pathdir = os.path.dirname(path) + body = b'hehe\n' + try: + os.makedirs(pathdir) + with open(path, 'wb') as fp: + fp.write(body) + url = url_quote('/static/héhé/index.html') + res = self.testapp.get(url, status=200) + self.assertEqual(res.body, body) + finally: + os.unlink(path) + os.rmdir(pathdir) + + def test_highchars_in_filename(self): + path = os.path.join( + here, + text_('fixtures/static/héhé.html', 'utf-8')) + body = b'hehe file\n' + with open(path, 'wb') as fp: + fp.write(body) + try: + url = url_quote('/static/héhé.html') + res = self.testapp.get(url, status=200) + self.assertEqual(res.body, body) + finally: + os.unlink(path) + + def test_not_modified(self): + self.testapp.extra_environ = { + 'HTTP_IF_MODIFIED_SINCE':httpdate(fiveyrsfuture)} + res = self.testapp.get('/minimal.txt', status=304) + self.assertEqual(res.body, b'') + + def test_file_in_subdir(self): + fn = os.path.join(here, 'fixtures/static/index.html') + res = self.testapp.get('/static/index.html', status=200) + _assertBody(res.body, fn) + + def test_directory_noslash_redir(self): + res = self.testapp.get('/static', status=301) + self.assertEqual(res.headers['Location'], 'http://localhost/static/') + + def test_directory_noslash_redir_preserves_qs(self): + res = self.testapp.get('/static?a=1&b=2', status=301) + self.assertEqual(res.headers['Location'], + 'http://localhost/static/?a=1&b=2') + + def test_directory_noslash_redir_with_scriptname(self): + self.testapp.extra_environ = {'SCRIPT_NAME':'/script_name'} + res = self.testapp.get('/static', status=301) + self.assertEqual(res.headers['Location'], + 'http://localhost/script_name/static/') + + def test_directory_withslash(self): + fn = os.path.join(here, 'fixtures/static/index.html') + res = self.testapp.get('/static/', status=200) + _assertBody(res.body, fn) + + def test_range_inclusive(self): + self.testapp.extra_environ = {'HTTP_RANGE':'bytes=1-2'} + res = self.testapp.get('/static/index.html', status=206) + self.assertEqual(res.body, b'ht') + + def test_range_tilend(self): + self.testapp.extra_environ = {'HTTP_RANGE':'bytes=-5'} + res = self.testapp.get('/static/index.html', status=206) + self.assertEqual(res.body, b'html>') + + def test_range_notbytes(self): + self.testapp.extra_environ = {'HTTP_RANGE':'kHz=-5'} + res = self.testapp.get('/static/index.html', status=200) + _assertBody(res.body, + os.path.join(here, 'fixtures/static/index.html')) + + def test_range_multiple(self): + res = self.testapp.get('/static/index.html', + [('HTTP_RANGE', 'bytes=10-11,11-12')], + status=200) + _assertBody(res.body, + os.path.join(here, 'fixtures/static/index.html')) + + def test_range_oob(self): + self.testapp.extra_environ = {'HTTP_RANGE':'bytes=1000-1002'} + self.testapp.get('/static/index.html', status=416) + + def test_notfound(self): + self.testapp.get('/static/wontbefound.html', status=404) + + def test_oob_dotdotslash(self): + self.testapp.get('/static/../../test_integration.py', status=404) + + def test_oob_dotdotslash_encoded(self): + self.testapp.get('/static/%2E%2E%2F/test_integration.py', status=404) + + def test_oob_slash(self): + self.testapp.get('/%2F/test_integration.py', status=404) + +class TestEventOnlySubscribers(IntegrationBase, unittest.TestCase): + package = 'pyramid.tests.pkgs.eventonly' + + def test_sendfoo(self): + res = self.testapp.get('/sendfoo', status=200) + self.assertEqual(sorted(res.body.split()), [b'foo', b'fooyup']) + + def test_sendfoobar(self): + res = self.testapp.get('/sendfoobar', status=200) + self.assertEqual(sorted(res.body.split()), + [b'foobar', b'foobar2', b'foobaryup', b'foobaryup2']) + +class TestStaticAppUsingAbsPath(StaticAppBase, unittest.TestCase): + package = 'pyramid.tests.pkgs.static_abspath' + +class TestStaticAppUsingAssetSpec(StaticAppBase, unittest.TestCase): + package = 'pyramid.tests.pkgs.static_assetspec' + +class TestStaticAppNoSubpath(unittest.TestCase): + staticapp = static_view(os.path.join(here, 'fixtures'), use_subpath=False) + def _makeRequest(self, extra): + from pyramid.request import Request + from io import BytesIO + kw = {'PATH_INFO':'', + 'SCRIPT_NAME':'', + 'SERVER_NAME':'localhost', + 'SERVER_PORT':'80', + 'REQUEST_METHOD':'GET', + 'wsgi.version':(1,0), + 'wsgi.url_scheme':'http', + 'wsgi.input':BytesIO()} + kw.update(extra) + request = Request(kw) + return request + + def test_basic(self): + request = self._makeRequest({'PATH_INFO':'/minimal.txt'}) + context = DummyContext() + result = self.staticapp(context, request) + self.assertEqual(result.status, '200 OK') + _assertBody(result.body, os.path.join(here, 'fixtures/minimal.txt')) + +class TestStaticAppWithRoutePrefix(IntegrationBase, unittest.TestCase): + package = 'pyramid.tests.pkgs.static_routeprefix' + + def test_includelevel1(self): + res = self.testapp.get('/static/minimal.txt', status=200) + _assertBody(res.body, + os.path.join(here, 'fixtures/minimal.txt')) + + def test_includelevel2(self): + res = self.testapp.get('/prefix/static/index.html', status=200) + _assertBody(res.body, + os.path.join(here, 'fixtures/static/index.html')) + + +class TestFixtureApp(IntegrationBase, unittest.TestCase): + package = 'pyramid.tests.pkgs.fixtureapp' + def test_another(self): + res = self.testapp.get('/another.html', status=200) + self.assertEqual(res.body, b'fixture') + + def test_root(self): + res = self.testapp.get('/', status=200) + self.assertEqual(res.body, b'fixture') + + def test_dummyskin(self): + self.testapp.get('/dummyskin.html', status=404) + + def test_error(self): + res = self.testapp.get('/error.html', status=200) + self.assertEqual(res.body, b'supressed') + + def test_protected(self): + self.testapp.get('/protected.html', status=403) + +class TestStaticPermApp(IntegrationBase, unittest.TestCase): + package = 'pyramid.tests.pkgs.staticpermapp' + root_factory = 'pyramid.tests.pkgs.staticpermapp:RootFactory' + def test_allowed(self): + result = self.testapp.get('/allowed/index.html', status=200) + _assertBody(result.body, + os.path.join(here, 'fixtures/static/index.html')) + + def test_denied_via_acl_global_root_factory(self): + self.testapp.extra_environ = {'REMOTE_USER':'bob'} + self.testapp.get('/protected/index.html', status=403) + + def test_allowed_via_acl_global_root_factory(self): + self.testapp.extra_environ = {'REMOTE_USER':'fred'} + result = self.testapp.get('/protected/index.html', status=200) + _assertBody(result.body, + os.path.join(here, 'fixtures/static/index.html')) + + def test_denied_via_acl_local_root_factory(self): + self.testapp.extra_environ = {'REMOTE_USER':'fred'} + self.testapp.get('/factory_protected/index.html', status=403) + + def test_allowed_via_acl_local_root_factory(self): + self.testapp.extra_environ = {'REMOTE_USER':'bob'} + result = self.testapp.get('/factory_protected/index.html', status=200) + _assertBody(result.body, + os.path.join(here, 'fixtures/static/index.html')) + +class TestCCBug(IntegrationBase, unittest.TestCase): + # "unordered" as reported in IRC by author of + # http://labs.creativecommons.org/2010/01/13/cc-engine-and-web-non-frameworks/ + package = 'pyramid.tests.pkgs.ccbugapp' + def test_rdf(self): + res = self.testapp.get('/licenses/1/v1/rdf', status=200) + self.assertEqual(res.body, b'rdf') + + def test_juri(self): + res = self.testapp.get('/licenses/1/v1/juri', status=200) + self.assertEqual(res.body, b'juri') + +class TestHybridApp(IntegrationBase, unittest.TestCase): + # make sure views registered for a route "win" over views registered + # without one, even though the context of the non-route view may + # be more specific than the route view. + package = 'pyramid.tests.pkgs.hybridapp' + def test_root(self): + res = self.testapp.get('/', status=200) + self.assertEqual(res.body, b'global') + + def test_abc(self): + res = self.testapp.get('/abc', status=200) + self.assertEqual(res.body, b'route') + + def test_def(self): + res = self.testapp.get('/def', status=200) + self.assertEqual(res.body, b'route2') + + def test_ghi(self): + res = self.testapp.get('/ghi', status=200) + self.assertEqual(res.body, b'global') + + def test_jkl(self): + self.testapp.get('/jkl', status=404) + + def test_mno(self): + self.testapp.get('/mno', status=404) + + def test_pqr_global2(self): + res = self.testapp.get('/pqr/global2', status=200) + self.assertEqual(res.body, b'global2') + + def test_error(self): + res = self.testapp.get('/error', status=200) + self.assertEqual(res.body, b'supressed') + + def test_error2(self): + res = self.testapp.get('/error2', status=200) + self.assertEqual(res.body, b'supressed2') + + def test_error_sub(self): + res = self.testapp.get('/error_sub', status=200) + self.assertEqual(res.body, b'supressed2') + +class TestRestBugApp(IntegrationBase, unittest.TestCase): + # test bug reported by delijati 2010/2/3 (http://pastebin.com/d4cc15515) + package = 'pyramid.tests.pkgs.restbugapp' + def test_it(self): + res = self.testapp.get('/pet', status=200) + self.assertEqual(res.body, b'gotten') + +class TestForbiddenAppHasResult(IntegrationBase, unittest.TestCase): + # test that forbidden exception has ACLDenied result attached + package = 'pyramid.tests.pkgs.forbiddenapp' + def test_it(self): + res = self.testapp.get('/x', status=403) + message, result = [x.strip() for x in res.body.split(b'\n')] + self.assertTrue(message.endswith(b'failed permission check')) + self.assertTrue( + result.startswith(b"ACLDenied permission 'private' via ACE " + b"'' in ACL " + b"'' on context")) + self.assertTrue( + result.endswith(b"for principals ['system.Everyone']")) + +class TestViewDecoratorApp(IntegrationBase, unittest.TestCase): + package = 'pyramid.tests.pkgs.viewdecoratorapp' + + def test_first(self): + res = self.testapp.get('/first', status=200) + self.assertTrue(b'OK' in res.body) + + def test_second(self): + res = self.testapp.get('/second', status=200) + self.assertTrue(b'OK2' in res.body) + +class TestNotFoundView(IntegrationBase, unittest.TestCase): + package = 'pyramid.tests.pkgs.notfoundview' + + def test_it(self): + res = self.testapp.get('/wontbefound', status=200) + self.assertTrue(b'generic_notfound' in res.body) + res = self.testapp.get('/bar', status=307) + self.assertEqual(res.location, 'http://localhost/bar/') + res = self.testapp.get('/bar/', status=200) + self.assertTrue(b'OK bar' in res.body) + res = self.testapp.get('/foo', status=307) + self.assertEqual(res.location, 'http://localhost/foo/') + res = self.testapp.get('/foo/', status=200) + self.assertTrue(b'OK foo2' in res.body) + res = self.testapp.get('/baz', status=200) + self.assertTrue(b'baz_notfound' in res.body) + +class TestForbiddenView(IntegrationBase, unittest.TestCase): + package = 'pyramid.tests.pkgs.forbiddenview' + + def test_it(self): + res = self.testapp.get('/foo', status=200) + self.assertTrue(b'foo_forbidden' in res.body) + res = self.testapp.get('/bar', status=200) + self.assertTrue(b'generic_forbidden' in res.body) + +class TestViewPermissionBug(IntegrationBase, unittest.TestCase): + # view_execution_permitted bug as reported by Shane at http://lists.repoze.org/pipermail/repoze-dev/2010-October/003603.html + package = 'pyramid.tests.pkgs.permbugapp' + def test_test(self): + res = self.testapp.get('/test', status=200) + self.assertTrue(b'ACLDenied' in res.body) + + def test_x(self): + self.testapp.get('/x', status=403) + +class TestDefaultViewPermissionBug(IntegrationBase, unittest.TestCase): + # default_view_permission bug as reported by Wiggy at http://lists.repoze.org/pipermail/repoze-dev/2010-October/003602.html + package = 'pyramid.tests.pkgs.defpermbugapp' + def test_x(self): + res = self.testapp.get('/x', status=403) + self.assertTrue(b'failed permission check' in res.body) + + def test_y(self): + res = self.testapp.get('/y', status=403) + self.assertTrue(b'failed permission check' in res.body) + + def test_z(self): + res = self.testapp.get('/z', status=200) + self.assertTrue(b'public' in res.body) + +from pyramid.tests.pkgs.exceptionviewapp.models import \ + AnException, NotAnException +excroot = {'anexception':AnException(), + 'notanexception':NotAnException()} + +class TestExceptionViewsApp(IntegrationBase, unittest.TestCase): + package = 'pyramid.tests.pkgs.exceptionviewapp' + root_factory = lambda *arg: excroot + def test_root(self): + res = self.testapp.get('/', status=200) + self.assertTrue(b'maybe' in res.body) + + def test_notanexception(self): + res = self.testapp.get('/notanexception', status=200) + self.assertTrue(b'no' in res.body) + + def test_anexception(self): + res = self.testapp.get('/anexception', status=200) + self.assertTrue(b'yes' in res.body) + + def test_route_raise_exception(self): + res = self.testapp.get('/route_raise_exception', status=200) + self.assertTrue(b'yes' in res.body) + + def test_route_raise_exception2(self): + res = self.testapp.get('/route_raise_exception2', status=200) + self.assertTrue(b'yes' in res.body) + + def test_route_raise_exception3(self): + res = self.testapp.get('/route_raise_exception3', status=200) + self.assertTrue(b'whoa' in res.body) + + def test_route_raise_exception4(self): + res = self.testapp.get('/route_raise_exception4', status=200) + self.assertTrue(b'whoa' in res.body) + + def test_raise_httpexception(self): + res = self.testapp.get('/route_raise_httpexception', status=200) + self.assertTrue(b'caught' in res.body) + +class TestConflictApp(unittest.TestCase): + package = 'pyramid.tests.pkgs.conflictapp' + def _makeConfig(self): + from pyramid.config import Configurator + config = Configurator() + return config + + def test_autoresolved_view(self): + config = self._makeConfig() + config.include(self.package) + app = config.make_wsgi_app() + self.testapp = TestApp(app) + res = self.testapp.get('/') + self.assertTrue(b'a view' in res.body) + res = self.testapp.get('/route') + self.assertTrue(b'route view' in res.body) + + def test_overridden_autoresolved_view(self): + from pyramid.response import Response + config = self._makeConfig() + config.include(self.package) + def thisview(request): + return Response('this view') + config.add_view(thisview) + app = config.make_wsgi_app() + self.testapp = TestApp(app) + res = self.testapp.get('/') + self.assertTrue(b'this view' in res.body) + + def test_overridden_route_view(self): + from pyramid.response import Response + config = self._makeConfig() + config.include(self.package) + def thisview(request): + return Response('this view') + config.add_view(thisview, route_name='aroute') + app = config.make_wsgi_app() + self.testapp = TestApp(app) + res = self.testapp.get('/route') + self.assertTrue(b'this view' in res.body) + + def test_nonoverridden_authorization_policy(self): + config = self._makeConfig() + config.include(self.package) + app = config.make_wsgi_app() + self.testapp = TestApp(app) + res = self.testapp.get('/protected', status=403) + self.assertTrue(b'403 Forbidden' in res.body) + + def test_overridden_authorization_policy(self): + config = self._makeConfig() + config.include(self.package) + from pyramid.testing import DummySecurityPolicy + config.set_authorization_policy(DummySecurityPolicy('fred')) + config.set_authentication_policy(DummySecurityPolicy(permissive=True)) + app = config.make_wsgi_app() + self.testapp = TestApp(app) + res = self.testapp.get('/protected', status=200) + self.assertTrue('protected view' in res) + +class ImperativeIncludeConfigurationTest(unittest.TestCase): + def setUp(self): + from pyramid.config import Configurator + config = Configurator() + from pyramid.tests.pkgs.includeapp1.root import configure + configure(config) + app = config.make_wsgi_app() + self.testapp = TestApp(app) + self.config = config + + def tearDown(self): + self.config.end() + + def test_root(self): + res = self.testapp.get('/', status=200) + self.assertTrue(b'root' in res.body) + + def test_two(self): + res = self.testapp.get('/two', status=200) + self.assertTrue(b'two' in res.body) + + def test_three(self): + res = self.testapp.get('/three', status=200) + self.assertTrue(b'three' in res.body) + +class SelfScanAppTest(unittest.TestCase): + def setUp(self): + from pyramid.tests.test_config.pkgs.selfscan import main + config = main() + app = config.make_wsgi_app() + self.testapp = TestApp(app) + self.config = config + + def tearDown(self): + self.config.end() + + def test_root(self): + res = self.testapp.get('/', status=200) + self.assertTrue(b'root' in res.body) + + def test_two(self): + res = self.testapp.get('/two', status=200) + self.assertTrue(b'two' in res.body) + +class WSGIApp2AppTest(unittest.TestCase): + def setUp(self): + from pyramid.tests.pkgs.wsgiapp2app import main + config = main() + app = config.make_wsgi_app() + self.testapp = TestApp(app) + self.config = config + + def tearDown(self): + self.config.end() + + def test_hello(self): + res = self.testapp.get('/hello', status=200) + self.assertTrue(b'Hello' in res.body) + +class SubrequestAppTest(unittest.TestCase): + def setUp(self): + from pyramid.tests.pkgs.subrequestapp import main + config = main() + app = config.make_wsgi_app() + self.testapp = TestApp(app) + self.config = config + + def tearDown(self): + self.config.end() + + def test_one(self): + res = self.testapp.get('/view_one', status=200) + self.assertTrue(b'This came from view_two, foo=bar' in res.body) + + def test_three(self): + res = self.testapp.get('/view_three', status=500) + self.assertTrue(b'Bad stuff happened' in res.body) + + def test_five(self): + res = self.testapp.get('/view_five', status=200) + self.assertTrue(b'Value error raised' in res.body) + +class RendererScanAppTest(IntegrationBase, unittest.TestCase): + package = 'pyramid.tests.pkgs.rendererscanapp' + def test_root(self): + res = self.testapp.get('/one', status=200) + self.assertTrue(b'One!' in res.body) + + def test_two(self): + res = self.testapp.get('/two', status=200) + self.assertTrue(b'Two!' in res.body) + + def test_rescan(self): + self.config.scan('pyramid.tests.pkgs.rendererscanapp') + app = self.config.make_wsgi_app() + testapp = TestApp(app) + res = testapp.get('/one', status=200) + self.assertTrue(b'One!' in res.body) + res = testapp.get('/two', status=200) + self.assertTrue(b'Two!' in res.body) + +class UnicodeInURLTest(unittest.TestCase): + def _makeConfig(self): + from pyramid.config import Configurator + config = Configurator() + return config + + def _makeTestApp(self, config): + app = config.make_wsgi_app() + return TestApp(app) + + def test_unicode_in_url_404(self): + request_path = '/avalia%C3%A7%C3%A3o_participante' + request_path_unicode = b'/avalia\xc3\xa7\xc3\xa3o_participante'.decode('utf-8') + + config = self._makeConfig() + testapp = self._makeTestApp(config) + + res = testapp.get(request_path, status=404) + + # Pyramid default 404 handler outputs: + # u'404 Not Found\n\nThe resource could not be found.\n\n\n' + # u'/avalia\xe7\xe3o_participante\n\n' + self.assertTrue(request_path_unicode in res.text) + + def test_unicode_in_url_200(self): + request_path = '/avalia%C3%A7%C3%A3o_participante' + request_path_unicode = b'/avalia\xc3\xa7\xc3\xa3o_participante'.decode('utf-8') + + def myview(request): + return 'XXX' + + config = self._makeConfig() + config.add_route('myroute', request_path_unicode) + config.add_view(myview, route_name='myroute', renderer='json') + testapp = self._makeTestApp(config) + + res = testapp.get(request_path, status=200) + + self.assertEqual(res.text, '"XXX"') + + +class AcceptContentTypeTest(unittest.TestCase): + def _makeConfig(self): + def hello_view(request): + return {'message': 'Hello!'} + from pyramid.config import Configurator + config = Configurator() + config.add_route('hello', '/hello') + config.add_view(hello_view, route_name='hello', + accept='text/plain', renderer='string') + config.add_view(hello_view, route_name='hello', + accept='application/json', renderer='json') + def hello_fallback_view(request): + request.response.content_type = 'text/x-fallback' + return 'hello fallback' + config.add_view(hello_fallback_view, route_name='hello', + renderer='string') + return config + + def _makeTestApp(self, config): + app = config.make_wsgi_app() + return TestApp(app) + + def tearDown(self): + import pyramid.config + pyramid.config.global_registries.empty() + + def test_client_side_ordering(self): + config = self._makeConfig() + app = self._makeTestApp(config) + res = app.get('/hello', headers={ + 'Accept': 'application/json; q=1.0, text/plain; q=0.9', + }, status=200) + self.assertEqual(res.content_type, 'application/json') + res = app.get('/hello', headers={ + 'Accept': 'text/plain; q=0.9, application/json; q=1.0', + }, status=200) + self.assertEqual(res.content_type, 'application/json') + res = app.get('/hello', headers={'Accept': 'application/*'}, status=200) + self.assertEqual(res.content_type, 'application/json') + res = app.get('/hello', headers={'Accept': 'text/*'}, status=200) + self.assertEqual(res.content_type, 'text/plain') + res = app.get('/hello', headers={'Accept': 'something/else'}, status=200) + self.assertEqual(res.content_type, 'text/x-fallback') + + def test_default_server_side_ordering(self): + config = self._makeConfig() + app = self._makeTestApp(config) + res = app.get('/hello', headers={ + 'Accept': 'application/json, text/plain', + }, status=200) + self.assertEqual(res.content_type, 'text/plain') + res = app.get('/hello', headers={ + 'Accept': 'text/plain, application/json', + }, status=200) + self.assertEqual(res.content_type, 'text/plain') + res = app.get('/hello', headers={'Accept': '*/*'}, status=200) + self.assertEqual(res.content_type, 'text/plain') + res = app.get('/hello', status=200) + self.assertEqual(res.content_type, 'text/plain') + res = app.get('/hello', headers={'Accept': 'invalid'}, status=200) + self.assertEqual(res.content_type, 'text/plain') + res = app.get('/hello', headers={'Accept': 'something/else'}, status=200) + self.assertEqual(res.content_type, 'text/x-fallback') + + def test_custom_server_side_ordering(self): + config = self._makeConfig() + config.add_accept_view_order( + 'application/json', weighs_more_than='text/plain') + app = self._makeTestApp(config) + res = app.get('/hello', headers={ + 'Accept': 'application/json, text/plain', + }, status=200) + self.assertEqual(res.content_type, 'application/json') + res = app.get('/hello', headers={ + 'Accept': 'text/plain, application/json', + }, status=200) + self.assertEqual(res.content_type, 'application/json') + res = app.get('/hello', headers={'Accept': '*/*'}, status=200) + self.assertEqual(res.content_type, 'application/json') + res = app.get('/hello', status=200) + self.assertEqual(res.content_type, 'application/json') + res = app.get('/hello', headers={'Accept': 'invalid'}, status=200) + self.assertEqual(res.content_type, 'application/json') + res = app.get('/hello', headers={'Accept': 'something/else'}, status=200) + self.assertEqual(res.content_type, 'text/x-fallback') + + def test_deprecated_ranges_in_route_predicate(self): + config = self._makeConfig() + config.add_route('foo', '/foo', accept='text/*') + config.add_view(lambda r: 'OK', route_name='foo', renderer='string') + app = self._makeTestApp(config) + res = app.get('/foo', headers={ + 'Accept': 'application/json; q=1.0, text/plain; q=0.9', + }, status=200) + self.assertEqual(res.content_type, 'text/plain') + self.assertEqual(res.body, b'OK') + res = app.get('/foo', headers={ + 'Accept': 'application/json', + }, status=404) + self.assertEqual(res.content_type, 'application/json') + + def test_deprecated_ranges_in_view_predicate(self): + config = self._makeConfig() + config.add_route('foo', '/foo') + config.add_view(lambda r: 'OK', route_name='foo', + accept='text/*', renderer='string') + app = self._makeTestApp(config) + res = app.get('/foo', headers={ + 'Accept': 'application/json; q=1.0, text/plain; q=0.9', + }, status=200) + self.assertEqual(res.content_type, 'text/plain') + self.assertEqual(res.body, b'OK') + res = app.get('/foo', headers={ + 'Accept': 'application/json', + }, status=404) + self.assertEqual(res.content_type, 'application/json') + + +class DummyContext(object): + pass + +class DummyRequest: + subpath = ('__init__.py',) + traversed = None + environ = {'REQUEST_METHOD':'GET', 'wsgi.version':(1,0)} + def get_response(self, application): + return application(None, None) + +def httpdate(ts): + return ts.strftime("%a, %d %b %Y %H:%M:%S GMT") + +def read_(filename): + with open(filename, 'rb') as fp: + val = fp.read() + return val + +def _assertBody(body, filename): + if defaultlocale is None: # pragma: no cover + # If system locale does not have an encoding then default to utf-8 + filename = filename.encode('utf-8') + # strip both \n and \r for windows + body = body.replace(b'\r', b'') + body = body.replace(b'\n', b'') + data = read_(filename) + data = data.replace(b'\r', b'') + data = data.replace(b'\n', b'') + assert(body == data) + + +class MemoryLeaksTest(unittest.TestCase): + + def tearDown(self): + import pyramid.config + pyramid.config.global_registries.empty() + + def get_gc_count(self): + last_collected = 0 + while True: + collected = gc.collect() + if collected == last_collected: + break + last_collected = collected + return len(gc.get_objects()) + + @skip_on('pypy') + def test_memory_leaks(self): + from pyramid.config import Configurator + Configurator().make_wsgi_app() # Initialize all global objects + + initial_count = self.get_gc_count() + Configurator().make_wsgi_app() + current_count = self.get_gc_count() + self.assertEqual(current_count, initial_count) diff --git a/src/pyramid/tests/test_location.py b/src/pyramid/tests/test_location.py new file mode 100644 index 000000000..e1f47f4ab --- /dev/null +++ b/src/pyramid/tests/test_location.py @@ -0,0 +1,40 @@ +import unittest + +class TestInside(unittest.TestCase): + def _callFUT(self, one, two): + from pyramid.location import inside + return inside(one, two) + + def test_inside(self): + o1 = Location() + o2 = Location(); o2.__parent__ = o1 + o3 = Location(); o3.__parent__ = o2 + o4 = Location(); o4.__parent__ = o3 + + self.assertEqual(self._callFUT(o1, o1), True) + self.assertEqual(self._callFUT(o2, o1), True) + self.assertEqual(self._callFUT(o3, o1), True) + self.assertEqual(self._callFUT(o4, o1), True) + self.assertEqual(self._callFUT(o1, o4), False) + self.assertEqual(self._callFUT(o1, None), False) + +class TestLineage(unittest.TestCase): + def _callFUT(self, context): + from pyramid.location import lineage + return lineage(context) + + def test_lineage(self): + o1 = Location() + o2 = Location(); o2.__parent__ = o1 + o3 = Location(); o3.__parent__ = o2 + o4 = Location(); o4.__parent__ = o3 + result = list(self._callFUT(o3)) + self.assertEqual(result, [o3, o2, o1]) + result = list(self._callFUT(o1)) + self.assertEqual(result, [o1]) + +from pyramid.interfaces import ILocation +from zope.interface import implementer +@implementer(ILocation) +class Location(object): + __name__ = __parent__ = None diff --git a/src/pyramid/tests/test_paster.py b/src/pyramid/tests/test_paster.py new file mode 100644 index 000000000..784458647 --- /dev/null +++ b/src/pyramid/tests/test_paster.py @@ -0,0 +1,168 @@ +import os +import unittest +from pyramid.tests.test_scripts.dummy import DummyLoader + +here = os.path.dirname(__file__) + +class Test_get_app(unittest.TestCase): + def _callFUT(self, config_file, section_name, options=None, _loader=None): + import pyramid.paster + old_loader = pyramid.paster.get_config_loader + try: + if _loader is not None: + pyramid.paster.get_config_loader = _loader + return pyramid.paster.get_app(config_file, section_name, + options=options) + finally: + pyramid.paster.get_config_loader = old_loader + + def test_it(self): + app = DummyApp() + loader = DummyLoader(app=app) + result = self._callFUT( + '/foo/bar/myapp.ini', 'myapp', options={'a': 'b'}, + _loader=loader) + self.assertEqual(loader.uri.path, '/foo/bar/myapp.ini') + self.assertEqual(len(loader.calls), 1) + self.assertEqual(loader.calls[0]['op'], 'app') + self.assertEqual(loader.calls[0]['name'], 'myapp') + self.assertEqual(loader.calls[0]['defaults'], {'a': 'b'}) + self.assertEqual(result, app) + + def test_it_with_dummyapp_requiring_options(self): + options = {'bar': 'baz'} + app = self._callFUT( + os.path.join(here, 'fixtures', 'dummy.ini'), + 'myapp', options=options) + self.assertEqual(app.settings['foo'], 'baz') + +class Test_get_appsettings(unittest.TestCase): + def _callFUT(self, config_file, section_name, options=None, _loader=None): + import pyramid.paster + old_loader = pyramid.paster.get_config_loader + try: + if _loader is not None: + pyramid.paster.get_config_loader = _loader + return pyramid.paster.get_appsettings(config_file, section_name, + options=options) + finally: + pyramid.paster.get_config_loader = old_loader + + def test_it(self): + values = {'a': 1} + loader = DummyLoader(app_settings=values) + result = self._callFUT( + '/foo/bar/myapp.ini', 'myapp', options={'a': 'b'}, + _loader=loader) + self.assertEqual(loader.uri.path, '/foo/bar/myapp.ini') + self.assertEqual(len(loader.calls), 1) + self.assertEqual(loader.calls[0]['op'], 'app_settings') + self.assertEqual(loader.calls[0]['name'], 'myapp') + self.assertEqual(loader.calls[0]['defaults'], {'a': 'b'}) + self.assertEqual(result, values) + + def test_it_with_dummyapp_requiring_options(self): + options = {'bar': 'baz'} + result = self._callFUT( + os.path.join(here, 'fixtures', 'dummy.ini'), + 'myapp', options=options) + self.assertEqual(result['foo'], 'baz') + +class Test_setup_logging(unittest.TestCase): + def _callFUT(self, config_file, global_conf=None, _loader=None): + import pyramid.paster + old_loader = pyramid.paster.get_config_loader + try: + if _loader is not None: + pyramid.paster.get_config_loader = _loader + return pyramid.paster.setup_logging(config_file, global_conf) + finally: + pyramid.paster.get_config_loader = old_loader + + def test_it_no_global_conf(self): + loader = DummyLoader() + self._callFUT('/abc.ini', _loader=loader) + self.assertEqual(loader.uri.path, '/abc.ini') + self.assertEqual(len(loader.calls), 1) + self.assertEqual(loader.calls[0]['op'], 'logging') + self.assertEqual(loader.calls[0]['defaults'], None) + + def test_it_global_conf_empty(self): + loader = DummyLoader() + self._callFUT('/abc.ini', global_conf={}, _loader=loader) + self.assertEqual(loader.uri.path, '/abc.ini') + self.assertEqual(len(loader.calls), 1) + self.assertEqual(loader.calls[0]['op'], 'logging') + self.assertEqual(loader.calls[0]['defaults'], {}) + + def test_it_global_conf_not_empty(self): + loader = DummyLoader() + self._callFUT('/abc.ini', global_conf={'key': 'val'}, _loader=loader) + self.assertEqual(loader.uri.path, '/abc.ini') + self.assertEqual(len(loader.calls), 1) + self.assertEqual(loader.calls[0]['op'], 'logging') + self.assertEqual(loader.calls[0]['defaults'], {'key': 'val'}) + +class Test_bootstrap(unittest.TestCase): + def _callFUT(self, config_uri, request=None): + from pyramid.paster import bootstrap + return bootstrap(config_uri, request) + + def setUp(self): + import pyramid.paster + self.original_get_app = pyramid.paster.get_app + self.original_prepare = pyramid.paster.prepare + self.app = app = DummyApp() + self.root = root = Dummy() + + class DummyGetApp(object): + def __call__(self, *a, **kw): + self.a = a + self.kw = kw + return app + self.get_app = pyramid.paster.get_app = DummyGetApp() + + class DummyPrepare(object): + def __call__(self, *a, **kw): + self.a = a + self.kw = kw + return {'root':root, 'closer':lambda: None} + self.getroot = pyramid.paster.prepare = DummyPrepare() + + def tearDown(self): + import pyramid.paster + pyramid.paster.get_app = self.original_get_app + pyramid.paster.prepare = self.original_prepare + + def test_it_request_with_registry(self): + request = DummyRequest({}) + request.registry = dummy_registry + result = self._callFUT('/foo/bar/myapp.ini', request) + self.assertEqual(result['app'], self.app) + self.assertEqual(result['root'], self.root) + self.assertTrue('closer' in result) + +class Dummy: + pass + +class DummyRegistry(object): + settings = {} + +dummy_registry = DummyRegistry() + +class DummyApp: + def __init__(self): + self.registry = dummy_registry + +def make_dummyapp(global_conf, **settings): + app = DummyApp() + app.settings = settings + app.global_conf = global_conf + return app + +class DummyRequest: + application_url = 'http://example.com:5432' + script_name = '' + def __init__(self, environ): + self.environ = environ + self.matchdict = {} diff --git a/src/pyramid/tests/test_path.py b/src/pyramid/tests/test_path.py new file mode 100644 index 000000000..563ece6d6 --- /dev/null +++ b/src/pyramid/tests/test_path.py @@ -0,0 +1,576 @@ +import unittest +import os +from pyramid.compat import PY2 + +here = os.path.abspath(os.path.dirname(__file__)) + +class TestCallerPath(unittest.TestCase): + def tearDown(self): + from pyramid.tests import test_path + if hasattr(test_path, '__abspath__'): + del test_path.__abspath__ + + def _callFUT(self, path, level=2): + from pyramid.path import caller_path + return caller_path(path, level) + + def test_isabs(self): + result = self._callFUT('/a/b/c') + self.assertEqual(result, '/a/b/c') + + def test_pkgrelative(self): + import os + result = self._callFUT('a/b/c') + self.assertEqual(result, os.path.join(here, 'a/b/c')) + + def test_memoization_has_abspath(self): + import os + from pyramid.tests import test_path + test_path.__abspath__ = '/foo/bar' + result = self._callFUT('a/b/c') + self.assertEqual(result, os.path.join('/foo/bar', 'a/b/c')) + + def test_memoization_success(self): + import os + from pyramid.tests import test_path + result = self._callFUT('a/b/c') + self.assertEqual(result, os.path.join(here, 'a/b/c')) + self.assertEqual(test_path.__abspath__, here) + +class TestCallerModule(unittest.TestCase): + def _callFUT(self, *arg, **kw): + from pyramid.path import caller_module + return caller_module(*arg, **kw) + + def test_it_level_1(self): + from pyramid.tests import test_path + result = self._callFUT(1) + self.assertEqual(result, test_path) + + def test_it_level_2(self): + from pyramid.tests import test_path + result = self._callFUT(2) + self.assertEqual(result, test_path) + + def test_it_level_3(self): + from pyramid.tests import test_path + result = self._callFUT(3) + self.assertNotEqual(result, test_path) + + def test_it_no___name__(self): + class DummyFrame(object): + f_globals = {} + class DummySys(object): + def _getframe(self, level): + return DummyFrame() + modules = {'__main__':'main'} + dummy_sys = DummySys() + result = self._callFUT(3, sys=dummy_sys) + self.assertEqual(result, 'main') + + +class TestCallerPackage(unittest.TestCase): + def _callFUT(self, *arg, **kw): + from pyramid.path import caller_package + return caller_package(*arg, **kw) + + def test_it_level_1(self): + from pyramid import tests + result = self._callFUT(1) + self.assertEqual(result, tests) + + def test_it_level_2(self): + from pyramid import tests + result = self._callFUT(2) + self.assertEqual(result, tests) + + def test_it_level_3(self): + import unittest + result = self._callFUT(3) + self.assertEqual(result, unittest) + + def test_it_package(self): + import pyramid.tests + def dummy_caller_module(*arg): + return pyramid.tests + result = self._callFUT(1, caller_module=dummy_caller_module) + self.assertEqual(result, pyramid.tests) + +class TestPackagePath(unittest.TestCase): + def _callFUT(self, package): + from pyramid.path import package_path + return package_path(package) + + def test_it_package(self): + from pyramid import tests + package = DummyPackageOrModule(tests) + result = self._callFUT(package) + self.assertEqual(result, package.package_path) + + def test_it_module(self): + from pyramid.tests import test_path + module = DummyPackageOrModule(test_path) + result = self._callFUT(module) + self.assertEqual(result, module.package_path) + + def test_memoization_success(self): + from pyramid.tests import test_path + module = DummyPackageOrModule(test_path) + self._callFUT(module) + self.assertEqual(module.__abspath__, module.package_path) + + def test_memoization_fail(self): + from pyramid.tests import test_path + module = DummyPackageOrModule(test_path, raise_exc=TypeError) + result = self._callFUT(module) + self.assertFalse(hasattr(module, '__abspath__')) + self.assertEqual(result, module.package_path) + +class TestPackageOf(unittest.TestCase): + def _callFUT(self, package): + from pyramid.path import package_of + return package_of(package) + + def test_it_package(self): + from pyramid import tests + package = DummyPackageOrModule(tests) + result = self._callFUT(package) + self.assertEqual(result, tests) + + def test_it_module(self): + import pyramid.tests.test_path + from pyramid import tests + package = DummyPackageOrModule(pyramid.tests.test_path) + result = self._callFUT(package) + self.assertEqual(result, tests) + +class TestPackageName(unittest.TestCase): + def _callFUT(self, package): + from pyramid.path import package_name + return package_name(package) + + def test_it_package(self): + from pyramid import tests + package = DummyPackageOrModule(tests) + result = self._callFUT(package) + self.assertEqual(result, 'pyramid.tests') + + def test_it_namespace_package(self): + from pyramid import tests + package = DummyNamespacePackage(tests) + result = self._callFUT(package) + self.assertEqual(result, 'pyramid.tests') + + def test_it_module(self): + from pyramid.tests import test_path + module = DummyPackageOrModule(test_path) + result = self._callFUT(module) + self.assertEqual(result, 'pyramid.tests') + + def test_it_None(self): + result = self._callFUT(None) + self.assertEqual(result, '__main__') + + def test_it_main(self): + import __main__ + result = self._callFUT(__main__) + self.assertEqual(result, '__main__') + +class TestResolver(unittest.TestCase): + def _getTargetClass(self): + from pyramid.path import Resolver + return Resolver + + def _makeOne(self, package): + return self._getTargetClass()(package) + + def test_get_package_caller_package(self): + import pyramid.tests + from pyramid.path import CALLER_PACKAGE + self.assertEqual(self._makeOne(CALLER_PACKAGE).get_package(), + pyramid.tests) + + def test_get_package_name_caller_package(self): + from pyramid.path import CALLER_PACKAGE + self.assertEqual(self._makeOne(CALLER_PACKAGE).get_package_name(), + 'pyramid.tests') + + def test_get_package_string(self): + import pyramid.tests + self.assertEqual(self._makeOne('pyramid.tests').get_package(), + pyramid.tests) + + def test_get_package_name_string(self): + self.assertEqual(self._makeOne('pyramid.tests').get_package_name(), + 'pyramid.tests') + +class TestAssetResolver(unittest.TestCase): + def _getTargetClass(self): + from pyramid.path import AssetResolver + return AssetResolver + + def _makeOne(self, package='pyramid.tests'): + return self._getTargetClass()(package) + + def test_ctor_as_package(self): + import sys + tests = sys.modules['pyramid.tests'] + inst = self._makeOne(tests) + self.assertEqual(inst.package, tests) + + def test_ctor_as_str(self): + import sys + tests = sys.modules['pyramid.tests'] + inst = self._makeOne('pyramid.tests') + self.assertEqual(inst.package, tests) + + def test_resolve_abspath(self): + from pyramid.path import FSAssetDescriptor + inst = self._makeOne(None) + r = inst.resolve(os.path.join(here, 'test_asset.py')) + self.assertEqual(r.__class__, FSAssetDescriptor) + self.assertTrue(r.exists()) + + def test_resolve_absspec(self): + from pyramid.path import PkgResourcesAssetDescriptor + inst = self._makeOne(None) + r = inst.resolve('pyramid.tests:test_asset.py') + self.assertEqual(r.__class__, PkgResourcesAssetDescriptor) + self.assertTrue(r.exists()) + + def test_resolve_relspec_with_pkg(self): + from pyramid.path import PkgResourcesAssetDescriptor + inst = self._makeOne('pyramid.tests') + r = inst.resolve('test_asset.py') + self.assertEqual(r.__class__, PkgResourcesAssetDescriptor) + self.assertTrue(r.exists()) + + def test_resolve_relspec_no_package(self): + inst = self._makeOne(None) + self.assertRaises(ValueError, inst.resolve, 'test_asset.py') + + def test_resolve_relspec_caller_package(self): + from pyramid.path import PkgResourcesAssetDescriptor + from pyramid.path import CALLER_PACKAGE + inst = self._makeOne(CALLER_PACKAGE) + r = inst.resolve('test_asset.py') + self.assertEqual(r.__class__, PkgResourcesAssetDescriptor) + self.assertTrue(r.exists()) + +class TestPkgResourcesAssetDescriptor(unittest.TestCase): + def _getTargetClass(self): + from pyramid.path import PkgResourcesAssetDescriptor + return PkgResourcesAssetDescriptor + + def _makeOne(self, pkg='pyramid.tests', path='test_asset.py'): + return self._getTargetClass()(pkg, path) + + def test_class_conforms_to_IAssetDescriptor(self): + from pyramid.interfaces import IAssetDescriptor + from zope.interface.verify import verifyClass + verifyClass(IAssetDescriptor, self._getTargetClass()) + + def test_instance_conforms_to_IAssetDescriptor(self): + from pyramid.interfaces import IAssetDescriptor + from zope.interface.verify import verifyObject + verifyObject(IAssetDescriptor, self._makeOne()) + + def test_absspec(self): + inst = self._makeOne() + self.assertEqual(inst.absspec(), 'pyramid.tests:test_asset.py') + + def test_abspath(self): + inst = self._makeOne() + self.assertEqual(inst.abspath(), os.path.join(here, 'test_asset.py')) + + def test_stream(self): + inst = self._makeOne() + inst.pkg_resources = DummyPkgResource() + inst.pkg_resources.resource_stream = lambda x, y: '%s:%s' % (x, y) + s = inst.stream() + self.assertEqual(s, + '%s:%s' % ('pyramid.tests', 'test_asset.py')) + + def test_isdir(self): + inst = self._makeOne() + inst.pkg_resources = DummyPkgResource() + inst.pkg_resources.resource_isdir = lambda x, y: '%s:%s' % (x, y) + self.assertEqual(inst.isdir(), + '%s:%s' % ('pyramid.tests', 'test_asset.py')) + + def test_listdir(self): + inst = self._makeOne() + inst.pkg_resources = DummyPkgResource() + inst.pkg_resources.resource_listdir = lambda x, y: '%s:%s' % (x, y) + self.assertEqual(inst.listdir(), + '%s:%s' % ('pyramid.tests', 'test_asset.py')) + + def test_exists(self): + inst = self._makeOne() + inst.pkg_resources = DummyPkgResource() + inst.pkg_resources.resource_exists = lambda x, y: '%s:%s' % (x, y) + self.assertEqual(inst.exists(), + '%s:%s' % ('pyramid.tests', 'test_asset.py')) + +class TestFSAssetDescriptor(unittest.TestCase): + def _getTargetClass(self): + from pyramid.path import FSAssetDescriptor + return FSAssetDescriptor + + def _makeOne(self, path=os.path.join(here, 'test_asset.py')): + return self._getTargetClass()(path) + + def test_class_conforms_to_IAssetDescriptor(self): + from pyramid.interfaces import IAssetDescriptor + from zope.interface.verify import verifyClass + verifyClass(IAssetDescriptor, self._getTargetClass()) + + def test_instance_conforms_to_IAssetDescriptor(self): + from pyramid.interfaces import IAssetDescriptor + from zope.interface.verify import verifyObject + verifyObject(IAssetDescriptor, self._makeOne()) + + def test_absspec(self): + inst = self._makeOne() + self.assertRaises(NotImplementedError, inst.absspec) + + def test_abspath(self): + inst = self._makeOne() + self.assertEqual(inst.abspath(), os.path.join(here, 'test_asset.py')) + + def test_stream(self): + inst = self._makeOne() + s = inst.stream() + val = s.read() + s.close() + self.assertTrue(b'asset' in val) + + def test_isdir_False(self): + inst = self._makeOne() + self.assertFalse(inst.isdir()) + + def test_isdir_True(self): + inst = self._makeOne(here) + self.assertTrue(inst.isdir()) + + def test_listdir(self): + inst = self._makeOne(here) + self.assertTrue(inst.listdir()) + + def test_exists(self): + inst = self._makeOne() + self.assertTrue(inst.exists()) + +class TestDottedNameResolver(unittest.TestCase): + def _makeOne(self, package=None): + from pyramid.path import DottedNameResolver + return DottedNameResolver(package) + + def config_exc(self, func, *arg, **kw): + try: + func(*arg, **kw) + except ValueError as e: + return e + else: + raise AssertionError('Invalid not raised') # pragma: no cover + + def test_zope_dottedname_style_resolve_builtin(self): + typ = self._makeOne() + if PY2: + result = typ._zope_dottedname_style('__builtin__.str', None) + else: + result = typ._zope_dottedname_style('builtins.str', None) + self.assertEqual(result, str) + + def test_zope_dottedname_style_resolve_absolute(self): + typ = self._makeOne() + result = typ._zope_dottedname_style( + 'pyramid.tests.test_path.TestDottedNameResolver', None) + self.assertEqual(result, self.__class__) + + def test_zope_dottedname_style_irrresolveable_absolute(self): + typ = self._makeOne() + self.assertRaises(ImportError, typ._zope_dottedname_style, + 'pyramid.test_path.nonexisting_name', None) + + def test__zope_dottedname_style_resolve_relative(self): + import pyramid.tests + typ = self._makeOne() + result = typ._zope_dottedname_style( + '.test_path.TestDottedNameResolver', pyramid.tests) + self.assertEqual(result, self.__class__) + + def test__zope_dottedname_style_resolve_relative_leading_dots(self): + import pyramid.tests.test_path + typ = self._makeOne() + result = typ._zope_dottedname_style( + '..tests.test_path.TestDottedNameResolver', pyramid.tests) + self.assertEqual(result, self.__class__) + + def test__zope_dottedname_style_resolve_relative_is_dot(self): + import pyramid.tests + typ = self._makeOne() + result = typ._zope_dottedname_style('.', pyramid.tests) + self.assertEqual(result, pyramid.tests) + + def test__zope_dottedname_style_irresolveable_relative_is_dot(self): + typ = self._makeOne() + e = self.config_exc(typ._zope_dottedname_style, '.', None) + self.assertEqual( + e.args[0], + "relative name '.' irresolveable without package") + + def test_zope_dottedname_style_resolve_relative_nocurrentpackage(self): + typ = self._makeOne() + e = self.config_exc(typ._zope_dottedname_style, '.whatever', None) + self.assertEqual( + e.args[0], + "relative name '.whatever' irresolveable without package") + + def test_zope_dottedname_style_irrresolveable_relative(self): + import pyramid.tests + typ = self._makeOne() + self.assertRaises(ImportError, typ._zope_dottedname_style, + '.notexisting', pyramid.tests) + + def test__zope_dottedname_style_resolveable_relative(self): + import pyramid + typ = self._makeOne() + result = typ._zope_dottedname_style('.tests', pyramid) + from pyramid import tests + self.assertEqual(result, tests) + + def test__zope_dottedname_style_irresolveable_absolute(self): + typ = self._makeOne() + self.assertRaises( + ImportError, + typ._zope_dottedname_style, 'pyramid.fudge.bar', None) + + def test__zope_dottedname_style_resolveable_absolute(self): + typ = self._makeOne() + result = typ._zope_dottedname_style( + 'pyramid.tests.test_path.TestDottedNameResolver', None) + self.assertEqual(result, self.__class__) + + def test__pkg_resources_style_resolve_absolute(self): + typ = self._makeOne() + result = typ._pkg_resources_style( + 'pyramid.tests.test_path:TestDottedNameResolver', None) + self.assertEqual(result, self.__class__) + + def test__pkg_resources_style_irrresolveable_absolute(self): + typ = self._makeOne() + self.assertRaises(ImportError, typ._pkg_resources_style, + 'pyramid.tests:nonexisting', None) + + def test__pkg_resources_style_resolve_relative(self): + import pyramid.tests + typ = self._makeOne() + result = typ._pkg_resources_style( + '.test_path:TestDottedNameResolver', pyramid.tests) + self.assertEqual(result, self.__class__) + + def test__pkg_resources_style_resolve_relative_is_dot(self): + import pyramid.tests + typ = self._makeOne() + result = typ._pkg_resources_style('.', pyramid.tests) + self.assertEqual(result, pyramid.tests) + + def test__pkg_resources_style_resolve_relative_nocurrentpackage(self): + typ = self._makeOne() + self.assertRaises(ValueError, typ._pkg_resources_style, + '.whatever', None) + + def test__pkg_resources_style_irrresolveable_relative(self): + import pyramid + typ = self._makeOne() + self.assertRaises(ImportError, typ._pkg_resources_style, + ':notexisting', pyramid) + + def test_resolve_not_a_string(self): + typ = self._makeOne() + e = self.config_exc(typ.resolve, None) + self.assertEqual(e.args[0], 'None is not a string') + + def test_resolve_using_pkgresources_style(self): + typ = self._makeOne() + result = typ.resolve( + 'pyramid.tests.test_path:TestDottedNameResolver') + self.assertEqual(result, self.__class__) + + def test_resolve_using_zope_dottedname_style(self): + typ = self._makeOne() + result = typ.resolve( + 'pyramid.tests.test_path:TestDottedNameResolver') + self.assertEqual(result, self.__class__) + + def test_resolve_missing_raises(self): + typ = self._makeOne() + self.assertRaises(ImportError, typ.resolve, 'cant.be.found') + + def test_resolve_caller_package(self): + from pyramid.path import CALLER_PACKAGE + typ = self._makeOne(CALLER_PACKAGE) + self.assertEqual(typ.resolve('.test_path.TestDottedNameResolver'), + self.__class__) + + def test_maybe_resolve_caller_package(self): + from pyramid.path import CALLER_PACKAGE + typ = self._makeOne(CALLER_PACKAGE) + self.assertEqual(typ.maybe_resolve('.test_path.TestDottedNameResolver'), + self.__class__) + + def test_ctor_string_module_resolveable(self): + import pyramid.tests + typ = self._makeOne('pyramid.tests.test_path') + self.assertEqual(typ.package, pyramid.tests) + + def test_ctor_string_package_resolveable(self): + import pyramid.tests + typ = self._makeOne('pyramid.tests') + self.assertEqual(typ.package, pyramid.tests) + + def test_ctor_string_irresolveable(self): + self.assertRaises(ValueError, self._makeOne, 'cant.be.found') + + def test_ctor_module(self): + import pyramid.tests + import pyramid.tests.test_path + typ = self._makeOne(pyramid.tests.test_path) + self.assertEqual(typ.package, pyramid.tests) + + def test_ctor_package(self): + import pyramid.tests + typ = self._makeOne(pyramid.tests) + self.assertEqual(typ.package, pyramid.tests) + + def test_ctor_None(self): + typ = self._makeOne(None) + self.assertEqual(typ.package, None) + +class DummyPkgResource(object): + pass + +class DummyPackageOrModule: + def __init__(self, real_package_or_module, raise_exc=None): + self.__dict__['raise_exc'] = raise_exc + self.__dict__['__name__'] = real_package_or_module.__name__ + import os + self.__dict__['package_path'] = os.path.dirname( + os.path.abspath(real_package_or_module.__file__)) + self.__dict__['__file__'] = real_package_or_module.__file__ + + def __setattr__(self, key, val): + if self.raise_exc is not None: + raise self.raise_exc + self.__dict__[key] = val + +class DummyNamespacePackage: + """Has no __file__ attribute. + """ + + def __init__(self, real_package_or_module): + self.__name__ = real_package_or_module.__name__ + import os + self.package_path = os.path.dirname( + os.path.abspath(real_package_or_module.__file__)) diff --git a/src/pyramid/tests/test_predicates.py b/src/pyramid/tests/test_predicates.py new file mode 100644 index 000000000..da0b44708 --- /dev/null +++ b/src/pyramid/tests/test_predicates.py @@ -0,0 +1,556 @@ +import unittest + +from pyramid import testing + +from pyramid.compat import text_ + +class TestXHRPredicate(unittest.TestCase): + def _makeOne(self, val): + from pyramid.predicates import XHRPredicate + return XHRPredicate(val, None) + + def test___call___true(self): + inst = self._makeOne(True) + request = Dummy() + request.is_xhr = True + result = inst(None, request) + self.assertTrue(result) + + def test___call___false(self): + inst = self._makeOne(True) + request = Dummy() + request.is_xhr = False + result = inst(None, request) + self.assertFalse(result) + + def test_text(self): + inst = self._makeOne(True) + self.assertEqual(inst.text(), 'xhr = True') + + def test_phash(self): + inst = self._makeOne(True) + self.assertEqual(inst.phash(), 'xhr = True') + +class TestRequestMethodPredicate(unittest.TestCase): + def _makeOne(self, val): + from pyramid.predicates import RequestMethodPredicate + return RequestMethodPredicate(val, None) + + def test_ctor_get_but_no_head(self): + inst = self._makeOne('GET') + self.assertEqual(inst.val, ('GET', 'HEAD')) + + def test___call___true_single(self): + inst = self._makeOne('GET') + request = Dummy() + request.method = 'GET' + result = inst(None, request) + self.assertTrue(result) + + def test___call___true_multi(self): + inst = self._makeOne(('GET','HEAD')) + request = Dummy() + request.method = 'GET' + result = inst(None, request) + self.assertTrue(result) + + def test___call___false(self): + inst = self._makeOne(('GET','HEAD')) + request = Dummy() + request.method = 'POST' + result = inst(None, request) + self.assertFalse(result) + + def test_text(self): + inst = self._makeOne(('HEAD','GET')) + self.assertEqual(inst.text(), 'request_method = GET,HEAD') + + def test_phash(self): + inst = self._makeOne(('HEAD','GET')) + self.assertEqual(inst.phash(), 'request_method = GET,HEAD') + +class TestPathInfoPredicate(unittest.TestCase): + def _makeOne(self, val): + from pyramid.predicates import PathInfoPredicate + return PathInfoPredicate(val, None) + + def test_ctor_compilefail(self): + from pyramid.exceptions import ConfigurationError + self.assertRaises(ConfigurationError, self._makeOne, '\\') + + def test___call___true(self): + inst = self._makeOne(r'/\d{2}') + request = Dummy() + request.upath_info = text_('/12') + result = inst(None, request) + self.assertTrue(result) + + def test___call___false(self): + inst = self._makeOne(r'/\d{2}') + request = Dummy() + request.upath_info = text_('/n12') + result = inst(None, request) + self.assertFalse(result) + + def test_text(self): + inst = self._makeOne('/') + self.assertEqual(inst.text(), 'path_info = /') + + def test_phash(self): + inst = self._makeOne('/') + self.assertEqual(inst.phash(), 'path_info = /') + +class TestRequestParamPredicate(unittest.TestCase): + def _makeOne(self, val): + from pyramid.predicates import RequestParamPredicate + return RequestParamPredicate(val, None) + + def test___call___true_exists(self): + inst = self._makeOne('abc') + request = Dummy() + request.params = {'abc':1} + result = inst(None, request) + self.assertTrue(result) + + def test___call___true_withval(self): + inst = self._makeOne('abc=1') + request = Dummy() + request.params = {'abc':'1'} + result = inst(None, request) + self.assertTrue(result) + + def test___call___true_multi(self): + inst = self._makeOne(('abc', '=def =2= ')) + request = Dummy() + request.params = {'abc':'1', '=def': '2='} + result = inst(None, request) + self.assertTrue(result) + + def test___call___false_multi(self): + inst = self._makeOne(('abc=3', 'def =2 ')) + request = Dummy() + request.params = {'abc':'3', 'def': '1'} + result = inst(None, request) + self.assertFalse(result) + + def test___call___false(self): + inst = self._makeOne('abc') + request = Dummy() + request.params = {} + result = inst(None, request) + self.assertFalse(result) + + def test_text_exists(self): + inst = self._makeOne('abc') + self.assertEqual(inst.text(), 'request_param abc') + + def test_text_exists_equal_sign(self): + inst = self._makeOne('=abc') + self.assertEqual(inst.text(), 'request_param =abc') + + def test_text_withval(self): + inst = self._makeOne('abc= 1') + self.assertEqual(inst.text(), 'request_param abc=1') + + def test_text_multi(self): + inst = self._makeOne(('abc= 1', 'def')) + self.assertEqual(inst.text(), 'request_param abc=1,def') + + def test_text_multi_equal_sign(self): + inst = self._makeOne(('abc= 1', '=def= 2')) + self.assertEqual(inst.text(), 'request_param =def=2,abc=1') + + def test_phash_exists(self): + inst = self._makeOne('abc') + self.assertEqual(inst.phash(), 'request_param abc') + + def test_phash_exists_equal_sign(self): + inst = self._makeOne('=abc') + self.assertEqual(inst.phash(), 'request_param =abc') + + def test_phash_withval(self): + inst = self._makeOne('abc= 1') + self.assertEqual(inst.phash(), "request_param abc=1") + +class TestMatchParamPredicate(unittest.TestCase): + def _makeOne(self, val): + from pyramid.predicates import MatchParamPredicate + return MatchParamPredicate(val, None) + + def test___call___true_single(self): + inst = self._makeOne('abc=1') + request = Dummy() + request.matchdict = {'abc':'1'} + result = inst(None, request) + self.assertTrue(result) + + + def test___call___true_multi(self): + inst = self._makeOne(('abc=1', 'def=2')) + request = Dummy() + request.matchdict = {'abc':'1', 'def':'2'} + result = inst(None, request) + self.assertTrue(result) + + def test___call___false(self): + inst = self._makeOne('abc=1') + request = Dummy() + request.matchdict = {} + result = inst(None, request) + self.assertFalse(result) + + def test___call___matchdict_is_None(self): + inst = self._makeOne('abc=1') + request = Dummy() + request.matchdict = None + result = inst(None, request) + self.assertFalse(result) + + def test_text(self): + inst = self._makeOne(('def= 1', 'abc =2')) + self.assertEqual(inst.text(), 'match_param abc=2,def=1') + + def test_phash(self): + inst = self._makeOne(('def= 1', 'abc =2')) + self.assertEqual(inst.phash(), 'match_param abc=2,def=1') + +class TestCustomPredicate(unittest.TestCase): + def _makeOne(self, val): + from pyramid.predicates import CustomPredicate + return CustomPredicate(val, None) + + def test___call___true(self): + def func(context, request): + self.assertEqual(context, None) + self.assertEqual(request, None) + return True + inst = self._makeOne(func) + result = inst(None, None) + self.assertTrue(result) + + def test___call___false(self): + def func(context, request): + self.assertEqual(context, None) + self.assertEqual(request, None) + return False + inst = self._makeOne(func) + result = inst(None, None) + self.assertFalse(result) + + def test_text_func_has___text__(self): + pred = predicate() + pred.__text__ = 'text' + inst = self._makeOne(pred) + self.assertEqual(inst.text(), 'text') + + def test_text_func_repr(self): + pred = predicate() + inst = self._makeOne(pred) + self.assertEqual(inst.text(), 'custom predicate: object predicate') + + def test_phash(self): + pred = predicate() + inst = self._makeOne(pred) + self.assertEqual(inst.phash(), 'custom:1') + +class TestTraversePredicate(unittest.TestCase): + def _makeOne(self, val): + from pyramid.predicates import TraversePredicate + return TraversePredicate(val, None) + + def test___call__traverse_has_remainder_already(self): + inst = self._makeOne('/1/:a/:b') + info = {'traverse':'abc'} + request = Dummy() + result = inst(info, request) + self.assertEqual(result, True) + self.assertEqual(info, {'traverse':'abc'}) + + def test___call__traverse_matches(self): + inst = self._makeOne('/1/:a/:b') + info = {'match':{'a':'a', 'b':'b'}} + request = Dummy() + result = inst(info, request) + self.assertEqual(result, True) + self.assertEqual(info, {'match': + {'a':'a', 'b':'b', 'traverse':('1', 'a', 'b')}}) + + def test___call__traverse_matches_with_highorder_chars(self): + inst = self._makeOne(text_(b'/La Pe\xc3\xb1a/{x}', 'utf-8')) + info = {'match':{'x':text_(b'Qu\xc3\xa9bec', 'utf-8')}} + request = Dummy() + result = inst(info, request) + self.assertEqual(result, True) + self.assertEqual( + info['match']['traverse'], + (text_(b'La Pe\xc3\xb1a', 'utf-8'), + text_(b'Qu\xc3\xa9bec', 'utf-8')) + ) + + def test_text(self): + inst = self._makeOne('/abc') + self.assertEqual(inst.text(), 'traverse matchdict pseudo-predicate') + + def test_phash(self): + inst = self._makeOne('/abc') + self.assertEqual(inst.phash(), '') + +class Test_CheckCSRFTokenPredicate(unittest.TestCase): + def _makeOne(self, val, config): + from pyramid.predicates import CheckCSRFTokenPredicate + return CheckCSRFTokenPredicate(val, config) + + def test_text(self): + inst = self._makeOne(True, None) + self.assertEqual(inst.text(), 'check_csrf = True') + + def test_phash(self): + inst = self._makeOne(True, None) + self.assertEqual(inst.phash(), 'check_csrf = True') + + def test_it_call_val_True(self): + inst = self._makeOne(True, None) + request = Dummy() + def check_csrf_token(req, val, raises=True): + self.assertEqual(req, request) + self.assertEqual(val, 'csrf_token') + self.assertEqual(raises, False) + return True + inst.check_csrf_token = check_csrf_token + result = inst(None, request) + self.assertEqual(result, True) + + def test_it_call_val_str(self): + inst = self._makeOne('abc', None) + request = Dummy() + def check_csrf_token(req, val, raises=True): + self.assertEqual(req, request) + self.assertEqual(val, 'abc') + self.assertEqual(raises, False) + return True + inst.check_csrf_token = check_csrf_token + result = inst(None, request) + self.assertEqual(result, True) + + def test_it_call_val_False(self): + inst = self._makeOne(False, None) + request = Dummy() + result = inst(None, request) + self.assertEqual(result, True) + +class TestHeaderPredicate(unittest.TestCase): + def _makeOne(self, val): + from pyramid.predicates import HeaderPredicate + return HeaderPredicate(val, None) + + def test___call___true_exists(self): + inst = self._makeOne('abc') + request = Dummy() + request.headers = {'abc':1} + result = inst(None, request) + self.assertTrue(result) + + def test___call___true_withval(self): + inst = self._makeOne('abc:1') + request = Dummy() + request.headers = {'abc':'1'} + result = inst(None, request) + self.assertTrue(result) + + def test___call___true_withregex(self): + inst = self._makeOne(r'abc:\d+') + request = Dummy() + request.headers = {'abc':'1'} + result = inst(None, request) + self.assertTrue(result) + + def test___call___false_withregex(self): + inst = self._makeOne(r'abc:\d+') + request = Dummy() + request.headers = {'abc':'a'} + result = inst(None, request) + self.assertFalse(result) + + def test___call___false(self): + inst = self._makeOne('abc') + request = Dummy() + request.headers = {} + result = inst(None, request) + self.assertFalse(result) + + def test_text_exists(self): + inst = self._makeOne('abc') + self.assertEqual(inst.text(), 'header abc') + + def test_text_withval(self): + inst = self._makeOne('abc:1') + self.assertEqual(inst.text(), 'header abc=1') + + def test_text_withregex(self): + inst = self._makeOne(r'abc:\d+') + self.assertEqual(inst.text(), r'header abc=\d+') + + def test_phash_exists(self): + inst = self._makeOne('abc') + self.assertEqual(inst.phash(), 'header abc') + + def test_phash_withval(self): + inst = self._makeOne('abc:1') + self.assertEqual(inst.phash(), "header abc=1") + + def test_phash_withregex(self): + inst = self._makeOne(r'abc:\d+') + self.assertEqual(inst.phash(), r'header abc=\d+') + +class Test_PhysicalPathPredicate(unittest.TestCase): + def _makeOne(self, val, config): + from pyramid.predicates import PhysicalPathPredicate + return PhysicalPathPredicate(val, config) + + def test_text(self): + inst = self._makeOne('/', None) + self.assertEqual(inst.text(), "physical_path = ('',)") + + def test_phash(self): + inst = self._makeOne('/', None) + self.assertEqual(inst.phash(), "physical_path = ('',)") + + def test_it_call_val_tuple_True(self): + inst = self._makeOne(('', 'abc'), None) + root = Dummy() + root.__name__ = '' + root.__parent__ = None + context = Dummy() + context.__name__ = 'abc' + context.__parent__ = root + self.assertTrue(inst(context, None)) + + def test_it_call_val_list_True(self): + inst = self._makeOne(['', 'abc'], None) + root = Dummy() + root.__name__ = '' + root.__parent__ = None + context = Dummy() + context.__name__ = 'abc' + context.__parent__ = root + self.assertTrue(inst(context, None)) + + def test_it_call_val_str_True(self): + inst = self._makeOne('/abc', None) + root = Dummy() + root.__name__ = '' + root.__parent__ = None + context = Dummy() + context.__name__ = 'abc' + context.__parent__ = root + self.assertTrue(inst(context, None)) + + def test_it_call_False(self): + inst = self._makeOne('/', None) + root = Dummy() + root.__name__ = '' + root.__parent__ = None + context = Dummy() + context.__name__ = 'abc' + context.__parent__ = root + self.assertFalse(inst(context, None)) + + def test_it_call_context_has_no_name(self): + inst = self._makeOne('/', None) + context = Dummy() + self.assertFalse(inst(context, None)) + +class Test_EffectivePrincipalsPredicate(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def _makeOne(self, val, config): + from pyramid.predicates import EffectivePrincipalsPredicate + return EffectivePrincipalsPredicate(val, config) + + def test_text(self): + inst = self._makeOne(('verna', 'fred'), None) + self.assertEqual(inst.text(), + "effective_principals = ['fred', 'verna']") + + def test_text_noniter(self): + inst = self._makeOne('verna', None) + self.assertEqual(inst.text(), + "effective_principals = ['verna']") + + def test_phash(self): + inst = self._makeOne(('verna', 'fred'), None) + self.assertEqual(inst.phash(), + "effective_principals = ['fred', 'verna']") + + def test_it_call_no_authentication_policy(self): + request = testing.DummyRequest() + inst = self._makeOne(('verna', 'fred'), None) + context = Dummy() + self.assertFalse(inst(context, request)) + + def test_it_call_authentication_policy_provides_superset(self): + request = testing.DummyRequest() + self.config.testing_securitypolicy('fred', groupids=('verna', 'bambi')) + inst = self._makeOne(('verna', 'fred'), None) + context = Dummy() + self.assertTrue(inst(context, request)) + + def test_it_call_authentication_policy_provides_superset_implicit(self): + from pyramid.security import Authenticated + request = testing.DummyRequest() + self.config.testing_securitypolicy('fred', groupids=('verna', 'bambi')) + inst = self._makeOne(Authenticated, None) + context = Dummy() + self.assertTrue(inst(context, request)) + + def test_it_call_authentication_policy_doesnt_provide_superset(self): + request = testing.DummyRequest() + self.config.testing_securitypolicy('fred') + inst = self._makeOne(('verna', 'fred'), None) + context = Dummy() + self.assertFalse(inst(context, request)) + + +class TestNotted(unittest.TestCase): + def _makeOne(self, predicate): + from pyramid.predicates import Notted + return Notted(predicate) + + def test_it_with_phash_val(self): + pred = DummyPredicate('val') + inst = self._makeOne(pred) + self.assertEqual(inst.text(), '!val') + self.assertEqual(inst.phash(), '!val') + self.assertEqual(inst(None, None), False) + + def test_it_without_phash_val(self): + pred = DummyPredicate('') + inst = self._makeOne(pred) + self.assertEqual(inst.text(), '') + self.assertEqual(inst.phash(), '') + self.assertEqual(inst(None, None), True) + +class predicate(object): + def __repr__(self): + return 'predicate' + def __hash__(self): + return 1 + +class Dummy(object): + pass + +class DummyPredicate(object): + def __init__(self, result): + self.result = result + + def text(self): + return self.result + + phash = text + + def __call__(self, context, request): + return True diff --git a/src/pyramid/tests/test_registry.py b/src/pyramid/tests/test_registry.py new file mode 100644 index 000000000..aa44b5408 --- /dev/null +++ b/src/pyramid/tests/test_registry.py @@ -0,0 +1,401 @@ +import unittest + +class TestRegistry(unittest.TestCase): + def _getTargetClass(self): + from pyramid.registry import Registry + return Registry + + def _makeOne(self, *args, **kw): + return self._getTargetClass()(*args, **kw) + + def test___nonzero__(self): + registry = self._makeOne() + self.assertEqual(registry.__nonzero__(), True) + + def test__lock(self): + registry = self._makeOne() + self.assertTrue(registry._lock) + + def test_clear_view_cache_lookup(self): + registry = self._makeOne() + registry._view_lookup_cache[1] = 2 + registry._clear_view_lookup_cache() + self.assertEqual(registry._view_lookup_cache, {}) + + def test_package_name(self): + package_name = 'testing' + registry = self._makeOne(package_name) + self.assertEqual(registry.package_name, package_name) + + def test_default_package_name(self): + registry = self._makeOne() + self.assertEqual(registry.package_name, 'pyramid.tests') + + def test_registerHandler_and_notify(self): + registry = self._makeOne() + self.assertEqual(registry.has_listeners, False) + L = [] + def f(event): + L.append(event) + registry.registerHandler(f, [IDummyEvent]) + self.assertEqual(registry.has_listeners, True) + event = DummyEvent() + registry.notify(event) + self.assertEqual(L, [event]) + + def test_registerSubscriptionAdapter(self): + registry = self._makeOne() + self.assertEqual(registry.has_listeners, False) + from zope.interface import Interface + registry.registerSubscriptionAdapter(DummyEvent, + [IDummyEvent], Interface) + self.assertEqual(registry.has_listeners, True) + + def test__get_settings(self): + registry = self._makeOne() + registry._settings = 'foo' + self.assertEqual(registry.settings, 'foo') + + def test__set_settings(self): + registry = self._makeOne() + registry.settings = 'foo' + self.assertEqual(registry._settings, 'foo') + + def test_init_forwards_args(self): + from zope.interface import Interface + from zope.interface.registry import Components + dummy = object() + c = Components() + c.registerUtility(dummy, Interface) + registry = self._makeOne('foo', (c,)) + self.assertEqual(registry.__name__, 'foo') + self.assertEqual(registry.getUtility(Interface), dummy) + + def test_init_forwards_kw(self): + from zope.interface import Interface + from zope.interface.registry import Components + dummy = object() + c = Components() + c.registerUtility(dummy, Interface) + registry = self._makeOne(bases=(c,)) + self.assertEqual(registry.getUtility(Interface), dummy) + +class TestIntrospector(unittest.TestCase): + def _getTargetClass(slf): + from pyramid.registry import Introspector + return Introspector + + def _makeOne(self): + return self._getTargetClass()() + + def test_conformance(self): + from zope.interface.verify import verifyClass + from zope.interface.verify import verifyObject + from pyramid.interfaces import IIntrospector + verifyClass(IIntrospector, self._getTargetClass()) + verifyObject(IIntrospector, self._makeOne()) + + def test_add(self): + inst = self._makeOne() + intr = DummyIntrospectable() + inst.add(intr) + self.assertEqual(intr.order, 0) + category = {'discriminator':intr, 'discriminator_hash':intr} + self.assertEqual(inst._categories, {'category':category}) + + def test_get_success(self): + inst = self._makeOne() + intr = DummyIntrospectable() + inst.add(intr) + self.assertEqual(inst.get('category', 'discriminator'), intr) + + def test_get_success_byhash(self): + inst = self._makeOne() + intr = DummyIntrospectable() + inst.add(intr) + self.assertEqual(inst.get('category', 'discriminator_hash'), intr) + + def test_get_fail(self): + inst = self._makeOne() + intr = DummyIntrospectable() + inst.add(intr) + self.assertEqual(inst.get('category', 'wontexist', 'foo'), 'foo') + + def test_get_category(self): + inst = self._makeOne() + intr = DummyIntrospectable() + intr2 = DummyIntrospectable() + intr2.discriminator = 'discriminator2' + intr2.discriminator_hash = 'discriminator2_hash' + inst.add(intr2) + inst.add(intr) + expected = [ + {'introspectable':intr2, 'related':[]}, + {'introspectable':intr, 'related':[]}, + ] + self.assertEqual(inst.get_category('category'), expected) + + def test_get_category_returns_default_on_miss(self): + inst = self._makeOne() + self.assertEqual(inst.get_category('category', '123'), '123') + + def test_get_category_with_sortkey(self): + import operator + inst = self._makeOne() + intr = DummyIntrospectable() + intr.foo = 2 + intr2 = DummyIntrospectable() + intr2.discriminator = 'discriminator2' + intr2.discriminator_hash = 'discriminator2_hash' + intr2.foo = 1 + inst.add(intr) + inst.add(intr2) + expected = [ + {'introspectable':intr2, 'related':[]}, + {'introspectable':intr, 'related':[]}, + ] + self.assertEqual( + inst.get_category('category', sort_key=operator.attrgetter('foo')), + expected) + + def test_categorized(self): + import operator + inst = self._makeOne() + intr = DummyIntrospectable() + intr.foo = 2 + intr2 = DummyIntrospectable() + intr2.discriminator = 'discriminator2' + intr2.discriminator_hash = 'discriminator2_hash' + intr2.foo = 1 + inst.add(intr) + inst.add(intr2) + expected = [('category', [ + {'introspectable':intr2, 'related':[]}, + {'introspectable':intr, 'related':[]}, + ])] + self.assertEqual( + inst.categorized(sort_key=operator.attrgetter('foo')), expected) + + def test_categories(self): + inst = self._makeOne() + inst._categories['a'] = 1 + inst._categories['b'] = 2 + self.assertEqual(list(inst.categories()), ['a', 'b']) + + def test_remove(self): + inst = self._makeOne() + intr = DummyIntrospectable() + intr2 = DummyIntrospectable() + intr2.category_name = 'category2' + intr2.discriminator = 'discriminator2' + intr2.discriminator_hash = 'discriminator2_hash' + inst.add(intr) + inst.add(intr2) + inst.relate(('category', 'discriminator'), + ('category2', 'discriminator2')) + inst.remove('category', 'discriminator') + self.assertEqual(inst._categories, + {'category': + {}, + 'category2': + {'discriminator2': intr2, + 'discriminator2_hash': intr2} + }) + self.assertEqual(inst._refs.get(intr), None) + self.assertEqual(inst._refs[intr2], []) + + def test_remove_fail(self): + inst = self._makeOne() + self.assertEqual(inst.remove('a', 'b'), None) + + def test_relate(self): + inst = self._makeOne() + intr = DummyIntrospectable() + intr2 = DummyIntrospectable() + intr2.category_name = 'category2' + intr2.discriminator = 'discriminator2' + intr2.discriminator_hash = 'discriminator2_hash' + inst.add(intr) + inst.add(intr2) + inst.relate(('category', 'discriminator'), + ('category2', 'discriminator2')) + self.assertEqual(inst._categories, + {'category': + {'discriminator':intr, + 'discriminator_hash':intr}, + 'category2': + {'discriminator2': intr2, + 'discriminator2_hash': intr2} + }) + self.assertEqual(inst._refs[intr], [intr2]) + self.assertEqual(inst._refs[intr2], [intr]) + + def test_relate_fail(self): + inst = self._makeOne() + intr = DummyIntrospectable() + inst.add(intr) + self.assertRaises( + KeyError, + inst.relate, + ('category', 'discriminator'), + ('category2', 'discriminator2') + ) + + def test_unrelate(self): + inst = self._makeOne() + intr = DummyIntrospectable() + intr2 = DummyIntrospectable() + intr2.category_name = 'category2' + intr2.discriminator = 'discriminator2' + intr2.discriminator_hash = 'discriminator2_hash' + inst.add(intr) + inst.add(intr2) + inst.relate(('category', 'discriminator'), + ('category2', 'discriminator2')) + inst.unrelate(('category', 'discriminator'), + ('category2', 'discriminator2')) + self.assertEqual(inst._categories, + {'category': + {'discriminator':intr, + 'discriminator_hash':intr}, + 'category2': + {'discriminator2': intr2, + 'discriminator2_hash': intr2} + }) + self.assertEqual(inst._refs[intr], []) + self.assertEqual(inst._refs[intr2], []) + + def test_related(self): + inst = self._makeOne() + intr = DummyIntrospectable() + intr2 = DummyIntrospectable() + intr2.category_name = 'category2' + intr2.discriminator = 'discriminator2' + intr2.discriminator_hash = 'discriminator2_hash' + inst.add(intr) + inst.add(intr2) + inst.relate(('category', 'discriminator'), + ('category2', 'discriminator2')) + self.assertEqual(inst.related(intr), [intr2]) + + def test_related_fail(self): + inst = self._makeOne() + intr = DummyIntrospectable() + intr2 = DummyIntrospectable() + intr2.category_name = 'category2' + intr2.discriminator = 'discriminator2' + intr2.discriminator_hash = 'discriminator2_hash' + inst.add(intr) + inst.add(intr2) + inst.relate(('category', 'discriminator'), + ('category2', 'discriminator2')) + del inst._categories['category'] + self.assertRaises(KeyError, inst.related, intr) + +class TestIntrospectable(unittest.TestCase): + def _getTargetClass(slf): + from pyramid.registry import Introspectable + return Introspectable + + def _makeOne(self, *arg, **kw): + return self._getTargetClass()(*arg, **kw) + + def _makeOnePopulated(self): + return self._makeOne('category', 'discrim', 'title', 'type') + + def test_conformance(self): + from zope.interface.verify import verifyClass + from zope.interface.verify import verifyObject + from pyramid.interfaces import IIntrospectable + verifyClass(IIntrospectable, self._getTargetClass()) + verifyObject(IIntrospectable, self._makeOnePopulated()) + + def test_relate(self): + inst = self._makeOnePopulated() + inst.relate('a', 'b') + self.assertEqual(inst._relations, [(True, 'a', 'b')]) + + def test_unrelate(self): + inst = self._makeOnePopulated() + inst.unrelate('a', 'b') + self.assertEqual(inst._relations, [(False, 'a', 'b')]) + + def test_discriminator_hash(self): + inst = self._makeOnePopulated() + self.assertEqual(inst.discriminator_hash, hash(inst.discriminator)) + + def test___hash__(self): + inst = self._makeOnePopulated() + self.assertEqual(hash(inst), + hash((inst.category_name,) + (inst.discriminator,))) + + def test___repr__(self): + inst = self._makeOnePopulated() + self.assertEqual( + repr(inst), + "") + + def test___nonzero__(self): + inst = self._makeOnePopulated() + self.assertEqual(inst.__nonzero__(), True) + + def test___bool__(self): + inst = self._makeOnePopulated() + self.assertEqual(inst.__bool__(), True) + + def test_register(self): + introspector = DummyIntrospector() + action_info = object() + inst = self._makeOnePopulated() + inst._relations.append((True, 'category1', 'discrim1')) + inst._relations.append((False, 'category2', 'discrim2')) + inst.register(introspector, action_info) + self.assertEqual(inst.action_info, action_info) + self.assertEqual(introspector.intrs, [inst]) + self.assertEqual(introspector.relations, + [(('category', 'discrim'), ('category1', 'discrim1'))]) + self.assertEqual(introspector.unrelations, + [(('category', 'discrim'), ('category2', 'discrim2'))]) + +class DummyIntrospector(object): + def __init__(self): + self.intrs = [] + self.relations = [] + self.unrelations = [] + + def add(self, intr): + self.intrs.append(intr) + + def relate(self, *pairs): + self.relations.append(pairs) + + def unrelate(self, *pairs): + self.unrelations.append(pairs) + +class DummyModule: + __path__ = "foo" + __name__ = "dummy" + __file__ = '' + +class DummyIntrospectable(object): + category_name = 'category' + discriminator = 'discriminator' + title = 'title' + type_name = 'type' + order = None + action_info = None + discriminator_hash = 'discriminator_hash' + + def __hash__(self): + return hash((self.category_name,) + (self.discriminator,)) + + +from zope.interface import Interface +from zope.interface import implementer +class IDummyEvent(Interface): + pass + +@implementer(IDummyEvent) +class DummyEvent(object): + pass + diff --git a/src/pyramid/tests/test_renderers.py b/src/pyramid/tests/test_renderers.py new file mode 100644 index 000000000..a2f7bf8c2 --- /dev/null +++ b/src/pyramid/tests/test_renderers.py @@ -0,0 +1,705 @@ +import unittest + +from pyramid.testing import cleanUp +from pyramid import testing +from pyramid.compat import text_ + +class TestJSON(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def _makeOne(self, **kw): + from pyramid.renderers import JSON + return JSON(**kw) + + def test_it(self): + renderer = self._makeOne()(None) + result = renderer({'a':1}, {}) + self.assertEqual(result, '{"a": 1}') + + def test_with_request_content_type_notset(self): + request = testing.DummyRequest() + renderer = self._makeOne()(None) + renderer({'a':1}, {'request':request}) + self.assertEqual(request.response.content_type, 'application/json') + + def test_with_request_content_type_set(self): + request = testing.DummyRequest() + request.response.content_type = 'text/mishmash' + renderer = self._makeOne()(None) + renderer({'a':1}, {'request':request}) + self.assertEqual(request.response.content_type, 'text/mishmash') + + def test_with_custom_adapter(self): + request = testing.DummyRequest() + from datetime import datetime + def adapter(obj, req): + self.assertEqual(req, request) + return obj.isoformat() + now = datetime.utcnow() + renderer = self._makeOne() + renderer.add_adapter(datetime, adapter) + result = renderer(None)({'a':now}, {'request':request}) + self.assertEqual(result, '{"a": "%s"}' % now.isoformat()) + + def test_with_custom_adapter2(self): + request = testing.DummyRequest() + from datetime import datetime + def adapter(obj, req): + self.assertEqual(req, request) + return obj.isoformat() + now = datetime.utcnow() + renderer = self._makeOne(adapters=((datetime, adapter),)) + result = renderer(None)({'a':now}, {'request':request}) + self.assertEqual(result, '{"a": "%s"}' % now.isoformat()) + + def test_with_custom_serializer(self): + class Serializer(object): + def __call__(self, obj, **kw): + self.obj = obj + self.kw = kw + return 'foo' + serializer = Serializer() + renderer = self._makeOne(serializer=serializer, baz=5) + obj = {'a':'b'} + result = renderer(None)(obj, {}) + self.assertEqual(result, 'foo') + self.assertEqual(serializer.obj, obj) + self.assertEqual(serializer.kw['baz'], 5) + self.assertTrue('default' in serializer.kw) + + def test_with_object_adapter(self): + request = testing.DummyRequest() + outerself = self + class MyObject(object): + def __init__(self, x): + self.x = x + def __json__(self, req): + outerself.assertEqual(req, request) + return {'x': self.x} + + objects = [MyObject(1), MyObject(2)] + renderer = self._makeOne()(None) + result = renderer(objects, {'request':request}) + self.assertEqual(result, '[{"x": 1}, {"x": 2}]') + + def test_with_object_adapter_no___json__(self): + class MyObject(object): + def __init__(self, x): + self.x = x + objects = [MyObject(1), MyObject(2)] + renderer = self._makeOne()(None) + self.assertRaises(TypeError, renderer, objects, {}) + +class Test_string_renderer_factory(unittest.TestCase): + def _callFUT(self, name): + from pyramid.renderers import string_renderer_factory + return string_renderer_factory(name) + + def test_it_unicode(self): + renderer = self._callFUT(None) + value = text_('La Pe\xc3\xb1a', 'utf-8') + result = renderer(value, {}) + self.assertEqual(result, value) + + def test_it_str(self): + renderer = self._callFUT(None) + value = 'La Pe\xc3\xb1a' + result = renderer(value, {}) + self.assertEqual(result, value) + + def test_it_other(self): + renderer = self._callFUT(None) + value = None + result = renderer(value, {}) + self.assertEqual(result, 'None') + + def test_with_request_content_type_notset(self): + request = testing.DummyRequest() + renderer = self._callFUT(None) + renderer('', {'request':request}) + self.assertEqual(request.response.content_type, 'text/plain') + + def test_with_request_content_type_set(self): + request = testing.DummyRequest() + request.response.content_type = 'text/mishmash' + renderer = self._callFUT(None) + renderer('', {'request':request}) + self.assertEqual(request.response.content_type, 'text/mishmash') + + +class TestRendererHelper(unittest.TestCase): + def setUp(self): + self.config = cleanUp() + + def tearDown(self): + cleanUp() + + def _makeOne(self, *arg, **kw): + from pyramid.renderers import RendererHelper + return RendererHelper(*arg, **kw) + + def test_instance_conforms(self): + from zope.interface.verify import verifyObject + from pyramid.interfaces import IRendererInfo + helper = self._makeOne() + verifyObject(IRendererInfo, helper) + + def test_settings_registry_settings_is_None(self): + class Dummy(object): + settings = None + helper = self._makeOne(registry=Dummy) + self.assertEqual(helper.settings, {}) + + def test_settings_registry_name_is_None(self): + class Dummy(object): + settings = None + helper = self._makeOne(registry=Dummy) + self.assertEqual(helper.name, None) + self.assertEqual(helper.type, '') + + def test_settings_registry_settings_is_not_None(self): + class Dummy(object): + settings = {'a':1} + helper = self._makeOne(registry=Dummy) + self.assertEqual(helper.settings, {'a':1}) + + def _registerRendererFactory(self): + from pyramid.interfaces import IRendererFactory + def renderer(*arg): + def respond(*arg): + return arg + renderer.respond = respond + return respond + self.config.registry.registerUtility(renderer, IRendererFactory, + name='.foo') + return renderer + + def _registerResponseFactory(self): + from pyramid.interfaces import IResponseFactory + class ResponseFactory(object): + pass + + self.config.registry.registerUtility( + lambda r: ResponseFactory(), IResponseFactory + ) + + def test_render_to_response(self): + self._registerRendererFactory() + self._registerResponseFactory() + request = Dummy() + helper = self._makeOne('loo.foo') + response = helper.render_to_response('values', {}, + request=request) + self.assertEqual(response.app_iter[0], 'values') + self.assertEqual(response.app_iter[1], {}) + + def test_get_renderer(self): + factory = self._registerRendererFactory() + helper = self._makeOne('loo.foo') + self.assertEqual(helper.get_renderer(), factory.respond) + + def test_render_view(self): + import pyramid.csrf + self._registerRendererFactory() + self._registerResponseFactory() + request = Dummy() + helper = self._makeOne('loo.foo') + view = 'view' + context = 'context' + request = testing.DummyRequest() + response = 'response' + response = helper.render_view(request, response, view, context) + get_csrf = response.app_iter[1].pop('get_csrf_token') + self.assertEqual(get_csrf.args, (request, )) + self.assertEqual(get_csrf.func, pyramid.csrf.get_csrf_token) + self.assertEqual(response.app_iter[0], 'response') + self.assertEqual(response.app_iter[1], + {'renderer_info': helper, + 'renderer_name': 'loo.foo', + 'request': request, + 'context': 'context', + 'view': 'view', + 'req': request,} + ) + + def test_render_explicit_registry(self): + factory = self._registerRendererFactory() + class DummyRegistry(object): + def __init__(self): + self.responses = [factory, lambda *arg: {}, None] + def queryUtility(self, iface, name=None): + self.queried = True + return self.responses.pop(0) + def notify(self, event): + self.event = event + reg = DummyRegistry() + helper = self._makeOne('loo.foo', registry=reg) + result = helper.render('value', {}) + self.assertEqual(result[0], 'value') + self.assertEqual(result[1], {}) + self.assertTrue(reg.queried) + self.assertEqual(reg.event, {}) + self.assertEqual(reg.event.__class__.__name__, 'BeforeRender') + + def test_render_system_values_is_None(self): + import pyramid.csrf + self._registerRendererFactory() + request = Dummy() + context = Dummy() + request.context = context + helper = self._makeOne('loo.foo') + result = helper.render('values', None, request=request) + get_csrf = result[1].pop('get_csrf_token') + self.assertEqual(get_csrf.args, (request, )) + self.assertEqual(get_csrf.func, pyramid.csrf.get_csrf_token) + system = {'request':request, + 'context':context, + 'renderer_name':'loo.foo', + 'view':None, + 'renderer_info':helper, + 'req':request, + } + self.assertEqual(result[0], 'values') + self.assertEqual(result[1], system) + + def test__make_response_request_is_None(self): + request = None + helper = self._makeOne('loo.foo') + response = helper._make_response('abc', request) + self.assertEqual(response.body, b'abc') + + def test__make_response_request_is_None_response_factory_exists(self): + self._registerResponseFactory() + request = None + helper = self._makeOne('loo.foo') + response = helper._make_response(b'abc', request) + self.assertEqual(response.__class__.__name__, 'ResponseFactory') + self.assertEqual(response.body, b'abc') + + def test__make_response_result_is_unicode(self): + from pyramid.response import Response + request = testing.DummyRequest() + request.response = Response() + helper = self._makeOne('loo.foo') + la = text_('/La Pe\xc3\xb1a', 'utf-8') + response = helper._make_response(la, request) + self.assertEqual(response.body, la.encode('utf-8')) + + def test__make_response_result_is_str(self): + from pyramid.response import Response + request = testing.DummyRequest() + request.response = Response() + helper = self._makeOne('loo.foo') + la = text_('/La Pe\xc3\xb1a', 'utf-8') + response = helper._make_response(la.encode('utf-8'), request) + self.assertEqual(response.body, la.encode('utf-8')) + + def test__make_response_result_is_iterable(self): + from pyramid.response import Response + request = testing.DummyRequest() + request.response = Response() + helper = self._makeOne('loo.foo') + la = text_('/La Pe\xc3\xb1a', 'utf-8') + response = helper._make_response([la.encode('utf-8')], request) + self.assertEqual(response.body, la.encode('utf-8')) + + def test__make_response_result_is_other(self): + self._registerResponseFactory() + request = None + helper = self._makeOne('loo.foo') + result = object() + response = helper._make_response(result, request) + self.assertEqual(response.body, result) + + def test__make_response_result_is_None_no_body(self): + from pyramid.response import Response + request = testing.DummyRequest() + request.response = Response() + helper = self._makeOne('loo.foo') + response = helper._make_response(None, request) + self.assertEqual(response.body, b'') + + def test__make_response_result_is_None_existing_body_not_molested(self): + from pyramid.response import Response + request = testing.DummyRequest() + response = Response() + response.body = b'abc' + request.response = response + helper = self._makeOne('loo.foo') + response = helper._make_response(None, request) + self.assertEqual(response.body, b'abc') + + def test_with_alternate_response_factory(self): + from pyramid.interfaces import IResponseFactory + class ResponseFactory(object): + def __init__(self): + pass + self.config.registry.registerUtility( + lambda r: ResponseFactory(), IResponseFactory + ) + request = testing.DummyRequest() + helper = self._makeOne('loo.foo') + response = helper._make_response(b'abc', request) + self.assertEqual(response.__class__, ResponseFactory) + self.assertEqual(response.body, b'abc') + + def test__make_response_with_real_request(self): + # functional + from pyramid.request import Request + request = Request({}) + request.registry = self.config.registry + request.response.status = '406 You Lose' + helper = self._makeOne('loo.foo') + response = helper._make_response('abc', request) + self.assertEqual(response.status, '406 You Lose') + self.assertEqual(response.body, b'abc') + + def test_clone_noargs(self): + helper = self._makeOne('name', 'package', 'registry') + cloned_helper = helper.clone() + self.assertEqual(cloned_helper.name, 'name') + self.assertEqual(cloned_helper.package, 'package') + self.assertEqual(cloned_helper.registry, 'registry') + self.assertFalse(helper is cloned_helper) + + def test_clone_allargs(self): + helper = self._makeOne('name', 'package', 'registry') + cloned_helper = helper.clone(name='name2', package='package2', + registry='registry2') + self.assertEqual(cloned_helper.name, 'name2') + self.assertEqual(cloned_helper.package, 'package2') + self.assertEqual(cloned_helper.registry, 'registry2') + self.assertFalse(helper is cloned_helper) + + def test_renderer_absolute_file(self): + registry = self.config.registry + settings = {} + registry.settings = settings + from pyramid.interfaces import IRendererFactory + import os + here = os.path.dirname(os.path.abspath(__file__)) + fixture = os.path.join(here, 'fixtures/minimal.pt') + def factory(info, **kw): + return info + self.config.registry.registerUtility( + factory, IRendererFactory, name='.pt') + result = self._makeOne(fixture).renderer + self.assertEqual(result.registry, registry) + self.assertEqual(result.type, '.pt') + self.assertEqual(result.package, None) + self.assertEqual(result.name, fixture) + self.assertEqual(result.settings, settings) + + def test_renderer_with_package(self): + import pyramid + registry = self.config.registry + settings = {} + registry.settings = settings + from pyramid.interfaces import IRendererFactory + import os + here = os.path.dirname(os.path.abspath(__file__)) + fixture = os.path.join(here, 'fixtures/minimal.pt') + def factory(info, **kw): + return info + self.config.registry.registerUtility( + factory, IRendererFactory, name='.pt') + result = self._makeOne(fixture, pyramid).renderer + self.assertEqual(result.registry, registry) + self.assertEqual(result.type, '.pt') + self.assertEqual(result.package, pyramid) + self.assertEqual(result.name, fixture) + self.assertEqual(result.settings, settings) + + def test_renderer_missing(self): + inst = self._makeOne('foo') + self.assertRaises(ValueError, getattr, inst, 'renderer') + +class TestNullRendererHelper(unittest.TestCase): + def setUp(self): + self.config = cleanUp() + + def tearDown(self): + cleanUp() + + def _makeOne(self, *arg, **kw): + from pyramid.renderers import NullRendererHelper + return NullRendererHelper(*arg, **kw) + + def test_instance_conforms(self): + from zope.interface.verify import verifyObject + from pyramid.interfaces import IRendererInfo + helper = self._makeOne() + verifyObject(IRendererInfo, helper) + + def test_render_view(self): + helper = self._makeOne() + self.assertEqual(helper.render_view(None, True, None, None), True) + + def test_render(self): + helper = self._makeOne() + self.assertEqual(helper.render(True, None, None), True) + + def test_render_to_response(self): + helper = self._makeOne() + self.assertEqual(helper.render_to_response(True, None, None), True) + + def test_clone(self): + helper = self._makeOne() + self.assertTrue(helper.clone() is helper) + +class Test_render(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def _callFUT(self, renderer_name, value, request=None, package=None): + from pyramid.renderers import render + return render(renderer_name, value, request=request, package=package) + + def _registerRenderer(self): + renderer = self.config.testing_add_renderer( + 'pyramid.tests:abc/def.pt') + renderer.string_response = 'abc' + return renderer + + def test_it_no_request(self): + renderer = self._registerRenderer() + result = self._callFUT('abc/def.pt', dict(a=1)) + self.assertEqual(result, 'abc') + renderer.assert_(a=1) + renderer.assert_(request=None) + + def test_it_with_request(self): + renderer = self._registerRenderer() + request = testing.DummyRequest() + result = self._callFUT('abc/def.pt', + dict(a=1), request=request) + self.assertEqual(result, 'abc') + renderer.assert_(a=1) + renderer.assert_(request=request) + + def test_it_with_package(self): + import pyramid.tests + renderer = self._registerRenderer() + request = testing.DummyRequest() + result = self._callFUT('abc/def.pt', dict(a=1), request=request, + package=pyramid.tests) + self.assertEqual(result, 'abc') + renderer.assert_(a=1) + renderer.assert_(request=request) + + def test_response_preserved(self): + request = testing.DummyRequest() + response = object() # should error if mutated + request.response = response + # use a json renderer, which will mutate the response + result = self._callFUT('json', dict(a=1), request=request) + self.assertEqual(result, '{"a": 1}') + self.assertEqual(request.response, response) + + def test_no_response_to_preserve(self): + from pyramid.decorator import reify + class DummyRequestWithClassResponse(object): + _response = DummyResponse() + _response.content_type = None + _response.default_content_type = None + @reify + def response(self): + return self._response + request = DummyRequestWithClassResponse() + # use a json renderer, which will mutate the response + result = self._callFUT('json', dict(a=1), request=request) + self.assertEqual(result, '{"a": 1}') + self.assertFalse('response' in request.__dict__) + +class Test_render_to_response(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def _callFUT(self, renderer_name, value, request=None, package=None, + response=None): + from pyramid.renderers import render_to_response + return render_to_response(renderer_name, value, request=request, + package=package, response=response) + + def test_it_no_request(self): + renderer = self.config.testing_add_renderer( + 'pyramid.tests:abc/def.pt') + renderer.string_response = 'abc' + response = self._callFUT('abc/def.pt', dict(a=1)) + self.assertEqual(response.body, b'abc') + renderer.assert_(a=1) + renderer.assert_(request=None) + + def test_it_with_request(self): + renderer = self.config.testing_add_renderer( + 'pyramid.tests:abc/def.pt') + renderer.string_response = 'abc' + request = testing.DummyRequest() + response = self._callFUT('abc/def.pt', + dict(a=1), request=request) + self.assertEqual(response.body, b'abc') + renderer.assert_(a=1) + renderer.assert_(request=request) + + def test_it_with_package(self): + import pyramid.tests + renderer = self.config.testing_add_renderer( + 'pyramid.tests:abc/def.pt') + renderer.string_response = 'abc' + request = testing.DummyRequest() + response = self._callFUT('abc/def.pt', dict(a=1), request=request, + package=pyramid.tests) + self.assertEqual(response.body, b'abc') + renderer.assert_(a=1) + renderer.assert_(request=request) + + def test_response_preserved(self): + request = testing.DummyRequest() + response = object() # should error if mutated + request.response = response + # use a json renderer, which will mutate the response + result = self._callFUT('json', dict(a=1), request=request) + self.assertEqual(result.body, b'{"a": 1}') + self.assertNotEqual(request.response, result) + self.assertEqual(request.response, response) + + def test_no_response_to_preserve(self): + from pyramid.decorator import reify + class DummyRequestWithClassResponse(object): + _response = DummyResponse() + _response.content_type = None + _response.default_content_type = None + @reify + def response(self): + return self._response + request = DummyRequestWithClassResponse() + # use a json renderer, which will mutate the response + result = self._callFUT('json', dict(a=1), request=request) + self.assertEqual(result.body, b'{"a": 1}') + self.assertFalse('response' in request.__dict__) + + def test_custom_response_object(self): + class DummyRequestWithClassResponse(object): + pass + request = DummyRequestWithClassResponse() + response = DummyResponse() + # use a json renderer, which will mutate the response + result = self._callFUT('json', dict(a=1), request=request, + response=response) + self.assertTrue(result is response) + self.assertEqual(result.body, b'{"a": 1}') + self.assertFalse('response' in request.__dict__) + +class Test_get_renderer(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def _callFUT(self, renderer_name, **kw): + from pyramid.renderers import get_renderer + return get_renderer(renderer_name, **kw) + + def test_it_no_package(self): + renderer = self.config.testing_add_renderer( + 'pyramid.tests:abc/def.pt') + result = self._callFUT('abc/def.pt') + self.assertEqual(result, renderer) + + def test_it_with_package(self): + import pyramid.tests + renderer = self.config.testing_add_renderer( + 'pyramid.tests:abc/def.pt') + result = self._callFUT('abc/def.pt', package=pyramid.tests) + self.assertEqual(result, renderer) + + def test_it_with_registry(self): + renderer = self.config.testing_add_renderer( + 'pyramid.tests:abc/def.pt') + result = self._callFUT('abc/def.pt', registry=self.config.registry) + self.assertEqual(result, renderer) + + def test_it_with_isolated_registry(self): + from pyramid.config import Configurator + isolated_config = Configurator() + renderer = isolated_config.testing_add_renderer( + 'pyramid.tests:abc/def.pt') + result = self._callFUT('abc/def.pt', registry=isolated_config.registry) + self.assertEqual(result, renderer) + +class TestJSONP(unittest.TestCase): + def _makeOne(self, param_name='callback'): + from pyramid.renderers import JSONP + return JSONP(param_name) + + def test_render_to_jsonp(self): + renderer_factory = self._makeOne() + renderer = renderer_factory(None) + request = testing.DummyRequest() + request.GET['callback'] = 'callback' + result = renderer({'a':'1'}, {'request':request}) + self.assertEqual(result, '/**/callback({"a": "1"});') + self.assertEqual(request.response.content_type, + 'application/javascript') + + def test_render_to_jsonp_with_dot(self): + renderer_factory = self._makeOne() + renderer = renderer_factory(None) + request = testing.DummyRequest() + request.GET['callback'] = 'angular.callbacks._0' + result = renderer({'a':'1'}, {'request':request}) + self.assertEqual(result, '/**/angular.callbacks._0({"a": "1"});') + self.assertEqual(request.response.content_type, + 'application/javascript') + + def test_render_to_json(self): + renderer_factory = self._makeOne() + renderer = renderer_factory(None) + request = testing.DummyRequest() + result = renderer({'a':'1'}, {'request':request}) + self.assertEqual(result, '{"a": "1"}') + self.assertEqual(request.response.content_type, + 'application/json') + + def test_render_without_request(self): + renderer_factory = self._makeOne() + renderer = renderer_factory(None) + result = renderer({'a':'1'}, {}) + self.assertEqual(result, '{"a": "1"}') + + def test_render_to_jsonp_invalid_callback(self): + from pyramid.httpexceptions import HTTPBadRequest + renderer_factory = self._makeOne() + renderer = renderer_factory(None) + request = testing.DummyRequest() + request.GET['callback'] = '78mycallback' + self.assertRaises(HTTPBadRequest, renderer, {'a':'1'}, {'request':request}) + + +class Dummy: + pass + +class DummyResponse: + status = '200 OK' + default_content_type = 'text/html' + content_type = default_content_type + headerlist = () + app_iter = () + body = b'' + + # compat for renderer that will set unicode on py3 + def _set_text(self, val): # pragma: no cover + self.body = val.encode('utf8') + text = property(fset=_set_text) + diff --git a/src/pyramid/tests/test_request.py b/src/pyramid/tests/test_request.py new file mode 100644 index 000000000..c79c84d63 --- /dev/null +++ b/src/pyramid/tests/test_request.py @@ -0,0 +1,588 @@ +from collections import deque +import unittest +from pyramid import testing + +from pyramid.compat import ( + PY2, + text_, + bytes_, + native_, + ) +from pyramid.security import ( + AuthenticationAPIMixin, + AuthorizationAPIMixin, + ) + +class TestRequest(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def _getTargetClass(self): + from pyramid.request import Request + return Request + + def _makeOne(self, environ=None): + if environ is None: + environ = {} + return self._getTargetClass()(environ) + + def _registerResourceURL(self): + from pyramid.interfaces import IResourceURL + from zope.interface import Interface + class DummyResourceURL(object): + def __init__(self, context, request): + self.physical_path = '/context/' + self.virtual_path = '/context/' + self.config.registry.registerAdapter( + DummyResourceURL, (Interface, Interface), + IResourceURL) + + def test_class_conforms_to_IRequest(self): + from zope.interface.verify import verifyClass + from pyramid.interfaces import IRequest + verifyClass(IRequest, self._getTargetClass()) + + def test_instance_conforms_to_IRequest(self): + from zope.interface.verify import verifyObject + from pyramid.interfaces import IRequest + verifyObject(IRequest, self._makeOne()) + + def test_ResponseClass_is_pyramid_Response(self): + from pyramid.response import Response + cls = self._getTargetClass() + self.assertEqual(cls.ResponseClass, Response) + + def test_implements_security_apis(self): + apis = (AuthenticationAPIMixin, AuthorizationAPIMixin) + r = self._makeOne() + self.assertTrue(isinstance(r, apis)) + + def test_charset_defaults_to_utf8(self): + r = self._makeOne({'PATH_INFO':'/'}) + self.assertEqual(r.charset, 'UTF-8') + + def test_exception_defaults_to_None(self): + r = self._makeOne({'PATH_INFO':'/'}) + self.assertEqual(r.exception, None) + + def test_matchdict_defaults_to_None(self): + r = self._makeOne({'PATH_INFO':'/'}) + self.assertEqual(r.matchdict, None) + + def test_matched_route_defaults_to_None(self): + r = self._makeOne({'PATH_INFO':'/'}) + self.assertEqual(r.matched_route, None) + + def test_params_decoded_from_utf_8_by_default(self): + environ = { + 'PATH_INFO':'/', + 'QUERY_STRING':'la=La%20Pe%C3%B1a' + } + request = self._makeOne(environ) + request.charset = None + self.assertEqual(request.GET['la'], text_(b'La Pe\xf1a')) + + def test_tmpl_context(self): + from pyramid.request import TemplateContext + inst = self._makeOne() + result = inst.tmpl_context + self.assertEqual(result.__class__, TemplateContext) + + def test_session_configured(self): + from pyramid.interfaces import ISessionFactory + inst = self._makeOne() + def factory(request): + return 'orangejuice' + self.config.registry.registerUtility(factory, ISessionFactory) + inst.registry = self.config.registry + self.assertEqual(inst.session, 'orangejuice') + self.assertEqual(inst.__dict__['session'], 'orangejuice') + + def test_session_not_configured(self): + inst = self._makeOne() + inst.registry = self.config.registry + self.assertRaises(AttributeError, getattr, inst, 'session') + + def test_setattr_and_getattr_dotnotation(self): + inst = self._makeOne() + inst.foo = 1 + self.assertEqual(inst.foo, 1) + + def test_setattr_and_getattr(self): + environ = {} + inst = self._makeOne(environ) + setattr(inst, 'bar', 1) + self.assertEqual(getattr(inst, 'bar'), 1) + self.assertEqual(environ, {}) # make sure we're not using adhoc attrs + + def test_add_response_callback(self): + inst = self._makeOne() + self.assertEqual(len(inst.response_callbacks), 0) + def callback(request, response): + """ """ + inst.add_response_callback(callback) + self.assertEqual(list(inst.response_callbacks), [callback]) + inst.add_response_callback(callback) + self.assertEqual(list(inst.response_callbacks), [callback, callback]) + + def test__process_response_callbacks(self): + inst = self._makeOne() + def callback1(request, response): + request.called1 = True + response.called1 = True + def callback2(request, response): + request.called2 = True + response.called2 = True + inst.add_response_callback(callback1) + inst.add_response_callback(callback2) + response = DummyResponse() + inst._process_response_callbacks(response) + self.assertEqual(inst.called1, True) + self.assertEqual(inst.called2, True) + self.assertEqual(response.called1, True) + self.assertEqual(response.called2, True) + self.assertEqual(len(inst.response_callbacks), 0) + + def test__process_response_callback_adding_response_callback(self): + """ + When a response callback adds another callback, that new callback should still be called. + + See https://github.com/Pylons/pyramid/pull/1373 + """ + inst = self._makeOne() + def callback1(request, response): + request.called1 = True + response.called1 = True + request.add_response_callback(callback2) + def callback2(request, response): + request.called2 = True + response.called2 = True + inst.add_response_callback(callback1) + response = DummyResponse() + inst._process_response_callbacks(response) + self.assertEqual(inst.called1, True) + self.assertEqual(inst.called2, True) + self.assertEqual(response.called1, True) + self.assertEqual(response.called2, True) + self.assertEqual(len(inst.response_callbacks), 0) + + def test_add_finished_callback(self): + inst = self._makeOne() + self.assertEqual(len(inst.finished_callbacks), 0) + def callback(request): + """ """ + inst.add_finished_callback(callback) + self.assertEqual(list(inst.finished_callbacks), [callback]) + inst.add_finished_callback(callback) + self.assertEqual(list(inst.finished_callbacks), [callback, callback]) + + def test__process_finished_callbacks(self): + inst = self._makeOne() + def callback1(request): + request.called1 = True + def callback2(request): + request.called2 = True + inst.add_finished_callback(callback1) + inst.add_finished_callback(callback2) + inst._process_finished_callbacks() + self.assertEqual(inst.called1, True) + self.assertEqual(inst.called2, True) + self.assertEqual(len(inst.finished_callbacks), 0) + + def test_resource_url(self): + self._registerResourceURL() + environ = { + 'PATH_INFO':'/', + 'SERVER_NAME':'example.com', + 'SERVER_PORT':'80', + 'wsgi.url_scheme':'http', + } + inst = self._makeOne(environ) + root = DummyContext() + result = inst.resource_url(root) + self.assertEqual(result, 'http://example.com/context/') + + def test_route_url(self): + environ = { + 'PATH_INFO':'/', + 'SERVER_NAME':'example.com', + 'SERVER_PORT':'5432', + 'QUERY_STRING':'la=La%20Pe%C3%B1a', + 'wsgi.url_scheme':'http', + } + from pyramid.interfaces import IRoutesMapper + inst = self._makeOne(environ) + mapper = DummyRoutesMapper(route=DummyRoute('/1/2/3')) + self.config.registry.registerUtility(mapper, IRoutesMapper) + result = inst.route_url('flub', 'extra1', 'extra2', + a=1, b=2, c=3, _query={'a':1}, + _anchor=text_("foo")) + self.assertEqual(result, + 'http://example.com:5432/1/2/3/extra1/extra2?a=1#foo') + + def test_route_path(self): + environ = { + 'PATH_INFO':'/', + 'SERVER_NAME':'example.com', + 'SERVER_PORT':'5432', + 'QUERY_STRING':'la=La%20Pe%C3%B1a', + 'wsgi.url_scheme':'http', + } + from pyramid.interfaces import IRoutesMapper + inst = self._makeOne(environ) + mapper = DummyRoutesMapper(route=DummyRoute('/1/2/3')) + self.config.registry.registerUtility(mapper, IRoutesMapper) + result = inst.route_path('flub', 'extra1', 'extra2', + a=1, b=2, c=3, _query={'a':1}, + _anchor=text_("foo")) + self.assertEqual(result, '/1/2/3/extra1/extra2?a=1#foo') + + def test_static_url(self): + from pyramid.interfaces import IStaticURLInfo + environ = { + 'PATH_INFO':'/', + 'SERVER_NAME':'example.com', + 'SERVER_PORT':'5432', + 'QUERY_STRING':'', + 'wsgi.url_scheme':'http', + } + request = self._makeOne(environ) + info = DummyStaticURLInfo('abc') + self.config.registry.registerUtility(info, IStaticURLInfo) + result = request.static_url('pyramid.tests:static/foo.css') + self.assertEqual(result, 'abc') + self.assertEqual(info.args, + ('pyramid.tests:static/foo.css', request, {}) ) + + def test_is_response_false(self): + request = self._makeOne() + request.registry = self.config.registry + self.assertEqual(request.is_response('abc'), False) + + def test_is_response_true_ob_is_pyramid_response(self): + from pyramid.response import Response + r = Response('hello') + request = self._makeOne() + request.registry = self.config.registry + self.assertEqual(request.is_response(r), True) + + def test_is_response_false_adapter_is_not_self(self): + from pyramid.interfaces import IResponse + request = self._makeOne() + request.registry = self.config.registry + def adapter(ob): + return object() + class Foo(object): + pass + foo = Foo() + request.registry.registerAdapter(adapter, (Foo,), IResponse) + self.assertEqual(request.is_response(foo), False) + + def test_is_response_adapter_true(self): + from pyramid.interfaces import IResponse + request = self._makeOne() + request.registry = self.config.registry + class Foo(object): + pass + foo = Foo() + def adapter(ob): + return ob + request.registry.registerAdapter(adapter, (Foo,), IResponse) + self.assertEqual(request.is_response(foo), True) + + def test_json_body_invalid_json(self): + request = self._makeOne({'REQUEST_METHOD':'POST'}) + request.body = b'{' + self.assertRaises(ValueError, getattr, request, 'json_body') + + def test_json_body_valid_json(self): + request = self._makeOne({'REQUEST_METHOD':'POST'}) + request.body = b'{"a":1}' + self.assertEqual(request.json_body, {'a':1}) + + def test_json_body_alternate_charset(self): + import json + request = self._makeOne({'REQUEST_METHOD':'POST'}) + inp = text_( + b'/\xe6\xb5\x81\xe8\xa1\x8c\xe8\xb6\x8b\xe5\x8a\xbf', + 'utf-8' + ) + if PY2: + body = json.dumps({'a':inp}).decode('utf-8').encode('utf-16') + else: + body = bytes(json.dumps({'a':inp}), 'utf-16') + request.body = body + request.content_type = 'application/json; charset=utf-16' + self.assertEqual(request.json_body, {'a':inp}) + + def test_json_body_GET_request(self): + request = self._makeOne({'REQUEST_METHOD':'GET'}) + self.assertRaises(ValueError, getattr, request, 'json_body') + + def test_set_property(self): + request = self._makeOne() + opts = [2, 1] + def connect(obj): + return opts.pop() + request.set_property(connect, name='db') + self.assertEqual(1, request.db) + self.assertEqual(2, request.db) + + def test_set_property_reify(self): + request = self._makeOne() + opts = [2, 1] + def connect(obj): + return opts.pop() + request.set_property(connect, name='db', reify=True) + self.assertEqual(1, request.db) + self.assertEqual(1, request.db) + +class Test_route_request_iface(unittest.TestCase): + def _callFUT(self, name): + from pyramid.request import route_request_iface + return route_request_iface(name) + + def test_it(self): + iface = self._callFUT('routename') + self.assertEqual(iface.__name__, 'routename_IRequest') + self.assertTrue(hasattr(iface, 'combined')) + self.assertEqual(iface.combined.__name__, 'routename_combined_IRequest') + + def test_it_routename_with_spaces(self): + # see https://github.com/Pylons/pyramid/issues/232 + iface = self._callFUT('routename with spaces') + self.assertEqual(iface.__name__, 'routename with spaces_IRequest') + self.assertTrue(hasattr(iface, 'combined')) + self.assertEqual(iface.combined.__name__, + 'routename with spaces_combined_IRequest') + + +class Test_add_global_response_headers(unittest.TestCase): + def _callFUT(self, request, headerlist): + from pyramid.request import add_global_response_headers + return add_global_response_headers(request, headerlist) + + def test_it(self): + request = DummyRequest() + response = DummyResponse() + self._callFUT(request, [('c', 1)]) + self.assertEqual(len(request.response_callbacks), 1) + request.response_callbacks[0](None, response) + self.assertEqual(response.headerlist, [('c', 1)] ) + +class Test_call_app_with_subpath_as_path_info(unittest.TestCase): + def _callFUT(self, request, app): + from pyramid.request import call_app_with_subpath_as_path_info + return call_app_with_subpath_as_path_info(request, app) + + def test_it_all_request_and_environment_data_missing(self): + request = DummyRequest({}) + response = self._callFUT(request, 'app') + self.assertTrue(request.copied) + self.assertEqual(response, 'app') + self.assertEqual(request.environ['SCRIPT_NAME'], '') + self.assertEqual(request.environ['PATH_INFO'], '/') + + def test_it_with_subpath_and_path_info(self): + request = DummyRequest({'PATH_INFO':'/hello'}) + request.subpath = ('hello',) + response = self._callFUT(request, 'app') + self.assertTrue(request.copied) + self.assertEqual(response, 'app') + self.assertEqual(request.environ['SCRIPT_NAME'], '') + self.assertEqual(request.environ['PATH_INFO'], '/hello') + + def test_it_with_subpath_and_path_info_path_info_endswith_slash(self): + request = DummyRequest({'PATH_INFO':'/hello/'}) + request.subpath = ('hello',) + response = self._callFUT(request, 'app') + self.assertTrue(request.copied) + self.assertEqual(response, 'app') + self.assertEqual(request.environ['SCRIPT_NAME'], '') + self.assertEqual(request.environ['PATH_INFO'], '/hello/') + + def test_it_with_subpath_and_path_info_extra_script_name(self): + request = DummyRequest({'PATH_INFO':'/hello', 'SCRIPT_NAME':'/script'}) + request.subpath = ('hello',) + response = self._callFUT(request, 'app') + self.assertTrue(request.copied) + self.assertEqual(response, 'app') + self.assertEqual(request.environ['SCRIPT_NAME'], '/script') + self.assertEqual(request.environ['PATH_INFO'], '/hello') + + def test_it_with_extra_slashes_in_path_info(self): + request = DummyRequest({'PATH_INFO':'//hello/', + 'SCRIPT_NAME':'/script'}) + request.subpath = ('hello',) + response = self._callFUT(request, 'app') + self.assertTrue(request.copied) + self.assertEqual(response, 'app') + self.assertEqual(request.environ['SCRIPT_NAME'], '/script') + self.assertEqual(request.environ['PATH_INFO'], '/hello/') + + def test_subpath_path_info_and_script_name_have_utf8(self): + encoded = native_(text_(b'La Pe\xc3\xb1a')) + decoded = text_(bytes_(encoded), 'utf-8') + request = DummyRequest({'PATH_INFO':'/' + encoded, + 'SCRIPT_NAME':'/' + encoded}) + request.subpath = (decoded, ) + response = self._callFUT(request, 'app') + self.assertTrue(request.copied) + self.assertEqual(response, 'app') + self.assertEqual(request.environ['SCRIPT_NAME'], '/' + encoded) + self.assertEqual(request.environ['PATH_INFO'], '/' + encoded) + +class Test_apply_request_extensions(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def _callFUT(self, request, extensions=None): + from pyramid.request import apply_request_extensions + return apply_request_extensions(request, extensions=extensions) + + def test_it_with_registry(self): + from pyramid.interfaces import IRequestExtensions + extensions = Dummy() + extensions.methods = {'foo': lambda x, y: y} + extensions.descriptors = {'bar': property(lambda x: 'bar')} + self.config.registry.registerUtility(extensions, IRequestExtensions) + request = DummyRequest() + request.registry = self.config.registry + self._callFUT(request) + self.assertEqual(request.bar, 'bar') + self.assertEqual(request.foo('abc'), 'abc') + + def test_it_override_extensions(self): + from pyramid.interfaces import IRequestExtensions + ignore = Dummy() + ignore.methods = {'x': lambda x, y, z: 'asdf'} + ignore.descriptors = {'bar': property(lambda x: 'asdf')} + self.config.registry.registerUtility(ignore, IRequestExtensions) + request = DummyRequest() + request.registry = self.config.registry + + extensions = Dummy() + extensions.methods = {'foo': lambda x, y: y} + extensions.descriptors = {'bar': property(lambda x: 'bar')} + self._callFUT(request, extensions=extensions) + self.assertRaises(AttributeError, lambda: request.x) + self.assertEqual(request.bar, 'bar') + self.assertEqual(request.foo('abc'), 'abc') + +class Dummy(object): + pass + +class Test_subclassing_Request(unittest.TestCase): + def test_subclass(self): + from pyramid.interfaces import IRequest + from pyramid.request import Request + + class RequestSub(Request): + pass + + self.assertTrue(hasattr(Request, '__provides__')) + self.assertTrue(hasattr(Request, '__implemented__')) + self.assertTrue(hasattr(Request, '__providedBy__')) + self.assertFalse(hasattr(RequestSub, '__provides__')) + self.assertTrue(hasattr(RequestSub, '__providedBy__')) + self.assertTrue(hasattr(RequestSub, '__implemented__')) + + self.assertTrue(IRequest.implementedBy(RequestSub)) + # The call to implementedBy will add __provides__ to the class + self.assertTrue(hasattr(RequestSub, '__provides__')) + + + def test_subclass_with_implementer(self): + from pyramid.interfaces import IRequest + from pyramid.request import Request + from pyramid.util import InstancePropertyHelper + from zope.interface import implementer + + @implementer(IRequest) + class RequestSub(Request): + pass + + self.assertTrue(hasattr(Request, '__provides__')) + self.assertTrue(hasattr(Request, '__implemented__')) + self.assertTrue(hasattr(Request, '__providedBy__')) + self.assertTrue(hasattr(RequestSub, '__provides__')) + self.assertTrue(hasattr(RequestSub, '__providedBy__')) + self.assertTrue(hasattr(RequestSub, '__implemented__')) + + req = RequestSub({}) + helper = InstancePropertyHelper() + helper.apply_properties(req, {'b': 'b'}) + + self.assertTrue(IRequest.providedBy(req)) + self.assertTrue(IRequest.implementedBy(RequestSub)) + + def test_subclass_mutate_before_providedBy(self): + from pyramid.interfaces import IRequest + from pyramid.request import Request + from pyramid.util import InstancePropertyHelper + + class RequestSub(Request): + pass + + req = RequestSub({}) + helper = InstancePropertyHelper() + helper.apply_properties(req, {'b': 'b'}) + + self.assertTrue(IRequest.providedBy(req)) + self.assertTrue(IRequest.implementedBy(RequestSub)) + + +class DummyRequest(object): + def __init__(self, environ=None): + if environ is None: + environ = {} + self.environ = environ + + def add_response_callback(self, callback): + self.response_callbacks = [callback] + + def get_response(self, app): + return app + + def copy(self): + self.copied = True + return self + +class DummyResponse: + def __init__(self): + self.headerlist = [] + + +class DummyContext: + pass + +class DummyRoutesMapper: + raise_exc = None + def __init__(self, route=None, raise_exc=False): + self.route = route + + def get_route(self, route_name): + return self.route + +class DummyRoute: + pregenerator = None + def __init__(self, result='/1/2/3'): + self.result = result + + def generate(self, kw): + self.kw = kw + return self.result + +class DummyStaticURLInfo: + def __init__(self, result): + self.result = result + + def generate(self, path, request, **kw): + self.args = path, request, kw + return self.result diff --git a/src/pyramid/tests/test_response.py b/src/pyramid/tests/test_response.py new file mode 100644 index 000000000..53e3ce17a --- /dev/null +++ b/src/pyramid/tests/test_response.py @@ -0,0 +1,214 @@ +import io +import mimetypes +import os +import unittest +from pyramid import testing + +class TestResponse(unittest.TestCase): + def _getTargetClass(self): + from pyramid.response import Response + return Response + + def test_implements_IResponse(self): + from pyramid.interfaces import IResponse + cls = self._getTargetClass() + self.assertTrue(IResponse.implementedBy(cls)) + + def test_provides_IResponse(self): + from pyramid.interfaces import IResponse + inst = self._getTargetClass()() + self.assertTrue(IResponse.providedBy(inst)) + +class TestFileResponse(unittest.TestCase): + def _makeOne(self, file, **kw): + from pyramid.response import FileResponse + return FileResponse(file, **kw) + + def _getPath(self, suffix='txt'): + here = os.path.dirname(__file__) + return os.path.join(here, 'fixtures', 'minimal.%s' % (suffix,)) + + def test_with_image_content_type(self): + path = self._getPath('jpg') + r = self._makeOne(path, content_type='image/jpeg') + self.assertEqual(r.content_type, 'image/jpeg') + self.assertEqual(r.headers['content-type'], 'image/jpeg') + path = self._getPath() + r.app_iter.close() + + def test_with_xml_content_type(self): + path = self._getPath('xml') + r = self._makeOne(path, content_type='application/xml') + self.assertEqual(r.content_type, 'application/xml') + self.assertEqual(r.headers['content-type'], + 'application/xml; charset=UTF-8') + r.app_iter.close() + + def test_with_pdf_content_type(self): + path = self._getPath('xml') + r = self._makeOne(path, content_type='application/pdf') + self.assertEqual(r.content_type, 'application/pdf') + self.assertEqual(r.headers['content-type'], 'application/pdf') + r.app_iter.close() + + def test_without_content_type(self): + for suffix in ('txt', 'xml', 'pdf'): + path = self._getPath(suffix) + r = self._makeOne(path) + self.assertEqual(r.headers['content-type'].split(';')[0], + mimetypes.guess_type(path, strict=False)[0]) + r.app_iter.close() + + def test_python_277_bug_15207(self): + # python 2.7.7 on windows has a bug where its mimetypes.guess_type + # function returns Unicode for the content_type, unlike any previous + # version of Python. See https://github.com/Pylons/pyramid/issues/1360 + # for more information. + from pyramid.compat import text_ + import mimetypes as old_mimetypes + from pyramid import response + class FakeMimetypesModule(object): + def guess_type(self, *arg, **kw): + return text_('foo/bar'), None + fake_mimetypes = FakeMimetypesModule() + try: + response.mimetypes = fake_mimetypes + path = self._getPath('xml') + r = self._makeOne(path) + self.assertEqual(r.content_type, 'foo/bar') + self.assertEqual(type(r.content_type), str) + finally: + response.mimetypes = old_mimetypes + +class TestFileIter(unittest.TestCase): + def _makeOne(self, file, block_size): + from pyramid.response import FileIter + return FileIter(file, block_size) + + def test___iter__(self): + f = io.BytesIO(b'abc') + inst = self._makeOne(f, 1) + self.assertEqual(inst.__iter__(), inst) + + def test_iteration(self): + data = b'abcdef' + f = io.BytesIO(b'abcdef') + inst = self._makeOne(f, 1) + r = b'' + for x in inst: + self.assertEqual(len(x), 1) + r+=x + self.assertEqual(r, data) + + def test_close(self): + f = io.BytesIO(b'abc') + inst = self._makeOne(f, 1) + inst.close() + self.assertTrue(f.closed) + +class Test_patch_mimetypes(unittest.TestCase): + def _callFUT(self, module): + from pyramid.response import init_mimetypes + return init_mimetypes(module) + + def test_has_init(self): + class DummyMimetypes(object): + def init(self): + self.initted = True + module = DummyMimetypes() + result = self._callFUT(module) + self.assertEqual(result, True) + self.assertEqual(module.initted, True) + + def test_missing_init(self): + class DummyMimetypes(object): + pass + module = DummyMimetypes() + result = self._callFUT(module) + self.assertEqual(result, False) + + +class TestResponseAdapter(unittest.TestCase): + def setUp(self): + registry = Dummy() + self.config = testing.setUp(registry=registry) + + def tearDown(self): + self.config.end() + + def _makeOne(self, *types_or_ifaces, **kw): + from pyramid.response import response_adapter + return response_adapter(*types_or_ifaces, **kw) + + def test_register_single(self): + from zope.interface import Interface + class IFoo(Interface): pass + dec = self._makeOne(IFoo) + def foo(): pass + config = DummyConfigurator() + scanner = Dummy() + scanner.config = config + dec.register(scanner, None, foo) + self.assertEqual(config.adapters, [(foo, IFoo)]) + + def test_register_multi(self): + from zope.interface import Interface + class IFoo(Interface): pass + class IBar(Interface): pass + dec = self._makeOne(IFoo, IBar) + def foo(): pass + config = DummyConfigurator() + scanner = Dummy() + scanner.config = config + dec.register(scanner, None, foo) + self.assertEqual(config.adapters, [(foo, IFoo), (foo, IBar)]) + + def test___call__(self): + from zope.interface import Interface + class IFoo(Interface): pass + dec = self._makeOne(IFoo) + dummy_venusian = DummyVenusian() + dec.venusian = dummy_venusian + def foo(): pass + dec(foo) + self.assertEqual(dummy_venusian.attached, + [(foo, dec.register, 'pyramid', 1)]) + + def test___call___with_venusian_args(self): + from zope.interface import Interface + class IFoo(Interface): pass + dec = self._makeOne(IFoo, _category='foo', _depth=1) + dummy_venusian = DummyVenusian() + dec.venusian = dummy_venusian + def foo(): pass + dec(foo) + self.assertEqual(dummy_venusian.attached, + [(foo, dec.register, 'foo', 2)]) + + +class TestGetResponseFactory(unittest.TestCase): + def test_get_factory(self): + from pyramid.registry import Registry + from pyramid.response import Response, _get_response_factory + + registry = Registry() + response = _get_response_factory(registry)(None) + self.assertTrue(isinstance(response, Response)) + + +class Dummy(object): + pass + +class DummyConfigurator(object): + def __init__(self): + self.adapters = [] + + def add_response_adapter(self, wrapped, type_or_iface): + self.adapters.append((wrapped, type_or_iface)) + +class DummyVenusian(object): + def __init__(self): + self.attached = [] + + def attach(self, wrapped, fn, category=None, depth=None): + self.attached.append((wrapped, fn, category, depth)) diff --git a/src/pyramid/tests/test_router.py b/src/pyramid/tests/test_router.py new file mode 100644 index 000000000..6097018f0 --- /dev/null +++ b/src/pyramid/tests/test_router.py @@ -0,0 +1,1410 @@ +import unittest + +from pyramid import testing + +class TestRouter(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + self.registry = self.config.registry + + def tearDown(self): + testing.tearDown() + + def _registerRouteRequest(self, name): + from pyramid.interfaces import IRouteRequest + from pyramid.request import route_request_iface + iface = route_request_iface(name) + self.registry.registerUtility(iface, IRouteRequest, name=name) + return iface + + def _connectRoute(self, name, path, factory=None): + from pyramid.interfaces import IRoutesMapper + from pyramid.urldispatch import RoutesMapper + mapper = self.registry.queryUtility(IRoutesMapper) + if mapper is None: + mapper = RoutesMapper() + self.registry.registerUtility(mapper, IRoutesMapper) + return mapper.connect(name, path, factory) + + def _registerLogger(self): + from pyramid.interfaces import IDebugLogger + logger = DummyLogger() + self.registry.registerUtility(logger, IDebugLogger) + return logger + + def _registerSettings(self, **kw): + settings = {'debug_authorization':False, + 'debug_notfound':False, + 'debug_routematch':False} + settings.update(kw) + self.registry.settings = settings + + def _registerTraverserFactory(self, context, view_name='', subpath=None, + traversed=None, virtual_root=None, + virtual_root_path=None, raise_error=None, + **kw): + from pyramid.interfaces import ITraverser + + if virtual_root is None: + virtual_root = context + if subpath is None: + subpath = [] + if traversed is None: + traversed = [] + if virtual_root_path is None: + virtual_root_path = [] + + class DummyTraverserFactory: + def __init__(self, root): + self.root = root + + def __call__(self, request): + if raise_error: + raise raise_error + values = {'root':self.root, + 'context':context, + 'view_name':view_name, + 'subpath':subpath, + 'traversed':traversed, + 'virtual_root':virtual_root, + 'virtual_root_path':virtual_root_path} + kw.update(values) + return kw + + self.registry.registerAdapter(DummyTraverserFactory, (None,), + ITraverser, name='') + + def _registerView(self, app, name, classifier, req_iface, ctx_iface): + from pyramid.interfaces import IView + self.registry.registerAdapter( + app, (classifier, req_iface, ctx_iface), IView, name) + + def _registerEventListener(self, iface): + L = [] + def listener(event): + L.append(event) + self.registry.registerHandler(listener, (iface,)) + return L + + def _registerRootFactory(self, val): + rootfactory = DummyRootFactory(val) + from pyramid.interfaces import IRootFactory + self.registry.registerUtility(rootfactory, IRootFactory) + return rootfactory + + def _getTargetClass(self): + from pyramid.router import Router + return Router + + def _makeOne(self): + klass = self._getTargetClass() + return klass(self.registry) + + def _makeEnviron(self, **extras): + environ = { + 'wsgi.url_scheme':'http', + 'SERVER_NAME':'localhost', + 'SERVER_PORT':'8080', + 'REQUEST_METHOD':'GET', + 'PATH_INFO':'/', + } + environ.update(extras) + return environ + + def test_ctor_registry_has_no_settings(self): + self.registry.settings = None + router = self._makeOne() + self.assertEqual(router.debug_notfound, False) + self.assertEqual(router.debug_routematch, False) + self.assertFalse('debug_notfound' in router.__dict__) + self.assertFalse('debug_routematch' in router.__dict__) + + def test_root_policy(self): + context = DummyContext() + self._registerTraverserFactory(context) + rootfactory = self._registerRootFactory('abc') + router = self._makeOne() + self.assertEqual(router.root_policy, rootfactory) + + def test_request_factory(self): + from pyramid.interfaces import IRequestFactory + class DummyRequestFactory(object): + pass + self.registry.registerUtility(DummyRequestFactory, IRequestFactory) + router = self._makeOne() + self.assertEqual(router.request_factory, DummyRequestFactory) + + def test_tween_factories(self): + from pyramid.interfaces import ITweens + from pyramid.config.tweens import Tweens + from pyramid.response import Response + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IResponse + tweens = Tweens() + self.registry.registerUtility(tweens, ITweens) + L = [] + def tween_factory1(handler, registry): + L.append((handler, registry)) + def wrapper(request): + request.environ['handled'].append('one') + return handler(request) + wrapper.name = 'one' + wrapper.child = handler + return wrapper + def tween_factory2(handler, registry): + L.append((handler, registry)) + def wrapper(request): + request.environ['handled'] = ['two'] + return handler(request) + wrapper.name = 'two' + wrapper.child = handler + return wrapper + tweens.add_implicit('one', tween_factory1) + tweens.add_implicit('two', tween_factory2) + router = self._makeOne() + self.assertEqual(router.handle_request.name, 'two') + self.assertEqual(router.handle_request.child.name, 'one') + self.assertEqual(router.handle_request.child.child.__name__, + 'handle_request') + context = DummyContext() + self._registerTraverserFactory(context) + environ = self._makeEnviron() + view = DummyView('abc') + self._registerView(self.config.derive_view(view), '', + IViewClassifier, None, None) + start_response = DummyStartResponse() + def make_response(s): + return Response(s) + router.registry.registerAdapter(make_response, (str,), IResponse) + app_iter = router(environ, start_response) + self.assertEqual(app_iter, [b'abc']) + self.assertEqual(start_response.status, '200 OK') + self.assertEqual(environ['handled'], ['two', 'one']) + + def test_call_traverser_default(self): + from pyramid.httpexceptions import HTTPNotFound + environ = self._makeEnviron() + logger = self._registerLogger() + router = self._makeOne() + start_response = DummyStartResponse() + why = exc_raised(HTTPNotFound, router, environ, start_response) + self.assertTrue('/' in why.args[0], why) + self.assertFalse('debug_notfound' in why.args[0]) + self.assertEqual(len(logger.messages), 0) + + def test_traverser_raises_notfound_class(self): + from pyramid.httpexceptions import HTTPNotFound + environ = self._makeEnviron() + context = DummyContext() + self._registerTraverserFactory(context, raise_error=HTTPNotFound) + router = self._makeOne() + start_response = DummyStartResponse() + self.assertRaises(HTTPNotFound, router, environ, start_response) + + def test_traverser_raises_notfound_instance(self): + from pyramid.httpexceptions import HTTPNotFound + environ = self._makeEnviron() + context = DummyContext() + self._registerTraverserFactory(context, raise_error=HTTPNotFound('foo')) + router = self._makeOne() + start_response = DummyStartResponse() + why = exc_raised(HTTPNotFound, router, environ, start_response) + self.assertTrue('foo' in why.args[0], why) + + def test_traverser_raises_forbidden_class(self): + from pyramid.httpexceptions import HTTPForbidden + environ = self._makeEnviron() + context = DummyContext() + self._registerTraverserFactory(context, raise_error=HTTPForbidden) + router = self._makeOne() + start_response = DummyStartResponse() + self.assertRaises(HTTPForbidden, router, environ, start_response) + + def test_traverser_raises_forbidden_instance(self): + from pyramid.httpexceptions import HTTPForbidden + environ = self._makeEnviron() + context = DummyContext() + self._registerTraverserFactory(context, + raise_error=HTTPForbidden('foo')) + router = self._makeOne() + start_response = DummyStartResponse() + why = exc_raised(HTTPForbidden, router, environ, start_response) + self.assertTrue('foo' in why.args[0], why) + + def test_call_no_view_registered_no_isettings(self): + from pyramid.httpexceptions import HTTPNotFound + environ = self._makeEnviron() + context = DummyContext() + self._registerTraverserFactory(context) + logger = self._registerLogger() + router = self._makeOne() + start_response = DummyStartResponse() + why = exc_raised(HTTPNotFound, router, environ, start_response) + self.assertTrue('/' in why.args[0], why) + self.assertFalse('debug_notfound' in why.args[0]) + self.assertEqual(len(logger.messages), 0) + + def test_call_no_view_registered_debug_notfound_false(self): + from pyramid.httpexceptions import HTTPNotFound + environ = self._makeEnviron() + context = DummyContext() + self._registerTraverserFactory(context) + logger = self._registerLogger() + self._registerSettings(debug_notfound=False) + router = self._makeOne() + start_response = DummyStartResponse() + why = exc_raised(HTTPNotFound, router, environ, start_response) + self.assertTrue('/' in why.args[0], why) + self.assertFalse('debug_notfound' in why.args[0]) + self.assertEqual(len(logger.messages), 0) + + def test_call_no_view_registered_debug_notfound_true(self): + from pyramid.httpexceptions import HTTPNotFound + environ = self._makeEnviron() + context = DummyContext() + self._registerTraverserFactory(context) + self._registerSettings(debug_notfound=True) + logger = self._registerLogger() + router = self._makeOne() + start_response = DummyStartResponse() + why = exc_raised(HTTPNotFound, router, environ, start_response) + self.assertTrue( + "debug_notfound of url http://localhost:8080/; " in why.args[0]) + self.assertTrue("view_name: '', subpath: []" in why.args[0]) + self.assertTrue('http://localhost:8080' in why.args[0], why) + + self.assertEqual(len(logger.messages), 1) + message = logger.messages[0] + self.assertTrue('of url http://localhost:8080' in message) + self.assertTrue("path_info: " in message) + self.assertTrue('DummyContext' in message) + self.assertTrue("view_name: ''" in message) + self.assertTrue("subpath: []" in message) + + def test_call_view_returns_non_iresponse(self): + from pyramid.interfaces import IViewClassifier + context = DummyContext() + self._registerTraverserFactory(context) + environ = self._makeEnviron() + view = DummyView('abc') + self._registerView(self.config.derive_view(view), '', IViewClassifier, + None, None) + router = self._makeOne() + start_response = DummyStartResponse() + self.assertRaises(ValueError, router, environ, start_response) + + def test_call_view_returns_adapted_response(self): + from pyramid.response import Response + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IResponse + context = DummyContext() + self._registerTraverserFactory(context) + environ = self._makeEnviron() + view = DummyView('abc') + self._registerView(self.config.derive_view(view), '', + IViewClassifier, None, None) + router = self._makeOne() + start_response = DummyStartResponse() + def make_response(s): + return Response(s) + router.registry.registerAdapter(make_response, (str,), IResponse) + app_iter = router(environ, start_response) + self.assertEqual(app_iter, [b'abc']) + self.assertEqual(start_response.status, '200 OK') + + def test_call_with_request_extensions(self): + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IRequestExtensions + from pyramid.interfaces import IRequest + from pyramid.request import Request + from pyramid.util import InstancePropertyHelper + context = DummyContext() + self._registerTraverserFactory(context) + class Extensions(object): + def __init__(self): + self.methods = {} + self.descriptors = {} + extensions = Extensions() + ext_method = lambda r: 'bar' + name, fn = InstancePropertyHelper.make_property(ext_method, name='foo') + extensions.descriptors[name] = fn + request = Request.blank('/') + request.request_iface = IRequest + request.registry = self.registry + def request_factory(environ): + return request + self.registry.registerUtility(extensions, IRequestExtensions) + environ = self._makeEnviron() + response = DummyResponse() + response.app_iter = ['Hello world'] + view = DummyView(response) + self._registerView(self.config.derive_view(view), '', + IViewClassifier, None, None) + router = self._makeOne() + router.request_factory = request_factory + start_response = DummyStartResponse() + router(environ, start_response) + self.assertEqual(view.request.foo, 'bar') + + def test_call_view_registered_nonspecific_default_path(self): + from pyramid.interfaces import IViewClassifier + context = DummyContext() + self._registerTraverserFactory(context) + response = DummyResponse() + response.app_iter = ['Hello world'] + view = DummyView(response) + environ = self._makeEnviron() + self._registerView(self.config.derive_view(view), '', + IViewClassifier, None, None) + self._registerRootFactory(context) + router = self._makeOne() + start_response = DummyStartResponse() + result = router(environ, start_response) + self.assertEqual(result, ['Hello world']) + self.assertEqual(start_response.headers, ()) + self.assertEqual(start_response.status, '200 OK') + request = view.request + self.assertEqual(request.view_name, '') + self.assertEqual(request.subpath, []) + self.assertEqual(request.context, context) + self.assertEqual(request.root, context) + + def test_call_view_registered_nonspecific_nondefault_path_and_subpath(self): + from pyramid.interfaces import IViewClassifier + context = DummyContext() + self._registerTraverserFactory(context, view_name='foo', + subpath=['bar'], + traversed=['context']) + self._registerRootFactory(context) + response = DummyResponse() + response.app_iter = ['Hello world'] + view = DummyView(response) + environ = self._makeEnviron() + self._registerView(view, 'foo', IViewClassifier, None, None) + router = self._makeOne() + start_response = DummyStartResponse() + result = router(environ, start_response) + self.assertEqual(result, ['Hello world']) + self.assertEqual(start_response.headers, ()) + self.assertEqual(start_response.status, '200 OK') + request = view.request + self.assertEqual(request.view_name, 'foo') + self.assertEqual(request.subpath, ['bar']) + self.assertEqual(request.context, context) + self.assertEqual(request.root, context) + + def test_call_view_registered_specific_success(self): + from zope.interface import Interface + from zope.interface import directlyProvides + class IContext(Interface): + pass + from pyramid.interfaces import IRequest + from pyramid.interfaces import IViewClassifier + context = DummyContext() + directlyProvides(context, IContext) + self._registerTraverserFactory(context) + self._registerRootFactory(context) + response = DummyResponse() + response.app_iter = ['Hello world'] + view = DummyView(response) + environ = self._makeEnviron() + self._registerView(view, '', IViewClassifier, IRequest, IContext) + router = self._makeOne() + start_response = DummyStartResponse() + result = router(environ, start_response) + self.assertEqual(result, ['Hello world']) + self.assertEqual(start_response.headers, ()) + self.assertEqual(start_response.status, '200 OK') + request = view.request + self.assertEqual(request.view_name, '') + self.assertEqual(request.subpath, []) + self.assertEqual(request.context, context) + self.assertEqual(request.root, context) + + def test_call_view_registered_specific_fail(self): + from zope.interface import Interface + from zope.interface import directlyProvides + from pyramid.httpexceptions import HTTPNotFound + from pyramid.interfaces import IViewClassifier + class IContext(Interface): + pass + class INotContext(Interface): + pass + from pyramid.interfaces import IRequest + context = DummyContext() + directlyProvides(context, INotContext) + self._registerTraverserFactory(context, subpath=['']) + response = DummyResponse() + view = DummyView(response) + environ = self._makeEnviron() + self._registerView(view, '', IViewClassifier, IRequest, IContext) + router = self._makeOne() + start_response = DummyStartResponse() + self.assertRaises(HTTPNotFound, router, environ, start_response) + + def test_call_view_raises_forbidden(self): + from zope.interface import Interface + from zope.interface import directlyProvides + from pyramid.httpexceptions import HTTPForbidden + class IContext(Interface): + pass + from pyramid.interfaces import IRequest + from pyramid.interfaces import IViewClassifier + context = DummyContext() + directlyProvides(context, IContext) + self._registerTraverserFactory(context, subpath=['']) + response = DummyResponse() + view = DummyView(response, + raise_exception=HTTPForbidden("unauthorized")) + environ = self._makeEnviron() + self._registerView(view, '', IViewClassifier, IRequest, IContext) + router = self._makeOne() + start_response = DummyStartResponse() + why = exc_raised(HTTPForbidden, router, environ, start_response) + self.assertEqual(why.args[0], 'unauthorized') + + def test_call_view_raises_notfound(self): + from zope.interface import Interface + from zope.interface import directlyProvides + class IContext(Interface): + pass + from pyramid.interfaces import IRequest + from pyramid.interfaces import IViewClassifier + from pyramid.httpexceptions import HTTPNotFound + context = DummyContext() + directlyProvides(context, IContext) + self._registerTraverserFactory(context, subpath=['']) + response = DummyResponse() + view = DummyView(response, raise_exception=HTTPNotFound("notfound")) + environ = self._makeEnviron() + self._registerView(view, '', IViewClassifier, IRequest, IContext) + router = self._makeOne() + start_response = DummyStartResponse() + why = exc_raised(HTTPNotFound, router, environ, start_response) + self.assertEqual(why.args[0], 'notfound') + + def test_call_view_raises_response_cleared(self): + from zope.interface import Interface + from zope.interface import directlyProvides + from pyramid.interfaces import IExceptionViewClassifier + class IContext(Interface): + pass + from pyramid.interfaces import IRequest + from pyramid.interfaces import IViewClassifier + context = DummyContext() + directlyProvides(context, IContext) + self._registerTraverserFactory(context, subpath=['']) + def view(context, request): + request.response.a = 1 + raise KeyError + def exc_view(context, request): + self.assertFalse(hasattr(request.response, 'a')) + request.response.body = b'OK' + return request.response + environ = self._makeEnviron() + self._registerView(view, '', IViewClassifier, IRequest, IContext) + self._registerView(exc_view, '', IExceptionViewClassifier, + IRequest, KeyError) + router = self._makeOne() + start_response = DummyStartResponse() + itera = router(environ, start_response) + self.assertEqual(itera, [b'OK']) + + def test_call_request_has_response_callbacks(self): + from zope.interface import Interface + from zope.interface import directlyProvides + class IContext(Interface): + pass + from pyramid.interfaces import IRequest + from pyramid.interfaces import IViewClassifier + context = DummyContext() + directlyProvides(context, IContext) + self._registerTraverserFactory(context, subpath=['']) + response = DummyResponse('200 OK') + def view(context, request): + def callback(request, response): + response.called_back = True + request.add_response_callback(callback) + return response + environ = self._makeEnviron() + self._registerView(view, '', IViewClassifier, IRequest, IContext) + router = self._makeOne() + start_response = DummyStartResponse() + router(environ, start_response) + self.assertEqual(response.called_back, True) + + def test_call_request_has_finished_callbacks_when_view_succeeds(self): + from zope.interface import Interface + from zope.interface import directlyProvides + class IContext(Interface): + pass + from pyramid.interfaces import IRequest + from pyramid.interfaces import IViewClassifier + context = DummyContext() + directlyProvides(context, IContext) + self._registerTraverserFactory(context, subpath=['']) + response = DummyResponse('200 OK') + def view(context, request): + def callback(request): + request.environ['called_back'] = True + request.add_finished_callback(callback) + return response + environ = self._makeEnviron() + self._registerView(view, '', IViewClassifier, IRequest, IContext) + router = self._makeOne() + start_response = DummyStartResponse() + router(environ, start_response) + self.assertEqual(environ['called_back'], True) + + def test_call_request_has_finished_callbacks_when_view_raises(self): + from zope.interface import Interface + from zope.interface import directlyProvides + class IContext(Interface): + pass + from pyramid.interfaces import IRequest + from pyramid.interfaces import IViewClassifier + context = DummyContext() + directlyProvides(context, IContext) + self._registerTraverserFactory(context, subpath=['']) + def view(context, request): + def callback(request): + request.environ['called_back'] = True + request.add_finished_callback(callback) + raise NotImplementedError + environ = self._makeEnviron() + self._registerView(view, '', IViewClassifier, IRequest, IContext) + router = self._makeOne() + start_response = DummyStartResponse() + exc_raised(NotImplementedError, router, environ, start_response) + self.assertEqual(environ['called_back'], True) + + def test_call_request_factory_raises(self): + # making sure finally doesnt barf when a request cannot be created + environ = self._makeEnviron() + router = self._makeOne() + def dummy_request_factory(environ): + raise NotImplementedError + router.request_factory = dummy_request_factory + start_response = DummyStartResponse() + exc_raised(NotImplementedError, router, environ, start_response) + + def test_call_eventsends(self): + from pyramid.interfaces import INewRequest + from pyramid.interfaces import INewResponse + from pyramid.interfaces import IBeforeTraversal + from pyramid.interfaces import IContextFound + from pyramid.interfaces import IViewClassifier + context = DummyContext() + self._registerTraverserFactory(context) + response = DummyResponse() + response.app_iter = ['Hello world'] + view = DummyView(response) + environ = self._makeEnviron() + self._registerView(view, '', IViewClassifier, None, None) + request_events = self._registerEventListener(INewRequest) + beforetraversal_events = self._registerEventListener(IBeforeTraversal) + context_found_events = self._registerEventListener(IContextFound) + response_events = self._registerEventListener(INewResponse) + router = self._makeOne() + start_response = DummyStartResponse() + result = router(environ, start_response) + self.assertEqual(len(request_events), 1) + self.assertEqual(request_events[0].request.environ, environ) + self.assertEqual(len(beforetraversal_events), 1) + self.assertEqual(beforetraversal_events[0].request.environ, environ) + self.assertEqual(len(context_found_events), 1) + self.assertEqual(context_found_events[0].request.environ, environ) + self.assertEqual(context_found_events[0].request.context, context) + self.assertEqual(len(response_events), 1) + self.assertEqual(response_events[0].response, response) + self.assertEqual(response_events[0].request.context, context) + self.assertEqual(result, response.app_iter) + + def test_call_newrequest_evllist_exc_can_be_caught_by_exceptionview(self): + from pyramid.interfaces import INewRequest + from pyramid.interfaces import IExceptionViewClassifier + from pyramid.interfaces import IRequest + context = DummyContext() + self._registerTraverserFactory(context) + environ = self._makeEnviron() + def listener(event): + raise KeyError + self.registry.registerHandler(listener, (INewRequest,)) + exception_response = DummyResponse() + exception_response.app_iter = ["Hello, world"] + exception_view = DummyView(exception_response) + environ = self._makeEnviron() + self._registerView(exception_view, '', IExceptionViewClassifier, + IRequest, KeyError) + router = self._makeOne() + start_response = DummyStartResponse() + result = router(environ, start_response) + self.assertEqual(result, exception_response.app_iter) + + def test_call_route_matches_and_has_factory(self): + from pyramid.interfaces import IViewClassifier + logger = self._registerLogger() + self._registerSettings(debug_routematch=True) + self._registerRouteRequest('foo') + root = object() + def factory(request): + return root + route = self._connectRoute('foo', 'archives/:action/:article', factory) + route.predicates = [DummyPredicate()] + context = DummyContext() + self._registerTraverserFactory(context) + response = DummyResponse() + response.app_iter = ['Hello world'] + view = DummyView(response) + environ = self._makeEnviron(PATH_INFO='/archives/action1/article1') + self._registerView(view, '', IViewClassifier, None, None) + self._registerRootFactory(context) + router = self._makeOne() + start_response = DummyStartResponse() + result = router(environ, start_response) + self.assertEqual(result, ['Hello world']) + self.assertEqual(start_response.headers, ()) + self.assertEqual(start_response.status, '200 OK') + request = view.request + self.assertEqual(request.view_name, '') + self.assertEqual(request.subpath, []) + self.assertEqual(request.context, context) + self.assertEqual(request.root, root) + matchdict = {'action':'action1', 'article':'article1'} + self.assertEqual(request.matchdict, matchdict) + self.assertEqual(request.matched_route.name, 'foo') + self.assertEqual(len(logger.messages), 1) + self.assertTrue( + logger.messages[0].startswith( + "route matched for url http://localhost:8080" + "/archives/action1/article1; " + "route_name: 'foo', " + "path_info: ") + ) + self.assertTrue( + "predicates: 'predicate'" in logger.messages[0] + ) + + def test_call_route_match_miss_debug_routematch(self): + from pyramid.httpexceptions import HTTPNotFound + logger = self._registerLogger() + self._registerSettings(debug_routematch=True) + self._registerRouteRequest('foo') + self._connectRoute('foo', 'archives/:action/:article') + context = DummyContext() + self._registerTraverserFactory(context) + environ = self._makeEnviron(PATH_INFO='/wontmatch') + self._registerRootFactory(context) + router = self._makeOne() + start_response = DummyStartResponse() + self.assertRaises(HTTPNotFound, router, environ, start_response) + + self.assertEqual(len(logger.messages), 1) + self.assertEqual( + logger.messages[0], + 'no route matched for url http://localhost:8080/wontmatch') + + def test_call_route_matches_doesnt_overwrite_subscriber_iface(self): + from pyramid.interfaces import INewRequest + from pyramid.interfaces import IViewClassifier + from zope.interface import alsoProvides + from zope.interface import Interface + self._registerRouteRequest('foo') + class IFoo(Interface): + pass + def listener(event): + alsoProvides(event.request, IFoo) + self.registry.registerHandler(listener, (INewRequest,)) + root = object() + def factory(request): + return root + self._connectRoute('foo', 'archives/:action/:article', factory) + context = DummyContext() + self._registerTraverserFactory(context) + response = DummyResponse() + response.app_iter = ['Hello world'] + view = DummyView(response) + environ = self._makeEnviron(PATH_INFO='/archives/action1/article1') + self._registerView(view, '', IViewClassifier, None, None) + self._registerRootFactory(context) + router = self._makeOne() + start_response = DummyStartResponse() + result = router(environ, start_response) + self.assertEqual(result, ['Hello world']) + self.assertEqual(start_response.headers, ()) + self.assertEqual(start_response.status, '200 OK') + request = view.request + self.assertEqual(request.view_name, '') + self.assertEqual(request.subpath, []) + self.assertEqual(request.context, context) + self.assertEqual(request.root, root) + matchdict = {'action':'action1', 'article':'article1'} + self.assertEqual(request.matchdict, matchdict) + self.assertEqual(request.matched_route.name, 'foo') + self.assertTrue(IFoo.providedBy(request)) + + def test_root_factory_raises_notfound(self): + from pyramid.interfaces import IRootFactory + from pyramid.httpexceptions import HTTPNotFound + from zope.interface import Interface + from zope.interface import directlyProvides + def rootfactory(request): + raise HTTPNotFound('from root factory') + self.registry.registerUtility(rootfactory, IRootFactory) + class IContext(Interface): + pass + context = DummyContext() + directlyProvides(context, IContext) + environ = self._makeEnviron() + router = self._makeOne() + start_response = DummyStartResponse() + why = exc_raised(HTTPNotFound, router, environ, start_response) + self.assertTrue('from root factory' in why.args[0]) + + def test_root_factory_raises_forbidden(self): + from pyramid.interfaces import IRootFactory + from pyramid.httpexceptions import HTTPForbidden + from zope.interface import Interface + from zope.interface import directlyProvides + def rootfactory(request): + raise HTTPForbidden('from root factory') + self.registry.registerUtility(rootfactory, IRootFactory) + class IContext(Interface): + pass + context = DummyContext() + directlyProvides(context, IContext) + environ = self._makeEnviron() + router = self._makeOne() + start_response = DummyStartResponse() + why = exc_raised(HTTPForbidden, router, environ, start_response) + self.assertTrue('from root factory' in why.args[0]) + + def test_root_factory_exception_propagating(self): + from pyramid.interfaces import IRootFactory + from zope.interface import Interface + from zope.interface import directlyProvides + def rootfactory(request): + raise RuntimeError() + self.registry.registerUtility(rootfactory, IRootFactory) + class IContext(Interface): + pass + context = DummyContext() + directlyProvides(context, IContext) + environ = self._makeEnviron() + router = self._makeOne() + start_response = DummyStartResponse() + self.assertRaises(RuntimeError, router, environ, start_response) + + def test_traverser_exception_propagating(self): + environ = self._makeEnviron() + context = DummyContext() + self._registerTraverserFactory(context, raise_error=RuntimeError()) + router = self._makeOne() + start_response = DummyStartResponse() + self.assertRaises(RuntimeError, router, environ, start_response) + + def test_call_view_exception_propagating(self): + from zope.interface import Interface + from zope.interface import directlyProvides + class IContext(Interface): + pass + from pyramid.interfaces import IRequest + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IRequestFactory + from pyramid.interfaces import IExceptionViewClassifier + def rfactory(environ): + return request + self.registry.registerUtility(rfactory, IRequestFactory) + from pyramid.request import Request + request = Request.blank('/') + context = DummyContext() + directlyProvides(context, IContext) + self._registerTraverserFactory(context, subpath=['']) + response = DummyResponse() + response.app_iter = ['OK'] + error = RuntimeError() + view = DummyView(response, raise_exception=error) + environ = self._makeEnviron() + def exception_view(context, request): + self.assertEqual(request.exc_info[0], RuntimeError) + return response + self._registerView(view, '', IViewClassifier, IRequest, IContext) + self._registerView(exception_view, '', IExceptionViewClassifier, + IRequest, RuntimeError) + router = self._makeOne() + start_response = DummyStartResponse() + result = router(environ, start_response) + self.assertEqual(result, ['OK']) + # exc_info and exception should still be around on the request after + # the excview tween has run (see + # https://github.com/Pylons/pyramid/issues/1223) + self.assertEqual(request.exception, error) + self.assertEqual(request.exc_info[:2], (RuntimeError, error,)) + + def test_call_view_raises_exception_view(self): + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IExceptionViewClassifier + from pyramid.interfaces import IRequest + response = DummyResponse() + exception_response = DummyResponse() + exception_response.app_iter = ["Hello, world"] + view = DummyView(response, raise_exception=RuntimeError) + def exception_view(context, request): + self.assertEqual(request.exception.__class__, RuntimeError) + return exception_response + environ = self._makeEnviron() + self._registerView(view, '', IViewClassifier, IRequest, None) + self._registerView(exception_view, '', IExceptionViewClassifier, + IRequest, RuntimeError) + router = self._makeOne() + start_response = DummyStartResponse() + result = router(environ, start_response) + self.assertEqual(result, ["Hello, world"]) + + def test_call_view_raises_super_exception_sub_exception_view(self): + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IExceptionViewClassifier + from pyramid.interfaces import IRequest + class SuperException(Exception): + pass + class SubException(SuperException): + pass + response = DummyResponse() + exception_response = DummyResponse() + exception_response.app_iter = ["Hello, world"] + view = DummyView(response, raise_exception=SuperException) + exception_view = DummyView(exception_response) + environ = self._makeEnviron() + self._registerView(view, '', IViewClassifier, IRequest, None) + self._registerView(exception_view, '', IExceptionViewClassifier, + IRequest, SubException) + router = self._makeOne() + start_response = DummyStartResponse() + self.assertRaises(SuperException, router, environ, start_response) + + def test_call_view_raises_sub_exception_super_exception_view(self): + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IExceptionViewClassifier + from pyramid.interfaces import IRequest + class SuperException(Exception): + pass + class SubException(SuperException): + pass + response = DummyResponse() + exception_response = DummyResponse() + exception_response.app_iter = ["Hello, world"] + view = DummyView(response, raise_exception=SubException) + exception_view = DummyView(exception_response) + environ = self._makeEnviron() + self._registerView(view, '', IViewClassifier, IRequest, None) + self._registerView(exception_view, '', IExceptionViewClassifier, + IRequest, SuperException) + router = self._makeOne() + start_response = DummyStartResponse() + result = router(environ, start_response) + self.assertEqual(result, ["Hello, world"]) + + def test_call_view_raises_exception_another_exception_view(self): + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IExceptionViewClassifier + from pyramid.interfaces import IRequest + class MyException(Exception): + pass + class AnotherException(Exception): + pass + response = DummyResponse() + exception_response = DummyResponse() + exception_response.app_iter = ["Hello, world"] + view = DummyView(response, raise_exception=MyException) + exception_view = DummyView(exception_response) + environ = self._makeEnviron() + self._registerView(view, '', IViewClassifier, IRequest, None) + self._registerView(exception_view, '', IExceptionViewClassifier, + IRequest, AnotherException) + router = self._makeOne() + start_response = DummyStartResponse() + self.assertRaises(MyException, router, environ, start_response) + + def test_root_factory_raises_exception_view(self): + from pyramid.interfaces import IRootFactory + from pyramid.interfaces import IRequest + from pyramid.interfaces import IExceptionViewClassifier + def rootfactory(request): + raise RuntimeError() + self.registry.registerUtility(rootfactory, IRootFactory) + exception_response = DummyResponse() + exception_response.app_iter = ["Hello, world"] + exception_view = DummyView(exception_response) + self._registerView(exception_view, '', IExceptionViewClassifier, + IRequest, RuntimeError) + environ = self._makeEnviron() + router = self._makeOne() + start_response = DummyStartResponse() + app_iter = router(environ, start_response) + self.assertEqual(app_iter, ["Hello, world"]) + + def test_traverser_raises_exception_view(self): + from pyramid.interfaces import IRequest + from pyramid.interfaces import IExceptionViewClassifier + environ = self._makeEnviron() + context = DummyContext() + self._registerTraverserFactory(context, raise_error=RuntimeError()) + exception_response = DummyResponse() + exception_response.app_iter = ["Hello, world"] + exception_view = DummyView(exception_response) + self._registerView(exception_view, '', IExceptionViewClassifier, + IRequest, RuntimeError) + router = self._makeOne() + start_response = DummyStartResponse() + result = router(environ, start_response) + self.assertEqual(result, ["Hello, world"]) + + def test_exception_view_returns_non_iresponse(self): + from pyramid.interfaces import IRequest + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IExceptionViewClassifier + environ = self._makeEnviron() + response = DummyResponse() + view = DummyView(response, raise_exception=RuntimeError) + + self._registerView(self.config.derive_view(view), '', + IViewClassifier, IRequest, None) + exception_view = DummyView(None) + self._registerView(self.config.derive_view(exception_view), '', + IExceptionViewClassifier, + IRequest, RuntimeError) + router = self._makeOne() + start_response = DummyStartResponse() + self.assertRaises(ValueError, router, environ, start_response) + + def test_call_route_raises_route_exception_view(self): + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IExceptionViewClassifier + req_iface = self._registerRouteRequest('foo') + self._connectRoute('foo', 'archives/:action/:article', None) + view = DummyView(DummyResponse(), raise_exception=RuntimeError) + self._registerView(view, '', IViewClassifier, req_iface, None) + response = DummyResponse() + response.app_iter = ["Hello, world"] + exception_view = DummyView(response) + self._registerView(exception_view, '', IExceptionViewClassifier, + req_iface, RuntimeError) + environ = self._makeEnviron(PATH_INFO='/archives/action1/article1') + start_response = DummyStartResponse() + router = self._makeOne() + result = router(environ, start_response) + self.assertEqual(result, ["Hello, world"]) + + def test_call_view_raises_exception_route_view(self): + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IExceptionViewClassifier + from pyramid.interfaces import IRequest + req_iface = self._registerRouteRequest('foo') + self._connectRoute('foo', 'archives/:action/:article', None) + view = DummyView(DummyResponse(), raise_exception=RuntimeError) + self._registerView(view, '', IViewClassifier, IRequest, None) + response = DummyResponse() + response.app_iter = ["Hello, world"] + exception_view = DummyView(response) + self._registerView(exception_view, '', IExceptionViewClassifier, + req_iface, RuntimeError) + environ = self._makeEnviron() + start_response = DummyStartResponse() + router = self._makeOne() + self.assertRaises(RuntimeError, router, environ, start_response) + + def test_call_route_raises_exception_view(self): + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IExceptionViewClassifier + from pyramid.interfaces import IRequest + req_iface = self._registerRouteRequest('foo') + self._connectRoute('foo', 'archives/:action/:article', None) + view = DummyView(DummyResponse(), raise_exception=RuntimeError) + self._registerView(view, '', IViewClassifier, req_iface, None) + response = DummyResponse() + response.app_iter = ["Hello, world"] + exception_view = DummyView(response) + self._registerView(exception_view, '', IExceptionViewClassifier, + IRequest, RuntimeError) + environ = self._makeEnviron(PATH_INFO='/archives/action1/article1') + start_response = DummyStartResponse() + router = self._makeOne() + result = router(environ, start_response) + self.assertEqual(result, ["Hello, world"]) + + def test_call_route_raises_super_exception_sub_exception_view(self): + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IExceptionViewClassifier + from pyramid.interfaces import IRequest + class SuperException(Exception): + pass + class SubException(SuperException): + pass + req_iface = self._registerRouteRequest('foo') + self._connectRoute('foo', 'archives/:action/:article', None) + view = DummyView(DummyResponse(), raise_exception=SuperException) + self._registerView(view, '', IViewClassifier, req_iface, None) + response = DummyResponse() + response.app_iter = ["Hello, world"] + exception_view = DummyView(response) + self._registerView(exception_view, '', IExceptionViewClassifier, + IRequest, SubException) + environ = self._makeEnviron(PATH_INFO='/archives/action1/article1') + start_response = DummyStartResponse() + router = self._makeOne() + self.assertRaises(SuperException, router, environ, start_response) + + def test_call_route_raises_sub_exception_super_exception_view(self): + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IExceptionViewClassifier + from pyramid.interfaces import IRequest + class SuperException(Exception): + pass + class SubException(SuperException): + pass + req_iface = self._registerRouteRequest('foo') + self._connectRoute('foo', 'archives/:action/:article', None) + view = DummyView(DummyResponse(), raise_exception=SubException) + self._registerView(view, '', IViewClassifier, req_iface, None) + response = DummyResponse() + response.app_iter = ["Hello, world"] + exception_view = DummyView(response) + self._registerView(exception_view, '', IExceptionViewClassifier, + IRequest, SuperException) + environ = self._makeEnviron(PATH_INFO='/archives/action1/article1') + start_response = DummyStartResponse() + router = self._makeOne() + result = router(environ, start_response) + self.assertEqual(result, ["Hello, world"]) + + def test_call_route_raises_exception_another_exception_view(self): + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IExceptionViewClassifier + from pyramid.interfaces import IRequest + class MyException(Exception): + pass + class AnotherException(Exception): + pass + req_iface = self._registerRouteRequest('foo') + self._connectRoute('foo', 'archives/:action/:article', None) + view = DummyView(DummyResponse(), raise_exception=MyException) + self._registerView(view, '', IViewClassifier, req_iface, None) + response = DummyResponse() + response.app_iter = ["Hello, world"] + exception_view = DummyView(response) + self._registerView(exception_view, '', IExceptionViewClassifier, + IRequest, AnotherException) + environ = self._makeEnviron(PATH_INFO='/archives/action1/article1') + start_response = DummyStartResponse() + router = self._makeOne() + self.assertRaises(MyException, router, environ, start_response) + + def test_call_route_raises_exception_view_specializing(self): + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IExceptionViewClassifier + from pyramid.interfaces import IRequest + req_iface = self._registerRouteRequest('foo') + self._connectRoute('foo', 'archives/:action/:article', None) + view = DummyView(DummyResponse(), raise_exception=RuntimeError) + self._registerView(view, '', IViewClassifier, req_iface, None) + response = DummyResponse() + response.app_iter = ["Hello, world"] + exception_view = DummyView(response) + self._registerView(exception_view, '', IExceptionViewClassifier, + IRequest, RuntimeError) + response_spec = DummyResponse() + response_spec.app_iter = ["Hello, special world"] + exception_view_spec = DummyView(response_spec) + self._registerView(exception_view_spec, '', IExceptionViewClassifier, + req_iface, RuntimeError) + environ = self._makeEnviron(PATH_INFO='/archives/action1/article1') + start_response = DummyStartResponse() + router = self._makeOne() + result = router(environ, start_response) + self.assertEqual(result, ["Hello, special world"]) + + def test_call_route_raises_exception_view_another_route(self): + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IExceptionViewClassifier + req_iface = self._registerRouteRequest('foo') + another_req_iface = self._registerRouteRequest('bar') + self._connectRoute('foo', 'archives/:action/:article', None) + view = DummyView(DummyResponse(), raise_exception=RuntimeError) + self._registerView(view, '', IViewClassifier, req_iface, None) + response = DummyResponse() + response.app_iter = ["Hello, world"] + exception_view = DummyView(response) + self._registerView(exception_view, '', IExceptionViewClassifier, + another_req_iface, RuntimeError) + environ = self._makeEnviron(PATH_INFO='/archives/action1/article1') + start_response = DummyStartResponse() + router = self._makeOne() + self.assertRaises(RuntimeError, router, environ, start_response) + + def test_call_view_raises_exception_view_route(self): + from pyramid.interfaces import IRequest + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IExceptionViewClassifier + req_iface = self._registerRouteRequest('foo') + response = DummyResponse() + exception_response = DummyResponse() + exception_response.app_iter = ["Hello, world"] + view = DummyView(response, raise_exception=RuntimeError) + exception_view = DummyView(exception_response) + environ = self._makeEnviron() + self._registerView(view, '', IViewClassifier, IRequest, None) + self._registerView(exception_view, '', IExceptionViewClassifier, + req_iface, RuntimeError) + router = self._makeOne() + start_response = DummyStartResponse() + self.assertRaises(RuntimeError, router, environ, start_response) + + def test_call_view_raises_predicate_mismatch(self): + from pyramid.exceptions import PredicateMismatch + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IRequest + view = DummyView(DummyResponse(), raise_exception=PredicateMismatch) + self._registerView(view, '', IViewClassifier, IRequest, None) + environ = self._makeEnviron() + router = self._makeOne() + start_response = DummyStartResponse() + self.assertRaises(PredicateMismatch, router, environ, start_response) + + def test_call_view_predicate_mismatch_doesnt_hide_views(self): + from pyramid.exceptions import PredicateMismatch + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IRequest, IResponse + from pyramid.response import Response + class BaseContext: + pass + class DummyContext(BaseContext): + pass + context = DummyContext() + self._registerTraverserFactory(context) + view = DummyView(DummyResponse(), raise_exception=PredicateMismatch) + self._registerView(view, '', IViewClassifier, IRequest, + DummyContext) + good_view = DummyView('abc') + self._registerView(self.config.derive_view(good_view), + '', IViewClassifier, IRequest, BaseContext) + router = self._makeOne() + def make_response(s): + return Response(s) + router.registry.registerAdapter(make_response, (str,), IResponse) + environ = self._makeEnviron() + start_response = DummyStartResponse() + app_iter = router(environ, start_response) + self.assertEqual(app_iter, [b'abc']) + + def test_call_view_multiple_predicate_mismatches_dont_hide_views(self): + from pyramid.exceptions import PredicateMismatch + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IRequest, IResponse + from pyramid.response import Response + from zope.interface import Interface, implementer + class IBaseContext(Interface): + pass + class IContext(IBaseContext): + pass + @implementer(IContext) + class DummyContext: + pass + context = DummyContext() + self._registerTraverserFactory(context) + view1 = DummyView(DummyResponse(), raise_exception=PredicateMismatch) + self._registerView(view1, '', IViewClassifier, IRequest, + DummyContext) + view2 = DummyView(DummyResponse(), raise_exception=PredicateMismatch) + self._registerView(view2, '', IViewClassifier, IRequest, + IContext) + good_view = DummyView('abc') + self._registerView(self.config.derive_view(good_view), + '', IViewClassifier, IRequest, IBaseContext) + router = self._makeOne() + def make_response(s): + return Response(s) + router.registry.registerAdapter(make_response, (str,), IResponse) + environ = self._makeEnviron() + start_response = DummyStartResponse() + app_iter = router(environ, start_response) + self.assertEqual(app_iter, [b'abc']) + + def test_call_view_predicate_mismatch_doesnt_find_unrelated_views(self): + from pyramid.exceptions import PredicateMismatch + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IRequest + from zope.interface import Interface, implementer + class IContext(Interface): + pass + class IOtherContext(Interface): + pass + @implementer(IContext) + class DummyContext: + pass + context = DummyContext() + self._registerTraverserFactory(context) + view = DummyView(DummyResponse(), raise_exception=PredicateMismatch) + self._registerView(view, '', IViewClassifier, IRequest, + DummyContext) + please_dont_call_me_view = DummyView('abc') + self._registerView(self.config.derive_view(please_dont_call_me_view), + '', IViewClassifier, IRequest, IOtherContext) + router = self._makeOne() + environ = self._makeEnviron() + router = self._makeOne() + start_response = DummyStartResponse() + self.assertRaises(PredicateMismatch, router, environ, start_response) + + def test_custom_execution_policy(self): + from pyramid.interfaces import IExecutionPolicy + from pyramid.request import Request + from pyramid.response import Response + registry = self.config.registry + def dummy_policy(environ, router): + return Response(status=200, body=b'foo') + registry.registerUtility(dummy_policy, IExecutionPolicy) + router = self._makeOne() + resp = Request.blank('/').get_response(router) + self.assertEqual(resp.status_code, 200) + self.assertEqual(resp.body, b'foo') + + def test_execution_policy_handles_exception(self): + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IExceptionViewClassifier + from pyramid.interfaces import IRequest + class Exception1(Exception): + pass + class Exception2(Exception): + pass + req_iface = self._registerRouteRequest('foo') + self._connectRoute('foo', 'archives/:action/:article', None) + view = DummyView(DummyResponse(), raise_exception=Exception1) + self._registerView(view, '', IViewClassifier, req_iface, None) + exception_view1 = DummyView(DummyResponse(), + raise_exception=Exception2) + self._registerView(exception_view1, '', IExceptionViewClassifier, + IRequest, Exception1) + response = DummyResponse() + response.app_iter = ["Hello, world"] + exception_view2 = DummyView(response) + self._registerView(exception_view2, '', IExceptionViewClassifier, + IRequest, Exception2) + environ = self._makeEnviron(PATH_INFO='/archives/action1/article1') + start_response = DummyStartResponse() + router = self._makeOne() + result = router(environ, start_response) + self.assertEqual(result, ["Hello, world"]) + + def test_request_context_with_statement(self): + from pyramid.threadlocal import get_current_request + from pyramid.interfaces import IExecutionPolicy + from pyramid.request import Request + from pyramid.response import Response + registry = self.config.registry + result = [] + def dummy_policy(environ, router): + with router.request_context(environ): + result.append(get_current_request()) + result.append(get_current_request()) + return Response(status=200, body=b'foo') + registry.registerUtility(dummy_policy, IExecutionPolicy) + router = self._makeOne() + resp = Request.blank('/test_path').get_response(router) + self.assertEqual(resp.status_code, 200) + self.assertEqual(resp.body, b'foo') + self.assertEqual(result[0].path_info, '/test_path') + self.assertEqual(result[1], None) + + def test_request_context_manually(self): + from pyramid.threadlocal import get_current_request + from pyramid.interfaces import IExecutionPolicy + from pyramid.request import Request + from pyramid.response import Response + registry = self.config.registry + result = [] + def dummy_policy(environ, router): + ctx = router.request_context(environ) + ctx.begin() + result.append(get_current_request()) + ctx.end() + result.append(get_current_request()) + return Response(status=200, body=b'foo') + registry.registerUtility(dummy_policy, IExecutionPolicy) + router = self._makeOne() + resp = Request.blank('/test_path').get_response(router) + self.assertEqual(resp.status_code, 200) + self.assertEqual(resp.body, b'foo') + self.assertEqual(result[0].path_info, '/test_path') + self.assertEqual(result[1], None) + +class DummyPredicate(object): + def __call__(self, info, request): + return True + def text(self): + return 'predicate' + +class DummyContext: + pass + +class DummyView: + def __init__(self, response, raise_exception=None): + self.response = response + self.raise_exception = raise_exception + + def __call__(self, context, request): + self.context = context + self.request = request + if self.raise_exception is not None: + raise self.raise_exception + return self.response + +class DummyRootFactory: + def __init__(self, root): + self.root = root + + def __call__(self, environ): + return self.root + +class DummyStartResponse: + status = () + headers = () + def __call__(self, status, headers): + self.status = status + self.headers = headers + +from pyramid.interfaces import IResponse +from zope.interface import implementer + +@implementer(IResponse) +class DummyResponse(object): + headerlist = () + app_iter = () + environ = None + def __init__(self, status='200 OK'): + self.status = status + + def __call__(self, environ, start_response): + self.environ = environ + start_response(self.status, self.headerlist) + return self.app_iter + +class DummyAuthenticationPolicy: + pass + +class DummyLogger: + def __init__(self): + self.messages = [] + def info(self, msg): + self.messages.append(msg) + warn = info + debug = info + +def exc_raised(exc, func, *arg, **kw): + try: + func(*arg, **kw) + except exc as e: + return e + else: + raise AssertionError('%s not raised' % exc) # pragma: no cover + + diff --git a/src/pyramid/tests/test_scaffolds/__init__.py b/src/pyramid/tests/test_scaffolds/__init__.py new file mode 100644 index 000000000..5bb534f79 --- /dev/null +++ b/src/pyramid/tests/test_scaffolds/__init__.py @@ -0,0 +1 @@ +# package diff --git a/src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/.badfile b/src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/.badfile new file mode 100644 index 000000000..e69de29bb diff --git a/src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/__init__.py_tmpl b/src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/__init__.py_tmpl new file mode 100644 index 000000000..d763b2435 --- /dev/null +++ b/src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/__init__.py_tmpl @@ -0,0 +1,12 @@ +from pyramid.config import Configurator +from {{package}}.resources import Root + +def main(global_config, **settings): + """ This function returns a Pyramid WSGI application. + """ + config = Configurator(root_factory=Root, settings=settings) + config.add_view('{{package}}.views.my_view', + context='{{package}}:resources.Root', + renderer='{{package}}:templates/mytemplate.pt') + config.add_static_view('static', '{{package}}:static', cache_max_age=3600) + return config.make_wsgi_app() diff --git a/src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/resources.py b/src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/resources.py new file mode 100644 index 000000000..3d811895c --- /dev/null +++ b/src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/resources.py @@ -0,0 +1,3 @@ +class Root(object): + def __init__(self, request): + self.request = request diff --git a/src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/static/favicon.ico b/src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/static/favicon.ico new file mode 100644 index 000000000..71f837c9e Binary files /dev/null and b/src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/static/favicon.ico differ diff --git a/src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/static/footerbg.png b/src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/static/footerbg.png new file mode 100644 index 000000000..1fbc873da Binary files /dev/null and b/src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/static/footerbg.png differ diff --git a/src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/static/headerbg.png b/src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/static/headerbg.png new file mode 100644 index 000000000..0596f2020 Binary files /dev/null and b/src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/static/headerbg.png differ diff --git a/src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/static/ie6.css b/src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/static/ie6.css new file mode 100644 index 000000000..b7c8493d8 --- /dev/null +++ b/src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/static/ie6.css @@ -0,0 +1,8 @@ +* html img, +* html .png{position:relative;behavior:expression((this.runtimeStyle.behavior="none")&&(this.pngSet?this.pngSet=true:(this.nodeName == "IMG" && this.src.toLowerCase().indexOf('.png')>-1?(this.runtimeStyle.backgroundImage = "none", +this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.src + "',sizingMethod='image')", +this.src = "static/transparent.gif"):(this.origBg = this.origBg? this.origBg :this.currentStyle.backgroundImage.toString().replace('url("','').replace('")',''), +this.runtimeStyle.filter = "progid:DXImageTransform.Microsoft.AlphaImageLoader(src='" + this.origBg + "',sizingMethod='crop')", +this.runtimeStyle.backgroundImage = "none")),this.pngSet=true) +);} +#wrap{display:table;height:100%} diff --git a/src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/static/middlebg.png b/src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/static/middlebg.png new file mode 100644 index 000000000..2369cfb7d Binary files /dev/null and b/src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/static/middlebg.png differ diff --git a/src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/static/pylons.css b/src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/static/pylons.css new file mode 100644 index 000000000..c54499ddd --- /dev/null +++ b/src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/static/pylons.css @@ -0,0 +1,65 @@ +html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,font,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td{margin:0;padding:0;border:0;outline:0;font-size:100%;/* 16px */ +vertical-align:baseline;background:transparent;} +body{line-height:1;} +ol,ul{list-style:none;} +blockquote,q{quotes:none;} +blockquote:before,blockquote:after,q:before,q:after{content:'';content:none;} +:focus{outline:0;} +ins{text-decoration:none;} +del{text-decoration:line-through;} +table{border-collapse:collapse;border-spacing:0;} +sub{vertical-align:sub;font-size:smaller;line-height:normal;} +sup{vertical-align:super;font-size:smaller;line-height:normal;} +ul,menu,dir{display:block;list-style-type:disc;margin:1em 0;padding-left:40px;} +ol{display:block;list-style-type:decimal-leading-zero;margin:1em 0;padding-left:40px;} +li{display:list-item;} +ul ul,ul ol,ul dir,ul menu,ul dl,ol ul,ol ol,ol dir,ol menu,ol dl,dir ul,dir ol,dir dir,dir menu,dir dl,menu ul,menu ol,menu dir,menu menu,menu dl,dl ul,dl ol,dl dir,dl menu,dl dl{margin-top:0;margin-bottom:0;} +ol ul,ul ul,menu ul,dir ul,ol menu,ul menu,menu menu,dir menu,ol dir,ul dir,menu dir,dir dir{list-style-type:circle;} +ol ol ul,ol ul ul,ol menu ul,ol dir ul,ol ol menu,ol ul menu,ol menu menu,ol dir menu,ol ol dir,ol ul dir,ol menu dir,ol dir dir,ul ol ul,ul ul ul,ul menu ul,ul dir ul,ul ol menu,ul ul menu,ul menu menu,ul dir menu,ul ol dir,ul ul dir,ul menu dir,ul dir dir,menu ol ul,menu ul ul,menu menu ul,menu dir ul,menu ol menu,menu ul menu,menu menu menu,menu dir menu,menu ol dir,menu ul dir,menu menu dir,menu dir dir,dir ol ul,dir ul ul,dir menu ul,dir dir ul,dir ol menu,dir ul menu,dir menu menu,dir dir menu,dir ol dir,dir ul dir,dir menu dir,dir dir dir{list-style-type:square;} +.hidden{display:none;} +p{line-height:1.5em;} +h1{font-size:1.75em;line-height:1.7em;font-family:helvetica,verdana;} +h2{font-size:1.5em;line-height:1.7em;font-family:helvetica,verdana;} +h3{font-size:1.25em;line-height:1.7em;font-family:helvetica,verdana;} +h4{font-size:1em;line-height:1.7em;font-family:helvetica,verdana;} +html,body{width:100%;height:100%;} +body{margin:0;padding:0;background-color:#ffffff;position:relative;font:16px/24px "NobileRegular","Lucida Grande",Lucida,Verdana,sans-serif;} +a{color:#1b61d6;text-decoration:none;} +a:hover{color:#e88f00;text-decoration:underline;} +body h1, +body h2, +body h3, +body h4, +body h5, +body h6{font-family:"NeutonRegular","Lucida Grande",Lucida,Verdana,sans-serif;font-weight:normal;color:#373839;font-style:normal;} +#wrap{min-height:100%;} +#header,#footer{width:100%;color:#ffffff;height:40px;position:absolute;text-align:center;line-height:40px;overflow:hidden;font-size:12px;vertical-align:middle;} +#header{background:#000000;top:0;font-size:14px;} +#footer{bottom:0;background:#000000 url(footerbg.png) repeat-x 0 top;position:relative;margin-top:-40px;clear:both;} +.header,.footer{width:750px;margin-right:auto;margin-left:auto;} +.wrapper{width:100%} +#top,#top-small,#bottom{width:100%;} +#top{color:#000000;height:230px;background:#ffffff url(headerbg.png) repeat-x 0 top;position:relative;} +#top-small{color:#000000;height:60px;background:#ffffff url(headerbg.png) repeat-x 0 top;position:relative;} +#bottom{color:#222;background-color:#ffffff;} +.top,.top-small,.middle,.bottom{width:750px;margin-right:auto;margin-left:auto;} +.top{padding-top:40px;} +.top-small{padding-top:10px;} +#middle{width:100%;height:100px;background:url(middlebg.png) repeat-x;border-top:2px solid #ffffff;border-bottom:2px solid #b2b2b2;} +.app-welcome{margin-top:25px;} +.app-name{color:#000000;font-weight:bold;} +.bottom{padding-top:50px;} +#left{width:350px;float:left;padding-right:25px;} +#right{width:350px;float:right;padding-left:25px;} +.align-left{text-align:left;} +.align-right{text-align:right;} +.align-center{text-align:center;} +ul.links{margin:0;padding:0;} +ul.links li{list-style-type:none;font-size:14px;} +form{border-style:none;} +fieldset{border-style:none;} +input{color:#222;border:1px solid #ccc;font-family:sans-serif;font-size:12px;line-height:16px;} +input[type=text],input[type=password]{width:205px;} +input[type=submit]{background-color:#ddd;font-weight:bold;} +/*Opera Fix*/ +body:before{content:"";height:100%;float:left;width:0;margin-top:-32767px;} diff --git a/src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/static/pyramid-small.png b/src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/static/pyramid-small.png new file mode 100644 index 000000000..a5bc0ade7 Binary files /dev/null and b/src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/static/pyramid-small.png differ diff --git a/src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/static/pyramid.png b/src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/static/pyramid.png new file mode 100644 index 000000000..347e05549 Binary files /dev/null and b/src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/static/pyramid.png differ diff --git a/src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/static/transparent.gif b/src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/static/transparent.gif new file mode 100644 index 000000000..0341802e5 Binary files /dev/null and b/src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/static/transparent.gif differ diff --git a/src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/templates/mytemplate.pt_tmpl b/src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/templates/mytemplate.pt_tmpl new file mode 100644 index 000000000..f4d98ec29 --- /dev/null +++ b/src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/templates/mytemplate.pt_tmpl @@ -0,0 +1,76 @@ + + + + The Pyramid Web Framework + + + + + + + + + + +
+
+
+
pyramid
+
+
+
+
+

+ Welcome to ${project}, an application generated by
+ the Pyramid Web Framework. +

+
+
+
+
+
+

Search documentation

+
+ + +
+
+ +
+
+
+ + + diff --git a/src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/test_no_content.py_tmpl b/src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/test_no_content.py_tmpl new file mode 100644 index 000000000..e69de29bb diff --git a/src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/tests.py_tmpl b/src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/tests.py_tmpl new file mode 100644 index 000000000..1627bf015 --- /dev/null +++ b/src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/tests.py_tmpl @@ -0,0 +1,16 @@ +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 {{package}}.views import my_view + request = testing.DummyRequest() + info = my_view(request) + self.assertEqual(info['project'], '{{project}}') diff --git a/src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/views.py_tmpl b/src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/views.py_tmpl new file mode 100644 index 000000000..12ed8832d --- /dev/null +++ b/src/pyramid/tests/test_scaffolds/fixture_scaffold/+package+/views.py_tmpl @@ -0,0 +1,2 @@ +def my_view(request): + return {'project':'{{project}}'} diff --git a/src/pyramid/tests/test_scaffolds/fixture_scaffold/CHANGES.txt_tmpl b/src/pyramid/tests/test_scaffolds/fixture_scaffold/CHANGES.txt_tmpl new file mode 100644 index 000000000..35a34f332 --- /dev/null +++ b/src/pyramid/tests/test_scaffolds/fixture_scaffold/CHANGES.txt_tmpl @@ -0,0 +1,4 @@ +0.0 +--- + +- Initial version diff --git a/src/pyramid/tests/test_scaffolds/fixture_scaffold/MANIFEST.in_tmpl b/src/pyramid/tests/test_scaffolds/fixture_scaffold/MANIFEST.in_tmpl new file mode 100644 index 000000000..0ff6eb7a0 --- /dev/null +++ b/src/pyramid/tests/test_scaffolds/fixture_scaffold/MANIFEST.in_tmpl @@ -0,0 +1,2 @@ +include *.txt *.ini *.cfg *.rst +recursive-include {{package}} *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml diff --git a/src/pyramid/tests/test_scaffolds/fixture_scaffold/README.txt_tmpl b/src/pyramid/tests/test_scaffolds/fixture_scaffold/README.txt_tmpl new file mode 100644 index 000000000..40f98d14a --- /dev/null +++ b/src/pyramid/tests/test_scaffolds/fixture_scaffold/README.txt_tmpl @@ -0,0 +1 @@ +{{project}} README diff --git a/src/pyramid/tests/test_scaffolds/fixture_scaffold/development.ini_tmpl b/src/pyramid/tests/test_scaffolds/fixture_scaffold/development.ini_tmpl new file mode 100644 index 000000000..01c504f99 --- /dev/null +++ b/src/pyramid/tests/test_scaffolds/fixture_scaffold/development.ini_tmpl @@ -0,0 +1,45 @@ +[app:main] +use = egg:{{project}} + +pyramid.reload_templates = true +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.debug_templates = true +pyramid.default_locale_name = en +pyramid.includes = pyramid_debugtoolbar + +[server:main] +use = egg:pyramid#wsgiref +listen = *:6543 + +# Begin logging configuration + +[loggers] +keys = root, {{package_logger}} + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = INFO +handlers = console + +[logger_{{package_logger}}] +level = DEBUG +handlers = +qualname = {{package}} + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s + +# End logging configuration diff --git a/src/pyramid/tests/test_scaffolds/fixture_scaffold/production.ini_tmpl b/src/pyramid/tests/test_scaffolds/fixture_scaffold/production.ini_tmpl new file mode 100644 index 000000000..becd3aa76 --- /dev/null +++ b/src/pyramid/tests/test_scaffolds/fixture_scaffold/production.ini_tmpl @@ -0,0 +1,44 @@ +[app:main] +use = egg:{{project}} + +pyramid.reload_templates = false +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.debug_templates = false +pyramid.default_locale_name = en + +[server:main] +use = egg:pyramid#wsgiref +listen = *:6543 + +# Begin logging configuration + +[loggers] +keys = root, {{package_logger}} + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console + +[logger_{{package_logger}}] +level = WARN +handlers = +qualname = {{package}} + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s + +# End logging configuration diff --git a/src/pyramid/tests/test_scaffolds/fixture_scaffold/setup.py_tmpl b/src/pyramid/tests/test_scaffolds/fixture_scaffold/setup.py_tmpl new file mode 100644 index 000000000..ee9fd5fda --- /dev/null +++ b/src/pyramid/tests/test_scaffolds/fixture_scaffold/setup.py_tmpl @@ -0,0 +1,38 @@ +import os + +from setuptools import setup, find_packages + +here = os.path.abspath(os.path.dirname(__file__)) +with open(os.path.join(here, 'README.txt')) as f: + README = f.read() +with open(os.path.join(here, 'CHANGES.txt')) as f: + CHANGES = f.read() + +requires = ['pyramid', 'pyramid_debugtoolbar'] + +setup(name='{{project}}', + version='0.0', + description='{{project}}', + long_description=README + '\n\n' + CHANGES, + classifiers=[ + "Programming Language :: Python", + "Framework :: Pylons", + "Topic :: Internet :: WWW/HTTP", + "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", + ], + author='', + author_email='', + url='', + keywords='web pyramid pylons', + packages=find_packages(), + include_package_data=True, + zip_safe=False, + install_requires=requires, + tests_require=requires, + test_suite="{{package}}", + entry_points = """\ + [paste.app_factory] + main = {{package}}:main + """, + ) + diff --git a/src/pyramid/tests/test_scaffolds/test_copydir.py b/src/pyramid/tests/test_scaffolds/test_copydir.py new file mode 100644 index 000000000..1e92b3c36 --- /dev/null +++ b/src/pyramid/tests/test_scaffolds/test_copydir.py @@ -0,0 +1,455 @@ +# -*- coding: utf-8 -*- +import unittest +import os +import pkg_resources + +class Test_copy_dir(unittest.TestCase): + def setUp(self): + import tempfile + from pyramid.compat import NativeIO + self.dirname = tempfile.mkdtemp() + self.out = NativeIO() + self.fixturetuple = ('pyramid.tests.test_scaffolds', + 'fixture_scaffold') + + def tearDown(self): + import shutil + shutil.rmtree(self.dirname, ignore_errors=True) + self.out.close() + + def _callFUT(self, *arg, **kw): + kw['out_'] = self.out + from pyramid.scaffolds.copydir import copy_dir + return copy_dir(*arg, **kw) + + def test_copy_source_as_pkg_resource(self): + vars = {'package':'mypackage'} + self._callFUT(self.fixturetuple, + self.dirname, + vars, + 1, False, + template_renderer=dummy_template_renderer) + result = self.out.getvalue() + self.assertTrue('Creating' in result) + self.assertTrue( + 'Copying fixture_scaffold/+package+/__init__.py_tmpl to' in result) + source = pkg_resources.resource_filename( + 'pyramid.tests.test_scaffolds', + 'fixture_scaffold/+package+/__init__.py_tmpl') + target = os.path.join(self.dirname, 'mypackage', '__init__.py') + with open(target, 'r') as f: + tcontent = f.read() + with open(source, 'r') as f: + scontent = f.read() + self.assertEqual(scontent, tcontent) + + def test_copy_source_as_dirname(self): + vars = {'package':'mypackage'} + source = pkg_resources.resource_filename(*self.fixturetuple) + self._callFUT(source, + self.dirname, + vars, + 1, False, + template_renderer=dummy_template_renderer) + result = self.out.getvalue() + self.assertTrue('Creating' in result) + self.assertTrue('Copying __init__.py_tmpl to' in result) + source = pkg_resources.resource_filename( + 'pyramid.tests.test_scaffolds', + 'fixture_scaffold/+package+/__init__.py_tmpl') + target = os.path.join(self.dirname, 'mypackage', '__init__.py') + with open(target, 'r') as f: + tcontent = f.read() + with open(source, 'r') as f: + scontent = f.read() + self.assertEqual(scontent, tcontent) + + def test_content_is_same_message(self): + vars = {'package':'mypackage'} + source = pkg_resources.resource_filename(*self.fixturetuple) + self._callFUT(source, + self.dirname, + vars, + 2, False, + template_renderer=dummy_template_renderer) + self._callFUT(source, + self.dirname, + vars, + 2, False, + template_renderer=dummy_template_renderer) + result = self.out.getvalue() + self.assertTrue('%s already exists (same content)' % \ + os.path.join(self.dirname, 'mypackage', '__init__.py') in result) + + def test_direxists_message(self): + vars = {'package':'mypackage'} + source = pkg_resources.resource_filename(*self.fixturetuple) + # if not os.path.exists(self.dirname): + # os.mkdir(self.dirname) + self._callFUT(source, + self.dirname, + vars, + 2, False, + template_renderer=dummy_template_renderer) + result = self.out.getvalue() + self.assertTrue('Directory %s exists' % self.dirname in result, result) + + def test_overwrite_false(self): + vars = {'package':'mypackage'} + source = pkg_resources.resource_filename(*self.fixturetuple) + self._callFUT(source, + self.dirname, + vars, + 1, False, + overwrite=False, + template_renderer=dummy_template_renderer) + # toplevel file + toplevel = os.path.join(self.dirname, 'mypackage', '__init__.py') + with open(toplevel, 'w') as f: + f.write('These are the words you are looking for.') + # sub directory file + sub = os.path.join(self.dirname, 'mypackage', 'templates', 'mytemplate.pt') + with open(sub, 'w') as f: + f.write('These are the words you are looking for.') + self._callFUT(source, + self.dirname, + vars, + 1, False, + overwrite=False, + template_renderer=dummy_template_renderer) + with open(toplevel, 'r') as f: + tcontent = f.read() + self.assertEqual('These are the words you are looking for.', tcontent) + with open(sub, 'r') as f: + tcontent = f.read() + self.assertEqual('These are the words you are looking for.', tcontent) + + def test_overwrite_true(self): + vars = {'package':'mypackage'} + source = pkg_resources.resource_filename(*self.fixturetuple) + self._callFUT(source, + self.dirname, + vars, + 1, False, + overwrite=True, + template_renderer=dummy_template_renderer) + # toplevel file + toplevel = os.path.join(self.dirname, 'mypackage', '__init__.py') + with open(toplevel, 'w') as f: + f.write('These are not the words you are looking for.') + # sub directory file + sub = os.path.join(self.dirname, 'mypackage', 'templates', 'mytemplate.pt') + with open(sub, 'w') as f: + f.write('These are not the words you are looking for.') + self._callFUT(source, + self.dirname, + vars, + 1, False, + overwrite=True, + template_renderer=dummy_template_renderer) + with open(toplevel, 'r') as f: + tcontent = f.read() + self.assertNotEqual('These are not the words you are looking for.', tcontent) + with open(sub, 'r') as f: + tcontent = f.read() + self.assertNotEqual('These are not the words you are looking for.', tcontent) + + def test_detect_SkipTemplate(self): + vars = {'package':'mypackage'} + source = pkg_resources.resource_filename(*self.fixturetuple) + def dummy_template_renderer(*args, **kwargs): + from pyramid.scaffolds.copydir import SkipTemplate + raise SkipTemplate + self._callFUT(source, + self.dirname, + vars, + 1, False, + template_renderer=dummy_template_renderer) + + def test_query_interactive(self): + from pyramid.scaffolds import copydir + vars = {'package':'mypackage'} + source = pkg_resources.resource_filename(*self.fixturetuple) + self._callFUT(source, + self.dirname, + vars, + 1, False, + overwrite=False, + template_renderer=dummy_template_renderer) + target = os.path.join(self.dirname, 'mypackage', '__init__.py') + with open(target, 'w') as f: + f.write('These are not the words you are looking for.') + # We need query_interactive to return False in order to force + # execution of a branch + original_code_object = copydir.query_interactive + copydir.query_interactive = lambda *args, **kwargs: False + self._callFUT(source, + self.dirname, + vars, + 1, False, + interactive=True, + overwrite=False, + template_renderer=dummy_template_renderer) + copydir.query_interactive = original_code_object + +class Test_raise_SkipTemplate(unittest.TestCase): + + def _callFUT(self, *arg, **kw): + from pyramid.scaffolds.copydir import skip_template + return skip_template(*arg, **kw) + + def test_raise_SkipTemplate(self): + from pyramid.scaffolds.copydir import SkipTemplate + self.assertRaises(SkipTemplate, + self._callFUT, True, "exc-message") + +class Test_makedirs(unittest.TestCase): + + def _callFUT(self, *arg, **kw): + from pyramid.scaffolds.copydir import makedirs + return makedirs(*arg, **kw) + + def test_makedirs_parent_dir(self): + import shutil + import tempfile + tmpdir = tempfile.mkdtemp() + target = os.path.join(tmpdir, 'nonexistent_subdir') + self._callFUT(target, 2, None) + shutil.rmtree(tmpdir) + + def test_makedirs_no_parent_dir(self): + import shutil + import tempfile + tmpdir = tempfile.mkdtemp() + target = os.path.join(tmpdir, 'nonexistent_subdir', 'non2') + self._callFUT(target, 2, None) + shutil.rmtree(tmpdir) + +class Test_support_functions(unittest.TestCase): + + def _call_html_quote(self, *arg, **kw): + from pyramid.scaffolds.copydir import html_quote + return html_quote(*arg, **kw) + + def _call_url_quote(self, *arg, **kw): + from pyramid.scaffolds.copydir import url_quote + return url_quote(*arg, **kw) + + def _call_test(self, *arg, **kw): + from pyramid.scaffolds.copydir import test + return test(*arg, **kw) + + def test_html_quote(self): + import string + s = None + self.assertEqual(self._call_html_quote(s), '') + s = string.ascii_letters + self.assertEqual(self._call_html_quote(s), s) + s = "Λεμεσός" + self.assertEqual(self._call_url_quote(s), + "%CE%9B%CE%B5%CE%BC%CE%B5%CF%83%CF%8C%CF%82") + + def test_url_quote(self): + import string + s = None + self.assertEqual(self._call_url_quote(s), '') + s = string.ascii_letters + self.assertEqual(self._call_url_quote(s), s) + s = "Λεμεσός" + self.assertEqual(self._call_url_quote(s), + "%CE%9B%CE%B5%CE%BC%CE%B5%CF%83%CF%8C%CF%82") + + def test_test(self): + conf = True + true_cond = "faked" + self.assertEqual(self._call_test( + conf, true_cond, false_cond=None), "faked") + conf = False + self.assertEqual(self._call_test( + conf, true_cond, false_cond="alsofaked"), "alsofaked") + + +class Test_should_skip_file(unittest.TestCase): + + def _callFUT(self, *arg, **kw): + from pyramid.scaffolds.copydir import should_skip_file + return should_skip_file(*arg, **kw) + + def test_should_skip_dot_hidden_file(self): + self.assertEqual( + self._callFUT('.a_filename'), + 'Skipping hidden file %(filename)s') + + def test_should_skip_backup_file(self): + for name in ('a_filename~', 'a_filename.bak'): + self.assertEqual( + self._callFUT(name), + 'Skipping backup file %(filename)s') + + def test_should_skip_bytecompiled_file(self): + for name in ('afilename.pyc', 'afilename.pyo'): + extension = os.path.splitext(name)[1] + self.assertEqual( + self._callFUT(name), + 'Skipping %s file ' % extension + '%(filename)s') + + def test_should_skip_jython_class_file(self): + self.assertEqual( + self._callFUT('afilename$py.class'), + 'Skipping $py.class file %(filename)s') + + def test_should_skip_version_control_directory(self): + for name in ('CVS', '_darcs'): + self.assertEqual( + self._callFUT(name), + 'Skipping version control directory %(filename)s') + + def test_valid_file_is_not_skipped(self): + self.assertEqual( + self._callFUT('a_filename'), None) + +class RawInputMockObject( object ): + count = 0 + def __init__( self, fake_input ): + self.input= fake_input + self.count = 0 + def __call__( self, prompt ): + # Don't cycle endlessly. + self.count += 1 + if self.count > 1: + return 'y' + else: + return self.input + +class Test_query_interactive(unittest.TestCase): + + def setUp(self): + import tempfile + from pyramid.compat import NativeIO + self.dirname = tempfile.mkdtemp() + self.out = NativeIO() + self.fixturetuple = ('pyramid.tests.test_scaffolds', + 'fixture_scaffold') + self.src_content = """\ +These are not the droids +that you are looking for.""" + self.dest_content = """\ +These are the droids for +whom you are looking; +now you have found them.""" + self.src_fn = os.path.join(self.dirname, 'mypackage', '__init__.py') + self.dest_fn = os.path.join(self.dirname, 'mypackage', '__init__.py') + # query_interactive is only normally executed when the destination + # is discovered to be already occupied by existing files, so ... + # create the required occupancy. + from pyramid.scaffolds.copydir import copy_dir + copy_dir(self.fixturetuple, + self.dirname, + {'package':'mypackage'}, + 0, False, + template_renderer=dummy_template_renderer) + + def tearDown(self): + import shutil + shutil.rmtree(self.dirname, ignore_errors=True) + self.out.close() + + def _callFUT(self, *arg, **kw): + from pyramid.scaffolds.copydir import query_interactive + return query_interactive(*arg, **kw) + + def test_query_interactive_0y(self): + from pyramid.scaffolds import copydir + copydir.input_ = RawInputMockObject("y") + self._callFUT(self.src_fn, self.dest_fn, + self.src_content, self.dest_content, + simulate=False, + out_=self.out) + self.assertTrue("Replace" in self.out.getvalue()) + + def test_query_interactive_1n(self): + from pyramid.scaffolds import copydir + copydir.input_ = RawInputMockObject("n") + self._callFUT(self.src_fn, self.dest_fn, + self.src_content, + '\n'.join(self.dest_content.split('\n')[:-1]), + simulate=False, + out_=self.out) + self.assertTrue("Replace" in self.out.getvalue()) + + def test_query_interactive_2b(self): + from pyramid.scaffolds import copydir + copydir.input_ = RawInputMockObject("b") + with open(os.path.join( + self.dirname, 'mypackage', '__init__.py.bak'), 'w') as fp: + fp.write("") + fp.close() + self._callFUT(self.src_fn, self.dest_fn, + self.dest_content, self.src_content, + simulate=False, + out_=self.out) + self.assertTrue("Backing up" in self.out.getvalue()) + + def test_query_interactive_3d(self): + from pyramid.scaffolds import copydir + copydir.input_ = RawInputMockObject("d") + self._callFUT(self.src_fn, self.dest_fn, + self.dest_content, self.src_content, + simulate=False, + out_=self.out) + output = self.out.getvalue() + # The useful text in self.out gets wiped out on the second + # call to raw_input, otherwise the test could be made + # more usefully precise... + # print("3d", output) + # self.assertTrue("@@" in output, output) + self.assertTrue("Replace" in output) + + def test_query_interactive_4dc(self): + from pyramid.scaffolds import copydir + copydir.input_ = RawInputMockObject("dc") + self._callFUT(self.src_fn, self.dest_fn, + self.dest_content, self.src_content, + simulate=False, + out_=self.out) + output = self.out.getvalue() + # The useful text in self.out gets wiped out on the second + # call to raw_input, otherwise, the test could be made + # more usefully precise... + # print("4dc", output) + # self.assertTrue("***" in output, output) + self.assertTrue("Replace" in output) + + def test_query_interactive_5allbad(self): + from pyramid.scaffolds import copydir + copydir.input_ = RawInputMockObject("all z") + self._callFUT(self.src_fn, self.dest_fn, + self.src_content, self.dest_content, + simulate=False, + out_=self.out) + output = self.out.getvalue() + # The useful text in self.out gets wiped out on the second + # call to raw_input, otherwise the test could be made + # more usefully precise... + # print("5allbad", output) + # self.assertTrue("Responses" in output, output) + self.assertTrue("Replace" in output) + + def test_query_interactive_6all(self): + from pyramid.scaffolds import copydir + copydir.input_ = RawInputMockObject("all b") + self._callFUT(self.src_fn, self.dest_fn, + self.src_content, self.dest_content, + simulate=False, + out_=self.out) + output = self.out.getvalue() + # The useful text in self.out gets wiped out on the second + # call to raw_input, otherwise the test could be made + # more usefully precise... + # print("6all", output) + # self.assertTrue("Responses" in output, output) + self.assertTrue("Replace" in output) + +def dummy_template_renderer(content, v, filename=None): + return content + diff --git a/src/pyramid/tests/test_scaffolds/test_init.py b/src/pyramid/tests/test_scaffolds/test_init.py new file mode 100644 index 000000000..f4d1b287a --- /dev/null +++ b/src/pyramid/tests/test_scaffolds/test_init.py @@ -0,0 +1,21 @@ +import unittest + +class TestPyramidTemplate(unittest.TestCase): + def _makeOne(self): + from pyramid.scaffolds import PyramidTemplate + return PyramidTemplate('name') + + def test_pre(self): + inst = self._makeOne() + vars = {'package':'one'} + inst.pre('command', 'output dir', vars) + self.assertTrue(vars['random_string']) + self.assertEqual(vars['package_logger'], 'one') + + def test_pre_root(self): + inst = self._makeOne() + vars = {'package':'root'} + inst.pre('command', 'output dir', vars) + self.assertTrue(vars['random_string']) + self.assertEqual(vars['package_logger'], 'app') + diff --git a/src/pyramid/tests/test_scaffolds/test_template.py b/src/pyramid/tests/test_scaffolds/test_template.py new file mode 100644 index 000000000..98f2daf73 --- /dev/null +++ b/src/pyramid/tests/test_scaffolds/test_template.py @@ -0,0 +1,155 @@ +import unittest + +from pyramid.compat import bytes_ + +class TestTemplate(unittest.TestCase): + def _makeOne(self, name='whatever'): + from pyramid.scaffolds.template import Template + return Template(name) + + def test_render_template_success(self): + inst = self._makeOne() + result = inst.render_template('{{a}} {{b}}', {'a':'1', 'b':'2'}) + self.assertEqual(result, bytes_('1 2')) + + def test_render_template_expr_failure(self): + inst = self._makeOne() + self.assertRaises(AttributeError, inst.render_template, + '{{a.foo}}', {'a':'1', 'b':'2'}) + + def test_render_template_expr_success(self): + inst = self._makeOne() + result = inst.render_template('{{a.lower()}}', {'a':'A'}) + self.assertEqual(result, b'a') + + def test_render_template_expr_success_via_pipe(self): + inst = self._makeOne() + result = inst.render_template('{{b|c|a.lower()}}', {'a':'A'}) + self.assertEqual(result, b'a') + + def test_render_template_expr_success_via_pipe2(self): + inst = self._makeOne() + result = inst.render_template('{{b|a.lower()|c}}', {'a':'A'}) + self.assertEqual(result, b'a') + + def test_render_template_expr_value_is_None(self): + inst = self._makeOne() + result = inst.render_template('{{a}}', {'a':None}) + self.assertEqual(result, b'') + + def test_render_template_with_escaped_double_braces(self): + inst = self._makeOne() + result = inst.render_template('{{a}} {{b}} \{\{a\}\} \{\{c\}\}', {'a':'1', 'b':'2'}) + self.assertEqual(result, bytes_('1 2 {{a}} {{c}}')) + + def test_render_template_with_breaking_escaped_braces(self): + inst = self._makeOne() + result = inst.render_template('{{a}} {{b}} \{\{a\} \{b\}\}', {'a':'1', 'b':'2'}) + self.assertEqual(result, bytes_('1 2 \{\{a\} \{b\}\}')) + + def test_render_template_with_escaped_single_braces(self): + inst = self._makeOne() + result = inst.render_template('{{a}} {{b}} \{a\} \{b', {'a':'1', 'b':'2'}) + self.assertEqual(result, bytes_('1 2 \{a\} \{b')) + + def test_module_dir(self): + import sys + import pkg_resources + package = sys.modules['pyramid.scaffolds.template'] + path = pkg_resources.resource_filename(package.__name__, '') + inst = self._makeOne() + result = inst.module_dir() + self.assertEqual(result, path) + + def test_template_dir__template_dir_is_None(self): + inst = self._makeOne() + self.assertRaises(AssertionError, inst.template_dir) + + def test_template_dir__template_dir_is_tuple(self): + inst = self._makeOne() + inst._template_dir = ('a', 'b') + self.assertEqual(inst.template_dir(), ('a', 'b')) + + def test_template_dir__template_dir_is_not_None(self): + import os + import sys + import pkg_resources + package = sys.modules['pyramid.scaffolds.template'] + path = pkg_resources.resource_filename(package.__name__, '') + inst = self._makeOne() + inst._template_dir ='foo' + result = inst.template_dir() + self.assertEqual(result, os.path.join(path, 'foo')) + + def test_write_files_path_exists(self): + import os + import sys + import pkg_resources + package = sys.modules['pyramid.scaffolds.template'] + path = pkg_resources.resource_filename(package.__name__, '') + inst = self._makeOne() + inst._template_dir = 'foo' + inst.exists = lambda *arg: True + copydir = DummyCopydir() + inst.copydir = copydir + command = DummyCommand() + inst.write_files(command, 'output dir', {'a':1}) + self.assertEqual(copydir.template_dir, os.path.join(path, 'foo')) + self.assertEqual(copydir.output_dir, 'output dir') + self.assertEqual(copydir.vars, {'a':1}) + self.assertEqual(copydir.kw, + {'template_renderer':inst.render_template, + 'indent':1, + 'verbosity':1, + 'simulate':False, + 'overwrite':False, + 'interactive':False, + }) + + def test_write_files_path_missing(self): + L = [] + inst = self._makeOne() + inst._template_dir = 'foo' + inst.exists = lambda *arg: False + inst.out = lambda *arg: None + inst.makedirs = lambda dir: L.append(dir) + copydir = DummyCopydir() + inst.copydir = copydir + command = DummyCommand() + inst.write_files(command, 'output dir', {'a':1}) + self.assertEqual(L, ['output dir']) + + def test_run(self): + L = [] + inst = self._makeOne() + inst._template_dir = 'foo' + inst.exists = lambda *arg: False + inst.out = lambda *arg: None + inst.makedirs = lambda dir: L.append(dir) + copydir = DummyCopydir() + inst.copydir = copydir + command = DummyCommand() + inst.run(command, 'output dir', {'a':1}) + self.assertEqual(L, ['output dir']) + + def test_check_vars(self): + inst = self._makeOne() + self.assertRaises(RuntimeError, inst.check_vars, 'one', 'two') + +class DummyCopydir(object): + def copy_dir(self, template_dir, output_dir, vars, **kw): + self.template_dir = template_dir + self.output_dir = output_dir + self.vars = vars + self.kw = kw + +class DummyArgs(object): + simulate = False + overwrite = False + interactive = False + +class DummyCommand(object): + args = DummyArgs() + verbosity = 1 + + diff --git a/src/pyramid/tests/test_scripting.py b/src/pyramid/tests/test_scripting.py new file mode 100644 index 000000000..ed88bb470 --- /dev/null +++ b/src/pyramid/tests/test_scripting.py @@ -0,0 +1,221 @@ +import unittest + +class Test_get_root(unittest.TestCase): + def _callFUT(self, app, request=None): + from pyramid.scripting import get_root + return get_root(app, request) + + def _makeRegistry(self): + return DummyRegistry([DummyFactory]) + + def setUp(self): + from pyramid.threadlocal import manager + self.manager = manager + self.default = manager.get() + + def test_it_norequest(self): + registry = self._makeRegistry() + app = DummyApp(registry=registry) + root, closer = self._callFUT(app) + self.assertEqual(dummy_root, root) + pushed = self.manager.get() + self.assertEqual(pushed['registry'], registry) + self.assertEqual(pushed['request'].registry, registry) + self.assertEqual(pushed['request'].environ['path'], '/') + closer() + self.assertEqual(self.default, self.manager.get()) + + def test_it_withrequest(self): + registry = self._makeRegistry() + app = DummyApp(registry=registry) + request = DummyRequest({}) + root, closer = self._callFUT(app, request) + self.assertEqual(dummy_root, root) + pushed = self.manager.get() + self.assertEqual(pushed['registry'], registry) + self.assertEqual(pushed['request'], request) + self.assertEqual(pushed['request'].registry, registry) + closer() + self.assertEqual(self.default, self.manager.get()) + +class Test_prepare(unittest.TestCase): + def _callFUT(self, request=None, registry=None): + from pyramid.scripting import prepare + return prepare(request, registry) + + def _makeRegistry(self, L=None): + if L is None: + L = [None, DummyFactory] + return DummyRegistry(L) + + def setUp(self): + from pyramid.threadlocal import manager + self.manager = manager + self.default = manager.get() + + def test_it_no_valid_apps(self): + from pyramid.exceptions import ConfigurationError + self.assertRaises(ConfigurationError, self._callFUT) + + def test_it_norequest(self): + registry = self._makeRegistry([DummyFactory, None, DummyFactory]) + info = self._callFUT(registry=registry) + root, closer, request = info['root'], info['closer'], info['request'] + pushed = self.manager.get() + self.assertEqual(pushed['registry'], registry) + self.assertEqual(pushed['request'].registry, registry) + self.assertEqual(root.a, (pushed['request'],)) + closer() + self.assertEqual(self.default, self.manager.get()) + self.assertEqual(request.context, root) + + def test_it_withrequest_hasregistry(self): + request = DummyRequest({}) + registry = request.registry = self._makeRegistry() + info = self._callFUT(request=request) + root, closer, request = info['root'], info['closer'], info['request'] + pushed = self.manager.get() + self.assertEqual(pushed['request'], request) + self.assertEqual(pushed['registry'], registry) + self.assertEqual(pushed['request'].registry, registry) + self.assertEqual(root.a, (request,)) + closer() + self.assertEqual(self.default, self.manager.get()) + self.assertEqual(request.context, root) + self.assertEqual(request.registry, registry) + + def test_it_withrequest_noregistry(self): + request = DummyRequest({}) + registry = self._makeRegistry() + info = self._callFUT(request=request, registry=registry) + root, closer, request = info['root'], info['closer'], info['request'] + closer() + self.assertEqual(request.context, root) + # should be set by prepare + self.assertEqual(request.registry, registry) + + def test_it_with_request_and_registry(self): + request = DummyRequest({}) + registry = request.registry = self._makeRegistry() + info = self._callFUT(request=request, registry=registry) + root, closer, root = info['root'], info['closer'], info['root'] + pushed = self.manager.get() + self.assertEqual(pushed['request'], request) + self.assertEqual(pushed['registry'], registry) + self.assertEqual(pushed['request'].registry, registry) + self.assertEqual(root.a, (request,)) + closer() + self.assertEqual(self.default, self.manager.get()) + self.assertEqual(request.context, root) + + def test_it_with_request_context_already_set(self): + request = DummyRequest({}) + context = Dummy() + request.context = context + registry = request.registry = self._makeRegistry() + info = self._callFUT(request=request, registry=registry) + root, closer, root = info['root'], info['closer'], info['root'] + closer() + self.assertEqual(request.context, context) + + def test_it_with_extensions(self): + from pyramid.util import InstancePropertyHelper + exts = DummyExtensions() + ext_method = lambda r: 'bar' + name, fn = InstancePropertyHelper.make_property(ext_method, 'foo') + exts.descriptors[name] = fn + request = DummyRequest({}) + registry = request.registry = self._makeRegistry([exts, DummyFactory]) + info = self._callFUT(request=request, registry=registry) + self.assertEqual(request.foo, 'bar') + root, closer = info['root'], info['closer'] + closer() + + def test_it_is_a_context_manager(self): + request = DummyRequest({}) + registry = request.registry = self._makeRegistry() + closer_called = [False] + with self._callFUT(request=request) as info: + root, request = info['root'], info['request'] + pushed = self.manager.get() + self.assertEqual(pushed['request'], request) + self.assertEqual(pushed['registry'], registry) + self.assertEqual(pushed['request'].registry, registry) + self.assertEqual(root.a, (request,)) + orig_closer = info['closer'] + def closer(): + orig_closer() + closer_called[0] = True + info['closer'] = closer + self.assertTrue(closer_called[0]) + self.assertEqual(self.default, self.manager.get()) + self.assertEqual(request.context, root) + self.assertEqual(request.registry, registry) + +class Test__make_request(unittest.TestCase): + def _callFUT(self, path='/', registry=None): + from pyramid.scripting import _make_request + return _make_request(path, registry) + + def _makeRegistry(self): + return DummyRegistry([DummyFactory]) + + def test_it_with_registry(self): + registry = self._makeRegistry() + request = self._callFUT('/', registry) + self.assertEqual(request.environ['path'], '/') + self.assertEqual(request.registry, registry) + + def test_it_with_no_registry(self): + from pyramid.config import global_registries + registry = self._makeRegistry() + global_registries.add(registry) + try: + request = self._callFUT('/hello') + self.assertEqual(request.environ['path'], '/hello') + self.assertEqual(request.registry, registry) + finally: + global_registries.empty() + +class Dummy: + pass + +dummy_root = Dummy() + +class DummyFactory(object): + @classmethod + def blank(cls, path): + req = DummyRequest({'path': path}) + return req + + def __init__(self, *a, **kw): + self.a = a + self.kw = kw + +class DummyRegistry(object): + def __init__(self, utilities): + self.utilities = utilities + + def queryUtility(self, iface, default=None): # pragma: no cover + if self.utilities: + return self.utilities.pop(0) + return default + +class DummyApp: + def __init__(self, registry=None): + if registry: + self.registry = registry + + def root_factory(self, environ): + return dummy_root + +class DummyRequest(object): + matchdict = None + matched_route = None + def __init__(self, environ): + self.environ = environ + +class DummyExtensions: + def __init__(self): + self.descriptors = {} + self.methods = {} diff --git a/src/pyramid/tests/test_scripts/__init__.py b/src/pyramid/tests/test_scripts/__init__.py new file mode 100644 index 000000000..5bb534f79 --- /dev/null +++ b/src/pyramid/tests/test_scripts/__init__.py @@ -0,0 +1 @@ +# package diff --git a/src/pyramid/tests/test_scripts/dummy.py b/src/pyramid/tests/test_scripts/dummy.py new file mode 100644 index 000000000..f1ef403f8 --- /dev/null +++ b/src/pyramid/tests/test_scripts/dummy.py @@ -0,0 +1,190 @@ +class DummyTweens(object): + def __init__(self, implicit, explicit): + self._implicit = implicit + self.explicit = explicit + self.name_to_alias = {} + def implicit(self): + return self._implicit + +class Dummy: + pass + +dummy_root = Dummy() + +class DummyRegistry(object): + settings = {} + def queryUtility(self, iface, default=None, name=''): + return default + +dummy_registry = DummyRegistry() + +class DummyShell(object): + env = {} + help = '' + called = False + dummy_attr = 1 + + def __call__(self, env, help): + self.env = env + self.help = help + self.called = True + self.env['request'].dummy_attr = self.dummy_attr + +class DummyInteractor: + def __call__(self, banner, local): + self.banner = banner + self.local = local + +class DummyApp: + def __init__(self): + self.registry = dummy_registry + +class DummyMapper(object): + def __init__(self, *routes): + self.routes = routes + + def get_routes(self, include_static=False): + return self.routes + +class DummyRoute(object): + def __init__(self, name, pattern, factory=None, + matchdict=None, predicate=None): + self.name = name + self.path = pattern + self.pattern = pattern + self.factory = factory + self.matchdict = matchdict + self.predicates = [] + if predicate is not None: + self.predicates = [predicate] + + def match(self, route): + return self.matchdict + +class DummyRequest: + application_url = 'http://example.com:5432' + script_name = '' + def __init__(self, environ): + self.environ = environ + self.matchdict = {} + +class DummyView(object): + def __init__(self, **attrs): + self.__request_attrs__ = attrs + + def view(context, request): pass + +from zope.interface import implementer +from pyramid.interfaces import IMultiView +@implementer(IMultiView) +class DummyMultiView(object): + + def __init__(self, *views, **attrs): + self.views = [(None, view, None) for view in views] + self.__request_attrs__ = attrs + +class DummyCloser(object): + def __call__(self): + self.called = True + +class DummyBootstrap(object): + def __init__(self, app=None, registry=None, request=None, root=None, + root_factory=None, closer=None): + self.app = app or DummyApp() + if registry is None: + registry = DummyRegistry() + self.registry = registry + if request is None: + request = DummyRequest({}) + self.request = request + if root is None: + root = Dummy() + self.root = root + if root_factory is None: + root_factory = Dummy() + self.root_factory = root_factory + if closer is None: + closer = DummyCloser() + self.closer = closer + + def __call__(self, *a, **kw): + self.a = a + self.kw = kw + registry = kw.get('registry', self.registry) + request = kw.get('request', self.request) + request.registry = registry + return { + 'app': self.app, + 'registry': registry, + 'request': request, + 'root': self.root, + 'root_factory': self.root_factory, + 'closer': self.closer, + } + + +class DummyEntryPoint(object): + def __init__(self, name, module): + self.name = name + self.module = module + + def load(self): + return self.module + + +class DummyPkgResources(object): + def __init__(self, entry_point_values): + self.entry_points = [] + + for name, module in entry_point_values.items(): + self.entry_points.append(DummyEntryPoint(name, module)) + + def iter_entry_points(self, name): + return self.entry_points + + +class dummy_setup_logging(object): + def __call__(self, config_uri, global_conf): + self.config_uri = config_uri + self.defaults = global_conf + + +class DummyLoader(object): + def __init__(self, settings=None, app_settings=None, app=None, server=None): + if not settings: + settings = {} + if not app_settings: + app_settings = {} + self.settings = settings + self.app_settings = app_settings + self.app = app + self.server = server + self.calls = [] + + def __call__(self, uri): + import plaster + self.uri = plaster.parse_uri(uri) + return self + + def add_call(self, op, name, defaults): + self.calls.append({'op': op, 'name': name, 'defaults': defaults}) + + def get_settings(self, name=None, defaults=None): + self.add_call('settings', name, defaults) + return self.settings.get(name, {}) + + def get_wsgi_app(self, name=None, defaults=None): + self.add_call('app', name, defaults) + return self.app + + def get_wsgi_app_settings(self, name=None, defaults=None): + self.add_call('app_settings', name, defaults) + return self.app_settings + + def get_wsgi_server(self, name=None, defaults=None): + self.add_call('server', name, defaults) + return self.server + + def setup_logging(self, defaults): + self.add_call('logging', None, defaults) + self.defaults = defaults diff --git a/src/pyramid/tests/test_scripts/pystartup.txt b/src/pyramid/tests/test_scripts/pystartup.txt new file mode 100644 index 000000000..c62c4ca74 --- /dev/null +++ b/src/pyramid/tests/test_scripts/pystartup.txt @@ -0,0 +1,3 @@ +# this file has a .txt extension to avoid coverage reports +# since it is not imported but rather the contents are read and exec'd +foo = 1 diff --git a/src/pyramid/tests/test_scripts/test_common.py b/src/pyramid/tests/test_scripts/test_common.py new file mode 100644 index 000000000..60741db92 --- /dev/null +++ b/src/pyramid/tests/test_scripts/test_common.py @@ -0,0 +1,13 @@ +import unittest + +class TestParseVars(unittest.TestCase): + def test_parse_vars_good(self): + from pyramid.scripts.common import parse_vars + vars = ['a=1', 'b=2'] + result = parse_vars(vars) + self.assertEqual(result, {'a': '1', 'b': '2'}) + + def test_parse_vars_bad(self): + from pyramid.scripts.common import parse_vars + vars = ['a'] + self.assertRaises(ValueError, parse_vars, vars) diff --git a/src/pyramid/tests/test_scripts/test_pcreate.py b/src/pyramid/tests/test_scripts/test_pcreate.py new file mode 100644 index 000000000..0286614ce --- /dev/null +++ b/src/pyramid/tests/test_scripts/test_pcreate.py @@ -0,0 +1,309 @@ +import unittest + + +class TestPCreateCommand(unittest.TestCase): + def setUp(self): + from pyramid.compat import NativeIO + self.out_ = NativeIO() + + def out(self, msg): + self.out_.write(msg) + + def _getTargetClass(self): + from pyramid.scripts.pcreate import PCreateCommand + return PCreateCommand + + def _makeOne(self, *args, **kw): + effargs = ['pcreate'] + effargs.extend(args) + tgt_class = kw.pop('target_class', self._getTargetClass()) + cmd = tgt_class(effargs, **kw) + cmd.out = self.out + return cmd + + def test_run_show_scaffolds_exist(self): + cmd = self._makeOne('-l') + result = cmd.run() + self.assertEqual(result, 0) + out = self.out_.getvalue() + self.assertTrue(out.count('Available scaffolds')) + + def test_run_show_scaffolds_none_exist(self): + cmd = self._makeOne('-l') + cmd.scaffolds = [] + result = cmd.run() + self.assertEqual(result, 0) + out = self.out_.getvalue() + self.assertTrue(out.count('No scaffolds available')) + + def test_run_no_scaffold_no_args(self): + cmd = self._makeOne(quiet=True) + result = cmd.run() + self.assertEqual(result, 2) + + def test_run_no_scaffold_name(self): + cmd = self._makeOne('dummy') + result = cmd.run() + self.assertEqual(result, 2) + out = self.out_.getvalue() + self.assertTrue(out.count( + 'You must provide at least one scaffold name')) + + def test_no_project_name(self): + cmd = self._makeOne('-s', 'dummy') + result = cmd.run() + self.assertEqual(result, 2) + out = self.out_.getvalue() + 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.count('Unavailable scaffolds')) + + def test_known_scaffold_single_rendered(self): + import os + cmd = self._makeOne('-s', 'dummy', 'Distro') + scaffold = DummyScaffold('dummy') + cmd.scaffolds = [scaffold] + cmd.pyramid_dist = DummyDist("0.1") + result = cmd.run() + self.assertEqual(result, 0) + self.assertEqual( + scaffold.output_dir, + os.path.normpath(os.path.join(os.getcwd(), 'Distro')) + ) + self.assertEqual( + scaffold.vars, + {'project': 'Distro', 'egg': 'Distro', 'package': 'distro', + 'pyramid_version': '0.1', 'pyramid_docs_branch':'0.1-branch'}) + + def test_scaffold_with_package_name(self): + import os + cmd = self._makeOne('-s', 'dummy', '--package-name', 'dummy_package', + 'Distro') + scaffold = DummyScaffold('dummy') + cmd.scaffolds = [scaffold] + cmd.pyramid_dist = DummyDist("0.1") + result = cmd.run() + + self.assertEqual(result, 0) + self.assertEqual( + scaffold.output_dir, + os.path.normpath(os.path.join(os.getcwd(), 'Distro')) + ) + self.assertEqual( + scaffold.vars, + {'project': 'Distro', 'egg': 'dummy_package', + 'package': 'dummy_package', 'pyramid_version': '0.1', + 'pyramid_docs_branch':'0.1-branch'}) + + + def test_scaffold_with_hyphen_in_project_name(self): + import os + cmd = self._makeOne('-s', 'dummy', 'Distro-') + scaffold = DummyScaffold('dummy') + cmd.scaffolds = [scaffold] + cmd.pyramid_dist = DummyDist("0.1") + result = cmd.run() + self.assertEqual(result, 0) + self.assertEqual( + scaffold.output_dir, + os.path.normpath(os.path.join(os.getcwd(), 'Distro-')) + ) + self.assertEqual( + scaffold.vars, + {'project': 'Distro-', 'egg': 'Distro_', 'package': 'distro_', + 'pyramid_version': '0.1', 'pyramid_docs_branch':'0.1-branch'}) + + def test_known_scaffold_absolute_path(self): + import os + path = os.path.abspath('Distro') + cmd = self._makeOne('-s', 'dummy', path) + cmd.pyramid_dist = DummyDist("0.1") + scaffold = DummyScaffold('dummy') + cmd.scaffolds = [scaffold] + cmd.pyramid_dist = DummyDist("0.1") + result = cmd.run() + self.assertEqual(result, 0) + self.assertEqual( + scaffold.output_dir, + os.path.normpath(os.path.join(os.getcwd(), 'Distro')) + ) + self.assertEqual( + scaffold.vars, + {'project': 'Distro', 'egg': 'Distro', 'package': 'distro', + 'pyramid_version': '0.1', 'pyramid_docs_branch':'0.1-branch'}) + + def test_known_scaffold_multiple_rendered(self): + import os + cmd = self._makeOne('-s', 'dummy1', '-s', 'dummy2', 'Distro') + scaffold1 = DummyScaffold('dummy1') + scaffold2 = DummyScaffold('dummy2') + cmd.scaffolds = [scaffold1, scaffold2] + cmd.pyramid_dist = DummyDist("0.1") + result = cmd.run() + self.assertEqual(result, 0) + self.assertEqual( + scaffold1.output_dir, + os.path.normpath(os.path.join(os.getcwd(), 'Distro')) + ) + self.assertEqual( + scaffold1.vars, + {'project': 'Distro', 'egg': 'Distro', 'package': 'distro', + 'pyramid_version': '0.1', 'pyramid_docs_branch':'0.1-branch'}) + self.assertEqual( + scaffold2.output_dir, + os.path.normpath(os.path.join(os.getcwd(), 'Distro')) + ) + self.assertEqual( + scaffold2.vars, + {'project': 'Distro', 'egg': 'Distro', 'package': 'distro', + 'pyramid_version': '0.1', 'pyramid_docs_branch':'0.1-branch'}) + + def test_known_scaffold_with_path_as_project_target_rendered(self): + import os + cmd = self._makeOne('-s', 'dummy', '/tmp/foo/Distro/') + scaffold = DummyScaffold('dummy') + cmd.scaffolds = [scaffold] + cmd.pyramid_dist = DummyDist("0.1") + result = cmd.run() + self.assertEqual(result, 0) + self.assertEqual( + scaffold.output_dir, + os.path.normpath(os.path.join(os.getcwd(), '/tmp/foo/Distro')) + ) + self.assertEqual( + scaffold.vars, + {'project': 'Distro', 'egg': 'Distro', 'package': 'distro', + 'pyramid_version': '0.1', 'pyramid_docs_branch':'0.1-branch'}) + + + def test_scaffold_with_prod_pyramid_version(self): + cmd = self._makeOne('-s', 'dummy', 'Distro') + scaffold = DummyScaffold('dummy') + cmd.scaffolds = [scaffold] + cmd.pyramid_dist = DummyDist("0.2") + result = cmd.run() + self.assertEqual(result, 0) + self.assertEqual( + scaffold.vars, + {'project': 'Distro', 'egg': 'Distro', 'package': 'distro', + 'pyramid_version': '0.2', 'pyramid_docs_branch':'0.2-branch'}) + + def test_scaffold_with_prod_pyramid_long_version(self): + cmd = self._makeOne('-s', 'dummy', 'Distro') + scaffold = DummyScaffold('dummy') + cmd.scaffolds = [scaffold] + cmd.pyramid_dist = DummyDist("0.2.1") + result = cmd.run() + self.assertEqual(result, 0) + self.assertEqual( + scaffold.vars, + {'project': 'Distro', 'egg': 'Distro', 'package': 'distro', + 'pyramid_version': '0.2.1', 'pyramid_docs_branch':'0.2-branch'}) + + def test_scaffold_with_prod_pyramid_unparsable_version(self): + cmd = self._makeOne('-s', 'dummy', 'Distro') + scaffold = DummyScaffold('dummy') + cmd.scaffolds = [scaffold] + cmd.pyramid_dist = DummyDist("abc") + result = cmd.run() + self.assertEqual(result, 0) + self.assertEqual( + scaffold.vars, + {'project': 'Distro', 'egg': 'Distro', 'package': 'distro', + 'pyramid_version': 'abc', 'pyramid_docs_branch':'latest'}) + + def test_scaffold_with_dev_pyramid_version(self): + cmd = self._makeOne('-s', 'dummy', 'Distro') + scaffold = DummyScaffold('dummy') + cmd.scaffolds = [scaffold] + cmd.pyramid_dist = DummyDist("0.12dev") + result = cmd.run() + self.assertEqual(result, 0) + self.assertEqual( + scaffold.vars, + {'project': 'Distro', 'egg': 'Distro', 'package': 'distro', + 'pyramid_version': '0.12dev', + 'pyramid_docs_branch': 'master'}) + + def test_scaffold_with_dev_pyramid_long_version(self): + cmd = self._makeOne('-s', 'dummy', 'Distro') + scaffold = DummyScaffold('dummy') + cmd.scaffolds = [scaffold] + cmd.pyramid_dist = DummyDist("0.10.1dev") + result = cmd.run() + self.assertEqual(result, 0) + self.assertEqual( + scaffold.vars, + {'project': 'Distro', 'egg': 'Distro', 'package': 'distro', + 'pyramid_version': '0.10.1dev', + 'pyramid_docs_branch': 'master'}) + + def test_confirm_override_conflicting_name(self): + from pyramid.scripts.pcreate import PCreateCommand + class YahInputPCreateCommand(PCreateCommand): + def confirm_bad_name(self, pkg_name): + return True + cmd = self._makeOne('-s', 'dummy', 'Unittest', target_class=YahInputPCreateCommand) + scaffold = DummyScaffold('dummy') + cmd.scaffolds = [scaffold] + cmd.pyramid_dist = DummyDist("0.10.1dev") + result = cmd.run() + self.assertEqual(result, 0) + self.assertEqual( + scaffold.vars, + {'project': 'Unittest', 'egg': 'Unittest', 'package': 'unittest', + 'pyramid_version': '0.10.1dev', + 'pyramid_docs_branch': 'master'}) + + def test_force_override_conflicting_name(self): + cmd = self._makeOne('-s', 'dummy', 'Unittest', '--ignore-conflicting-name') + scaffold = DummyScaffold('dummy') + cmd.scaffolds = [scaffold] + cmd.pyramid_dist = DummyDist("0.10.1dev") + result = cmd.run() + self.assertEqual(result, 0) + self.assertEqual( + scaffold.vars, + {'project': 'Unittest', 'egg': 'Unittest', 'package': 'unittest', + 'pyramid_version': '0.10.1dev', + 'pyramid_docs_branch': 'master'}) + + def test_force_override_site_name(self): + from pyramid.scripts.pcreate import PCreateCommand + class NayInputPCreateCommand(PCreateCommand): + def confirm_bad_name(self, pkg_name): + return False + cmd = self._makeOne('-s', 'dummy', 'Site', target_class=NayInputPCreateCommand) + scaffold = DummyScaffold('dummy') + cmd.scaffolds = [scaffold] + cmd.pyramid_dist = DummyDist("0.10.1dev") + result = cmd.run() + self.assertEqual(result, 2) + + +class Test_main(unittest.TestCase): + def _callFUT(self, argv): + from pyramid.scripts.pcreate import main + return main(argv, quiet=True) + + def test_it(self): + result = self._callFUT(['pcreate']) + self.assertEqual(result, 2) + +class DummyScaffold(object): + def __init__(self, name): + self.name = name + + def run(self, command, output_dir, vars): + self.command = command + self.output_dir = output_dir + self.vars = vars + +class DummyDist(object): + def __init__(self, version): + self.version = version diff --git a/src/pyramid/tests/test_scripts/test_pdistreport.py b/src/pyramid/tests/test_scripts/test_pdistreport.py new file mode 100644 index 000000000..e229667c5 --- /dev/null +++ b/src/pyramid/tests/test_scripts/test_pdistreport.py @@ -0,0 +1,73 @@ +import unittest + +class TestPDistReportCommand(unittest.TestCase): + def _callFUT(self, **kw): + argv = [] + from pyramid.scripts.pdistreport import main + return main(argv, **kw) + + def test_no_dists(self): + def platform(): + return 'myplatform' + pkg_resources = DummyPkgResources() + L = [] + def out(*args): + L.extend(args) + result = self._callFUT(pkg_resources=pkg_resources, platform=platform, + out=out) + self.assertEqual(result, None) + self.assertEqual( + L, + ['Pyramid version:', '1', + 'Platform:', 'myplatform', + 'Packages:'] + ) + + def test_with_dists(self): + def platform(): + return 'myplatform' + working_set = (DummyDistribution('abc'), DummyDistribution('def')) + pkg_resources = DummyPkgResources(working_set) + L = [] + def out(*args): + L.extend(args) + result = self._callFUT(pkg_resources=pkg_resources, platform=platform, + out=out) + self.assertEqual(result, None) + self.assertEqual( + L, + ['Pyramid version:', + '1', + 'Platform:', + 'myplatform', + 'Packages:', + ' ', + 'abc', + '1', + ' ', + '/projects/abc', + ' ', + 'def', + '1', + ' ', + '/projects/def'] + ) + +class DummyPkgResources(object): + def __init__(self, working_set=()): + self.working_set = working_set + + def get_distribution(self, name): + return Version('1') + +class Version(object): + def __init__(self, version): + self.version = version + +class DummyDistribution(object): + def __init__(self, name): + self.project_name = name + self.version = '1' + self.location = '/projects/%s' % name + + diff --git a/src/pyramid/tests/test_scripts/test_prequest.py b/src/pyramid/tests/test_scripts/test_prequest.py new file mode 100644 index 000000000..75d5cc198 --- /dev/null +++ b/src/pyramid/tests/test_scripts/test_prequest.py @@ -0,0 +1,214 @@ +import unittest +from pyramid.tests.test_scripts import dummy + +class TestPRequestCommand(unittest.TestCase): + def _getTargetClass(self): + from pyramid.scripts.prequest import PRequestCommand + return PRequestCommand + + def _makeOne(self, argv, headers=None): + cmd = self._getTargetClass()(argv) + + def helloworld(environ, start_request): + self._environ = environ + self._path_info = environ['PATH_INFO'] + start_request('200 OK', headers or []) + return [b'abc'] + self.loader = dummy.DummyLoader(app=helloworld) + self._out = [] + cmd._get_config_loader = self.loader + cmd.out = self.out + return cmd + + def out(self, msg): + self._out.append(msg) + + def test_command_not_enough_args(self): + command = self._makeOne([]) + command.run() + self.assertEqual(self._out, ['You must provide at least two arguments']) + + def test_command_two_args(self): + command = self._makeOne(['', 'development.ini', '/'], + [('Content-Type', 'text/html; charset=UTF-8')]) + command.run() + self.assertEqual(self._path_info, '/') + self.assertEqual(self.loader.uri.path, 'development.ini') + self.assertEqual(self.loader.calls[0]['op'], 'logging') + self.assertEqual(self.loader.calls[1]['op'], 'app') + self.assertEqual(self.loader.calls[1]['name'], None) + self.assertEqual(self._out, ['abc']) + + def test_command_path_doesnt_start_with_slash(self): + command = self._makeOne(['', 'development.ini', 'abc'], + [('Content-Type', 'text/html; charset=UTF-8')]) + command.run() + self.assertEqual(self._path_info, '/abc') + self.assertEqual(self.loader.uri.path, 'development.ini') + self.assertEqual(self._out, ['abc']) + + def test_command_has_bad_config_header(self): + command = self._makeOne( + ['', '--header=name','development.ini', '/']) + command.run() + self.assertEqual( + self._out[0], + ("Bad --header=name option, value must be in the form " + "'name:value'")) + + def test_command_has_good_header_var(self): + command = self._makeOne( + ['', '--header=name:value','development.ini', '/'], + [('Content-Type', 'text/html; charset=UTF-8')]) + command.run() + self.assertEqual(self._environ['HTTP_NAME'], 'value') + self.assertEqual(self._path_info, '/') + self.assertEqual(self._out, ['abc']) + + def test_command_w_basic_auth(self): + command = self._makeOne( + ['', '--login=user:password', + '--header=name:value','development.ini', '/'], + [('Content-Type', 'text/html; charset=UTF-8')]) + command.run() + self.assertEqual(self._environ['HTTP_NAME'], 'value') + self.assertEqual(self._environ['HTTP_AUTHORIZATION'], + 'Basic dXNlcjpwYXNzd29yZA==') + self.assertEqual(self._path_info, '/') + self.assertEqual(self._out, ['abc']) + + def test_command_has_content_type_header_var(self): + command = self._makeOne( + ['', '--header=content-type:app/foo','development.ini', '/'], + [('Content-Type', 'text/html; charset=UTF-8')]) + command.run() + self.assertEqual(self._environ['CONTENT_TYPE'], 'app/foo') + self.assertEqual(self._path_info, '/') + self.assertEqual(self._out, ['abc']) + + def test_command_has_multiple_header_vars(self): + command = self._makeOne( + ['', + '--header=name:value', + '--header=name2:value2', + 'development.ini', + '/'], + [('Content-Type', 'text/html; charset=UTF-8')] + ) + command.run() + self.assertEqual(self._environ['HTTP_NAME'], 'value') + self.assertEqual(self._environ['HTTP_NAME2'], 'value2') + self.assertEqual(self._path_info, '/') + self.assertEqual(self._out, ['abc']) + + def test_command_method_get(self): + command = self._makeOne(['', '--method=GET', 'development.ini', '/'], + [('Content-Type', 'text/html; charset=UTF-8')]) + command.run() + self.assertEqual(self._environ['REQUEST_METHOD'], 'GET') + self.assertEqual(self._path_info, '/') + self.assertEqual(self._out, ['abc']) + + def test_command_method_post(self): + from pyramid.compat import NativeIO + command = self._makeOne(['', '--method=POST', 'development.ini', '/'], + [('Content-Type', 'text/html; charset=UTF-8')]) + stdin = NativeIO() + command.stdin = stdin + command.run() + self.assertEqual(self._environ['REQUEST_METHOD'], 'POST') + self.assertEqual(self._environ['CONTENT_LENGTH'], '-1') + self.assertEqual(self._environ['wsgi.input'], stdin) + self.assertEqual(self._path_info, '/') + self.assertEqual(self._out, ['abc']) + + def test_command_method_put(self): + from pyramid.compat import NativeIO + command = self._makeOne(['', '--method=PUT', 'development.ini', '/'], + [('Content-Type', 'text/html; charset=UTF-8')]) + stdin = NativeIO() + command.stdin = stdin + command.run() + self.assertEqual(self._environ['REQUEST_METHOD'], 'PUT') + self.assertEqual(self._environ['CONTENT_LENGTH'], '-1') + self.assertEqual(self._environ['wsgi.input'], stdin) + self.assertEqual(self._path_info, '/') + self.assertEqual(self._out, ['abc']) + + def test_command_method_patch(self): + from pyramid.compat import NativeIO + command = self._makeOne(['', '--method=PATCH', 'development.ini', '/'], + [('Content-Type', 'text/html; charset=UTF-8')]) + stdin = NativeIO() + command.stdin = stdin + command.run() + self.assertEqual(self._environ['REQUEST_METHOD'], 'PATCH') + self.assertEqual(self._environ['CONTENT_LENGTH'], '-1') + self.assertEqual(self._environ['wsgi.input'], stdin) + self.assertEqual(self._path_info, '/') + self.assertEqual(self._out, ['abc']) + + def test_command_method_propfind(self): + from pyramid.compat import NativeIO + command = self._makeOne(['', '--method=PROPFIND', 'development.ini', + '/'], + [('Content-Type', 'text/html; charset=UTF-8')]) + stdin = NativeIO() + command.stdin = stdin + command.run() + self.assertEqual(self._environ['REQUEST_METHOD'], 'PROPFIND') + self.assertEqual(self._path_info, '/') + self.assertEqual(self._out, ['abc']) + + def test_command_method_options(self): + from pyramid.compat import NativeIO + command = self._makeOne(['', '--method=OPTIONS', 'development.ini', + '/'], + [('Content-Type', 'text/html; charset=UTF-8')]) + stdin = NativeIO() + command.stdin = stdin + command.run() + self.assertEqual(self._environ['REQUEST_METHOD'], 'OPTIONS') + self.assertEqual(self._path_info, '/') + self.assertEqual(self._out, ['abc']) + + def test_command_with_query_string(self): + command = self._makeOne(['', 'development.ini', '/abc?a=1&b=2&c'], + [('Content-Type', 'text/html; charset=UTF-8')]) + command.run() + self.assertEqual(self._environ['QUERY_STRING'], 'a=1&b=2&c') + self.assertEqual(self._path_info, '/abc') + self.assertEqual(self._out, ['abc']) + + def test_command_display_headers(self): + command = self._makeOne( + ['', '--display-headers', 'development.ini', '/'], + [('Content-Type', 'text/html; charset=UTF-8')]) + command.run() + self.assertEqual(self._path_info, '/') + self.assertEqual( + self._out, + ['200 OK', 'Content-Type: text/html; charset=UTF-8', 'abc']) + + def test_command_response_has_no_charset(self): + command = self._makeOne(['', '--method=GET', 'development.ini', '/'], + headers=[('Content-Type', 'image/jpeg')]) + command.run() + self.assertEqual(self._path_info, '/') + + self.assertEqual(self._out, [b'abc']) + + def test_command_method_configures_logging(self): + command = self._makeOne(['', 'development.ini', '/']) + command.run() + self.assertEqual(self.loader.calls[0]['op'], 'logging') + + +class Test_main(unittest.TestCase): + def _callFUT(self, argv): + from pyramid.scripts.prequest import main + return main(argv, True) + + def test_it(self): + result = self._callFUT(['prequest']) + self.assertEqual(result, 2) diff --git a/src/pyramid/tests/test_scripts/test_proutes.py b/src/pyramid/tests/test_scripts/test_proutes.py new file mode 100644 index 000000000..fab5e163e --- /dev/null +++ b/src/pyramid/tests/test_scripts/test_proutes.py @@ -0,0 +1,792 @@ +import os +import unittest +from pyramid.tests.test_scripts import dummy + + +class DummyIntrospector(object): + def __init__(self): + self.relations = {} + self.introspectables = {} + + def get(self, name, discrim): + pass + + +class TestPRoutesCommand(unittest.TestCase): + def _getTargetClass(self): + from pyramid.scripts.proutes import PRoutesCommand + return PRoutesCommand + + def _makeOne(self): + cmd = self._getTargetClass()([]) + cmd.bootstrap = dummy.DummyBootstrap() + cmd.get_config_loader = dummy.DummyLoader() + cmd.args.config_uri = '/foo/bar/myapp.ini#myapp' + + return cmd + + def _makeRegistry(self): + from pyramid.registry import Registry + registry = Registry() + registry.introspector = DummyIntrospector() + return registry + + def _makeConfig(self, *arg, **kw): + from pyramid.config import Configurator + config = Configurator(*arg, **kw) + return config + + def test_good_args(self): + cmd = self._getTargetClass()([]) + cmd.bootstrap = dummy.DummyBootstrap() + cmd.get_config_loader = dummy.DummyLoader() + cmd.args.config_uri = '/foo/bar/myapp.ini#myapp' + cmd.args.config_args = ('a=1',) + route = dummy.DummyRoute('a', '/a') + mapper = dummy.DummyMapper(route) + cmd._get_mapper = lambda *arg: mapper + registry = self._makeRegistry() + cmd.bootstrap = dummy.DummyBootstrap(registry=registry) + L = [] + cmd.out = lambda msg: L.append(msg) + cmd.run() + self.assertTrue('' in ''.join(L)) + + def test_bad_args(self): + cmd = self._getTargetClass()([]) + cmd.bootstrap = dummy.DummyBootstrap() + cmd.get_config_loader = dummy.DummyLoader() + cmd.args.config_uri = '/foo/bar/myapp.ini#myapp' + cmd.args.config_vars = ('a',) + route = dummy.DummyRoute('a', '/a') + mapper = dummy.DummyMapper(route) + cmd._get_mapper = lambda *arg: mapper + + self.assertRaises(ValueError, cmd.run) + + def test_no_routes(self): + command = self._makeOne() + mapper = dummy.DummyMapper() + command._get_mapper = lambda *arg: mapper + L = [] + command.out = L.append + result = command.run() + self.assertEqual(result, 0) + self.assertEqual(L, []) + + def test_no_mapper(self): + command = self._makeOne() + command._get_mapper = lambda *arg:None + L = [] + command.out = L.append + result = command.run() + self.assertEqual(result, 0) + self.assertEqual(L, []) + + def test_single_route_no_route_registered(self): + command = self._makeOne() + route = dummy.DummyRoute('a', '/a') + mapper = dummy.DummyMapper(route) + command._get_mapper = lambda *arg: mapper + registry = self._makeRegistry() + command.bootstrap = dummy.DummyBootstrap(registry=registry) + + L = [] + command.out = L.append + result = command.run() + self.assertEqual(result, 0) + self.assertEqual(len(L), 3) + self.assertEqual(L[-1].split(), ['a', '/a', '', '*']) + + def test_route_with_no_slash_prefix(self): + command = self._makeOne() + route = dummy.DummyRoute('a', 'a') + mapper = dummy.DummyMapper(route) + command._get_mapper = lambda *arg: mapper + L = [] + command.out = L.append + registry = self._makeRegistry() + command.bootstrap = dummy.DummyBootstrap(registry=registry) + result = command.run() + self.assertEqual(result, 0) + self.assertEqual(len(L), 3) + self.assertEqual(L[-1].split(), ['a', '/a', '', '*']) + + def test_single_route_no_views_registered(self): + from zope.interface import Interface + from pyramid.interfaces import IRouteRequest + registry = self._makeRegistry() + + def view():pass + class IMyRoute(Interface): + pass + registry.registerUtility(IMyRoute, IRouteRequest, name='a') + command = self._makeOne() + route = dummy.DummyRoute('a', '/a') + mapper = dummy.DummyMapper(route) + command._get_mapper = lambda *arg: mapper + L = [] + command.out = L.append + command.bootstrap = dummy.DummyBootstrap(registry=registry) + result = command.run() + self.assertEqual(result, 0) + self.assertEqual(len(L), 3) + self.assertEqual(L[-1].split()[:3], ['a', '/a', '']) + + def test_single_route_one_view_registered(self): + from zope.interface import Interface + from pyramid.interfaces import IRouteRequest + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IView + registry = self._makeRegistry() + + def view():pass + class IMyRoute(Interface): + pass + registry.registerAdapter(view, + (IViewClassifier, IMyRoute, Interface), + IView, '') + registry.registerUtility(IMyRoute, IRouteRequest, name='a') + command = self._makeOne() + route = dummy.DummyRoute('a', '/a') + mapper = dummy.DummyMapper(route) + command._get_mapper = lambda *arg: mapper + L = [] + command.out = L.append + command.bootstrap = dummy.DummyBootstrap(registry=registry) + result = command.run() + self.assertEqual(result, 0) + self.assertEqual(len(L), 3) + compare_to = L[-1].split()[:3] + self.assertEqual( + compare_to, + ['a', '/a', 'pyramid.tests.test_scripts.test_proutes.view'] + ) + + def test_one_route_with_long_name_one_view_registered(self): + from zope.interface import Interface + from pyramid.interfaces import IRouteRequest + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IView + registry = self._makeRegistry() + + def view():pass + + class IMyRoute(Interface): + pass + + registry.registerAdapter( + view, + (IViewClassifier, IMyRoute, Interface), + IView, '' + ) + + registry.registerUtility(IMyRoute, IRouteRequest, + name='very_long_name_123') + + command = self._makeOne() + route = dummy.DummyRoute( + 'very_long_name_123', + '/and_very_long_pattern_as_well' + ) + mapper = dummy.DummyMapper(route) + command._get_mapper = lambda *arg: mapper + L = [] + command.out = L.append + command.bootstrap = dummy.DummyBootstrap(registry=registry) + result = command.run() + self.assertEqual(result, 0) + self.assertEqual(len(L), 3) + compare_to = L[-1].split()[:3] + self.assertEqual( + compare_to, + ['very_long_name_123', + '/and_very_long_pattern_as_well', + 'pyramid.tests.test_scripts.test_proutes.view'] + ) + + def test_class_view(self): + from pyramid.renderers import null_renderer as nr + + config = self._makeConfig(autocommit=True) + config.add_route('foo', '/a/b') + config.add_view( + route_name='foo', + view=dummy.DummyView, + attr='view', + renderer=nr, + request_method='POST' + ) + + command = self._makeOne() + L = [] + command.out = L.append + command.bootstrap = dummy.DummyBootstrap(registry=config.registry) + result = command.run() + self.assertEqual(result, 0) + self.assertEqual(len(L), 3) + compare_to = L[-1].split() + expected = [ + 'foo', '/a/b', + 'pyramid.tests.test_scripts.dummy.DummyView.view', 'POST' + ] + self.assertEqual(compare_to, expected) + + def test_single_route_one_view_registered_with_factory(self): + from zope.interface import Interface + from pyramid.interfaces import IRouteRequest + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IView + registry = self._makeRegistry() + + def view():pass + class IMyRoot(Interface): + pass + class IMyRoute(Interface): + pass + registry.registerAdapter(view, + (IViewClassifier, IMyRoute, IMyRoot), + IView, '') + registry.registerUtility(IMyRoute, IRouteRequest, name='a') + command = self._makeOne() + def factory(request): pass + route = dummy.DummyRoute('a', '/a', factory=factory) + mapper = dummy.DummyMapper(route) + command._get_mapper = lambda *arg: mapper + L = [] + command.out = L.append + command.bootstrap = dummy.DummyBootstrap(registry=registry) + result = command.run() + self.assertEqual(result, 0) + self.assertEqual(len(L), 3) + self.assertEqual(L[-1].split()[:3], ['a', '/a', '']) + + def test_single_route_multiview_registered(self): + from zope.interface import Interface + from pyramid.interfaces import IRouteRequest + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IMultiView + + registry = self._makeRegistry() + + def view(): pass + + class IMyRoute(Interface): + pass + + multiview1 = dummy.DummyMultiView( + view, context='context', + view_name='a1' + ) + + registry.registerAdapter( + multiview1, + (IViewClassifier, IMyRoute, Interface), + IMultiView, '' + ) + registry.registerUtility(IMyRoute, IRouteRequest, name='a') + command = self._makeOne() + route = dummy.DummyRoute('a', '/a') + mapper = dummy.DummyMapper(route) + command._get_mapper = lambda *arg: mapper + L = [] + command.out = L.append + command.bootstrap = dummy.DummyBootstrap(registry=registry) + result = command.run() + self.assertEqual(result, 0) + self.assertEqual(len(L), 3) + compare_to = L[-1].split()[:3] + view_module = 'pyramid.tests.test_scripts.dummy' + view_str = '' + ] + self.assertEqual(compare_to, expected) + + def test_route_static_views(self): + from pyramid.renderers import null_renderer as nr + config = self._makeConfig(autocommit=True) + config.add_static_view('static', 'static', cache_max_age=3600) + path2 = os.path.normpath('/var/www/static') + config.add_static_view(name='static2', path=path2) + config.add_static_view( + name='pyramid_scaffold', + path='pyramid:scaffolds/starter/+package+/static' + ) + + command = self._makeOne() + L = [] + command.out = L.append + command.bootstrap = dummy.DummyBootstrap(registry=config.registry) + result = command.run() + self.assertEqual(result, 0) + self.assertEqual(len(L), 5) + + expected = [ + ['__static/', '/static/*subpath', + 'pyramid.tests.test_scripts:static/', '*'], + ['__static2/', '/static2/*subpath', path2 + os.sep, '*'], + ['__pyramid_scaffold/', '/pyramid_scaffold/*subpath', + 'pyramid:scaffolds/starter/+package+/static/', '*'], + ] + + for index, line in enumerate(L[2:]): + data = line.split() + self.assertEqual(data, expected[index]) + + def test_route_no_view(self): + from pyramid.renderers import null_renderer as nr + config = self._makeConfig(autocommit=True) + config.add_route('foo', '/a/b', request_method='POST') + + command = self._makeOne() + L = [] + command.out = L.append + command.bootstrap = dummy.DummyBootstrap(registry=config.registry) + result = command.run() + self.assertEqual(result, 0) + self.assertEqual(len(L), 3) + compare_to = L[-1].split() + expected = [ + 'foo', '/a/b', + '', + 'POST', + ] + self.assertEqual(compare_to, expected) + + def test_route_as_wsgiapp(self): + from pyramid.wsgi import wsgiapp2 + + config1 = self._makeConfig(autocommit=True) + def view1(context, request): return 'view1' + config1.add_route('foo', '/a/b', request_method='POST') + config1.add_view(view=view1, route_name='foo') + + config2 = self._makeConfig(autocommit=True) + config2.add_route('foo', '/a/b', request_method='POST') + config2.add_view( + wsgiapp2(config1.make_wsgi_app()), + route_name='foo', + ) + + command = self._makeOne() + L = [] + command.out = L.append + command.bootstrap = dummy.DummyBootstrap(registry=config2.registry) + result = command.run() + self.assertEqual(result, 0) + self.assertEqual(len(L), 3) + compare_to = L[-1].split() + expected = [ + 'foo', '/a/b', + '', + 'POST', + ] + self.assertEqual(compare_to, expected) + + def test_route_is_get_view_request_method_not_post(self): + from pyramid.renderers import null_renderer as nr + from pyramid.config import not_ + + def view1(context, request): return 'view1' + + config = self._makeConfig(autocommit=True) + config.add_route('foo', '/a/b', request_method='GET') + config.add_view( + route_name='foo', + view=view1, + renderer=nr, + request_method=not_('POST') + ) + + command = self._makeOne() + L = [] + command.out = L.append + command.bootstrap = dummy.DummyBootstrap(registry=config.registry) + result = command.run() + self.assertEqual(result, 0) + self.assertEqual(len(L), 3) + compare_to = L[-1].split() + expected = [ + 'foo', '/a/b', + 'pyramid.tests.test_scripts.test_proutes.view1', + 'GET' + ] + self.assertEqual(compare_to, expected) + + def test_view_request_method_not_post(self): + from pyramid.renderers import null_renderer as nr + from pyramid.config import not_ + + def view1(context, request): return 'view1' + + config = self._makeConfig(autocommit=True) + config.add_route('foo', '/a/b') + config.add_view( + route_name='foo', + view=view1, + renderer=nr, + request_method=not_('POST') + ) + + command = self._makeOne() + L = [] + command.out = L.append + command.bootstrap = dummy.DummyBootstrap(registry=config.registry) + result = command.run() + self.assertEqual(result, 0) + self.assertEqual(len(L), 3) + compare_to = L[-1].split() + expected = [ + 'foo', '/a/b', + 'pyramid.tests.test_scripts.test_proutes.view1', + '!POST,*' + ] + self.assertEqual(compare_to, expected) + + def test_view_glob(self): + from pyramid.renderers import null_renderer as nr + from pyramid.config import not_ + + def view1(context, request): return 'view1' + def view2(context, request): return 'view2' + + config = self._makeConfig(autocommit=True) + config.add_route('foo', '/a/b') + config.add_view( + route_name='foo', + view=view1, + renderer=nr, + request_method=not_('POST') + ) + + config.add_route('bar', '/b/a') + config.add_view( + route_name='bar', + view=view2, + renderer=nr, + request_method=not_('POST') + ) + + command = self._makeOne() + command.args.glob = '*foo*' + + L = [] + command.out = L.append + command.bootstrap = dummy.DummyBootstrap(registry=config.registry) + result = command.run() + self.assertEqual(result, 0) + self.assertEqual(len(L), 3) + compare_to = L[-1].split() + expected = [ + 'foo', '/a/b', + 'pyramid.tests.test_scripts.test_proutes.view1', + '!POST,*' + ] + self.assertEqual(compare_to, expected) + + def test_good_format(self): + from pyramid.renderers import null_renderer as nr + from pyramid.config import not_ + + def view1(context, request): return 'view1' + + config = self._makeConfig(autocommit=True) + config.add_route('foo', '/a/b') + config.add_view( + route_name='foo', + view=view1, + renderer=nr, + request_method=not_('POST') + ) + + command = self._makeOne() + command.args.glob = '*foo*' + command.args.format = 'method,name' + L = [] + command.out = L.append + command.bootstrap = dummy.DummyBootstrap(registry=config.registry) + result = command.run() + self.assertEqual(result, 0) + self.assertEqual(len(L), 3) + compare_to = L[-1].split() + expected = ['!POST,*', 'foo'] + + self.assertEqual(compare_to, expected) + self.assertEqual(L[0].split(), ['Method', 'Name']) + + def test_bad_format(self): + from pyramid.renderers import null_renderer as nr + from pyramid.config import not_ + + def view1(context, request): return 'view1' + + config = self._makeConfig(autocommit=True) + config.add_route('foo', '/a/b') + config.add_view( + route_name='foo', + view=view1, + renderer=nr, + request_method=not_('POST') + ) + + command = self._makeOne() + command.args.glob = '*foo*' + command.args.format = 'predicates,name,pattern' + L = [] + command.out = L.append + command.bootstrap = dummy.DummyBootstrap(registry=config.registry) + expected = ( + "You provided invalid formats ['predicates'], " + "Available formats are ['name', 'pattern', 'view', 'method']" + ) + result = command.run() + self.assertEqual(result, 2) + self.assertEqual(L[0], expected) + + def test_config_format_ini_newlines(self): + from pyramid.renderers import null_renderer as nr + from pyramid.config import not_ + + def view1(context, request): return 'view1' + + config = self._makeConfig(autocommit=True) + config.add_route('foo', '/a/b') + config.add_view( + route_name='foo', + view=view1, + renderer=nr, + request_method=not_('POST') + ) + + command = self._makeOne() + + L = [] + command.out = L.append + command.bootstrap = dummy.DummyBootstrap(registry=config.registry) + command.get_config_loader = dummy.DummyLoader( + {'proutes': {'format': 'method\nname'}}) + + result = command.run() + self.assertEqual(result, 0) + self.assertEqual(len(L), 3) + compare_to = L[-1].split() + expected = ['!POST,*', 'foo'] + + self.assertEqual(compare_to, expected) + self.assertEqual(L[0].split(), ['Method', 'Name']) + + def test_config_format_ini_spaces(self): + from pyramid.renderers import null_renderer as nr + from pyramid.config import not_ + + def view1(context, request): return 'view1' + + config = self._makeConfig(autocommit=True) + config.add_route('foo', '/a/b') + config.add_view( + route_name='foo', + view=view1, + renderer=nr, + request_method=not_('POST') + ) + + command = self._makeOne() + + L = [] + command.out = L.append + command.bootstrap = dummy.DummyBootstrap(registry=config.registry) + command.get_config_loader = dummy.DummyLoader( + {'proutes': {'format': 'method name'}}) + + result = command.run() + self.assertEqual(result, 0) + self.assertEqual(len(L), 3) + compare_to = L[-1].split() + expected = ['!POST,*', 'foo'] + + self.assertEqual(compare_to, expected) + self.assertEqual(L[0].split(), ['Method', 'Name']) + + def test_config_format_ini_commas(self): + from pyramid.renderers import null_renderer as nr + from pyramid.config import not_ + + def view1(context, request): return 'view1' + + config = self._makeConfig(autocommit=True) + config.add_route('foo', '/a/b') + config.add_view( + route_name='foo', + view=view1, + renderer=nr, + request_method=not_('POST') + ) + + command = self._makeOne() + + L = [] + command.out = L.append + command.bootstrap = dummy.DummyBootstrap(registry=config.registry) + command.get_config_loader = dummy.DummyLoader( + {'proutes': {'format': 'method,name'}}) + + result = command.run() + self.assertEqual(result, 0) + self.assertEqual(len(L), 3) + compare_to = L[-1].split() + expected = ['!POST,*', 'foo'] + + self.assertEqual(compare_to, expected) + self.assertEqual(L[0].split(), ['Method', 'Name']) + + def test_static_routes_included_in_list(self): + from pyramid.renderers import null_renderer as nr + + config = self._makeConfig(autocommit=True) + config.add_route('foo', 'http://example.com/bar.aspx', static=True) + + command = self._makeOne() + L = [] + command.out = L.append + command.bootstrap = dummy.DummyBootstrap(registry=config.registry) + result = command.run() + self.assertEqual(result, 0) + self.assertEqual(len(L), 3) + compare_to = L[-1].split() + expected = [ + 'foo', 'http://example.com/bar.aspx', + '', '*', + ] + self.assertEqual(compare_to, expected) + +class Test_main(unittest.TestCase): + def _callFUT(self, argv): + from pyramid.scripts.proutes import main + return main(argv, quiet=True) + + def test_it(self): + result = self._callFUT(['proutes']) + self.assertEqual(result, 2) diff --git a/src/pyramid/tests/test_scripts/test_pserve.py b/src/pyramid/tests/test_scripts/test_pserve.py new file mode 100644 index 000000000..485cf38cb --- /dev/null +++ b/src/pyramid/tests/test_scripts/test_pserve.py @@ -0,0 +1,131 @@ +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() + + def out(self, msg): + self.out_.write(msg) + + def _getTargetClass(self): + from pyramid.scripts.pserve import PServeCommand + return PServeCommand + + def _makeOne(self, *args): + effargs = ['pserve'] + effargs.extend(args) + cmd = self._getTargetClass()(effargs) + cmd.out = self.out + self.loader = dummy.DummyLoader() + cmd._get_config_loader = self.loader + return cmd + + def test_run_no_args(self): + inst = self._makeOne() + result = inst.run() + self.assertEqual(result, 2) + self.assertEqual(self.out_.getvalue(), 'You must give a config file') + + def test_parse_vars_good(self): + inst = self._makeOne('development.ini', 'a=1', 'b=2') + app = dummy.DummyApp() + + def get_app(name, global_conf): + app.name = name + app.global_conf = global_conf + return app + self.loader.get_wsgi_app = get_app + self.loader.server = lambda x: x + + inst.run() + self.assertEqual(app.global_conf, {'a': '1', 'b': '2'}) + + def test_parse_vars_bad(self): + inst = self._makeOne('development.ini', 'a') + self.assertRaises(ValueError, inst.run) + + def test_config_file_finds_watch_files(self): + inst = self._makeOne('development.ini') + loader = self.loader('/base/path.ini') + loader.settings = {'pserve': { + 'watch_files': 'foo\n/baz\npyramid.tests.test_scripts:*.py', + }} + inst.pserve_file_config(loader, global_conf={'a': '1'}) + self.assertEqual(loader.calls[0]['defaults'], { + 'a': '1', + }) + self.assertEqual(inst.watch_files, set([ + os.path.abspath('/base/foo'), + os.path.abspath('/baz'), + os.path.abspath(os.path.join(here, '*.py')), + ])) + + def test_config_file_finds_open_url(self): + inst = self._makeOne('development.ini') + loader = self.loader('/base/path.ini') + loader.settings = {'pserve': { + 'open_url': 'http://127.0.0.1:8080/', + }} + inst.pserve_file_config(loader, global_conf={'a': '1'}) + self.assertEqual(loader.calls[0]['defaults'], { + 'a': '1', + }) + self.assertEqual(inst.open_url, 'http://127.0.0.1:8080/') + + def test_guess_server_url(self): + inst = self._makeOne('development.ini') + loader = self.loader('/base/path.ini') + loader.settings = {'server:foo': { + 'port': '8080', + }} + url = inst.guess_server_url(loader, 'foo', global_conf={'a': '1'}) + self.assertEqual(loader.calls[0]['defaults'], { + 'a': '1', + }) + self.assertEqual(url, 'http://127.0.0.1:8080') + + def test_reload_call_hupper_with_correct_args(self): + from pyramid.scripts import pserve + + class AttrDict(dict): + def __init__(self, *args, **kwargs): + super(AttrDict, self).__init__(*args, **kwargs) + self.__dict__ = self + + def dummy_start_reloader(*args, **kwargs): + dummy_start_reloader.args = args + dummy_start_reloader.kwargs = kwargs + + orig_hupper = pserve.hupper + try: + pserve.hupper = AttrDict(is_active=lambda: False, + start_reloader=dummy_start_reloader) + + inst = self._makeOne('--reload', 'development.ini') + inst.run() + finally: + pserve.hupper = orig_hupper + + self.assertEquals(dummy_start_reloader.args, ('pyramid.scripts.pserve.main',)) + self.assertEquals(dummy_start_reloader.kwargs, { + 'reload_interval': 1, + 'verbose': 1, + 'worker_kwargs': {'argv': ['pserve', '--reload', 'development.ini'], + 'quiet': False}}) + + +class Test_main(unittest.TestCase): + def _callFUT(self, argv): + from pyramid.scripts.pserve import main + return main(argv, quiet=True) + + def test_it(self): + result = self._callFUT(['pserve']) + self.assertEqual(result, 2) diff --git a/src/pyramid/tests/test_scripts/test_pshell.py b/src/pyramid/tests/test_scripts/test_pshell.py new file mode 100644 index 000000000..df664bea9 --- /dev/null +++ b/src/pyramid/tests/test_scripts/test_pshell.py @@ -0,0 +1,398 @@ +import os +import unittest +from pyramid.tests.test_scripts import dummy + + +class TestPShellCommand(unittest.TestCase): + def _getTargetClass(self): + from pyramid.scripts.pshell import PShellCommand + return PShellCommand + + def _makeOne(self, patch_bootstrap=True, patch_loader=True, + patch_args=True, patch_options=True): + cmd = self._getTargetClass()([]) + + if patch_bootstrap: + self.bootstrap = dummy.DummyBootstrap() + cmd.bootstrap = self.bootstrap + if patch_loader: + self.loader = dummy.DummyLoader() + cmd.get_config_loader = self.loader + if patch_args: + class Args(object): pass + self.args = Args() + self.args.config_uri = '/foo/bar/myapp.ini#myapp' + cmd.args.config_uri = self.args.config_uri + if patch_options: + class Options(object): pass + self.options = Options() + self.options.python_shell = '' + self.options.setup = None + self.options.list = None + cmd.options = self.options + + # default to None to prevent side-effects from running tests in + # unknown environments + cmd.pystartup = None + return cmd + + def _makeEntryPoints(self, command, shells): + command.pkg_resources = dummy.DummyPkgResources(shells) + + def test_command_loads_default_shell(self): + command = self._makeOne() + shell = dummy.DummyShell() + self._makeEntryPoints(command, {}) + + command.default_runner = shell + command.run() + self.assertEqual(self.bootstrap.a[0], '/foo/bar/myapp.ini#myapp') + self.assertEqual(shell.env, { + 'app':self.bootstrap.app, 'root':self.bootstrap.root, + 'registry':self.bootstrap.registry, + 'request':self.bootstrap.request, + 'root_factory':self.bootstrap.root_factory, + }) + self.assertTrue(self.bootstrap.closer.called) + self.assertTrue(shell.help) + + def test_command_errors_with_unknown_shell(self): + command = self._makeOne() + out_calls = [] + + def out(msg): + out_calls.append(msg) + + command.out = out + + shell = dummy.DummyShell() + + self._makeEntryPoints(command, {}) + + command.default_runner = shell + command.args.python_shell = 'unknown_python_shell' + result = command.run() + self.assertEqual(result, 1) + self.assertEqual( + out_calls, ['could not find a shell named "unknown_python_shell"'] + ) + self.assertEqual(self.bootstrap.a[0], '/foo/bar/myapp.ini#myapp') + self.assertTrue(self.bootstrap.closer.called) + + def test_command_loads_ipython(self): + command = self._makeOne() + shell = dummy.DummyShell() + bad_shell = dummy.DummyShell() + self._makeEntryPoints( + command, + { + 'ipython': shell, + 'bpython': bad_shell, + } + ) + + command.args.python_shell = 'ipython' + + command.run() + self.assertEqual(self.bootstrap.a[0], '/foo/bar/myapp.ini#myapp') + self.assertEqual(shell.env, { + 'app':self.bootstrap.app, 'root':self.bootstrap.root, + 'registry':self.bootstrap.registry, + 'request':self.bootstrap.request, + 'root_factory':self.bootstrap.root_factory, + }) + self.assertTrue(self.bootstrap.closer.called) + self.assertTrue(shell.help) + + def test_shell_entry_points(self): + command = self._makeOne() + dshell = dummy.DummyShell() + + self._makeEntryPoints( + command, + { + 'ipython': dshell, + 'bpython': dshell, + } + ) + + command.default_runner = None + shell = command.make_shell() + self.assertEqual(shell, dshell) + + def test_shell_override(self): + command = self._makeOne() + ipshell = dummy.DummyShell() + bpshell = dummy.DummyShell() + dshell = dummy.DummyShell() + + self._makeEntryPoints(command, {}) + + command.default_runner = dshell + + shell = command.make_shell() + self.assertEqual(shell, dshell) + + command.args.python_shell = 'ipython' + self.assertRaises(ValueError, command.make_shell) + + self._makeEntryPoints( + command, + { + 'ipython': ipshell, + 'bpython': bpshell, + 'python': dshell, + } + ) + + command.args.python_shell = 'ipython' + shell = command.make_shell() + self.assertEqual(shell, ipshell) + + command.args.python_shell = 'bpython' + shell = command.make_shell() + self.assertEqual(shell, bpshell) + + command.args.python_shell = 'python' + shell = command.make_shell() + self.assertEqual(shell, dshell) + + def test_shell_ordering(self): + command = self._makeOne() + ipshell = dummy.DummyShell() + bpshell = dummy.DummyShell() + dshell = dummy.DummyShell() + + self._makeEntryPoints( + command, + { + 'ipython': ipshell, + 'bpython': bpshell, + 'python': dshell, + } + ) + + command.default_runner = dshell + + command.preferred_shells = ['ipython', 'bpython'] + shell = command.make_shell() + self.assertEqual(shell, ipshell) + + command.preferred_shells = ['bpython', 'python'] + shell = command.make_shell() + self.assertEqual(shell, bpshell) + + command.preferred_shells = ['python', 'ipython'] + shell = command.make_shell() + self.assertEqual(shell, dshell) + + def test_command_loads_custom_items(self): + command = self._makeOne() + model = dummy.Dummy() + user = dummy.Dummy() + self.loader.settings = {'pshell': {'m': model, 'User': user}} + shell = dummy.DummyShell() + command.run(shell) + self.assertEqual(self.bootstrap.a[0], '/foo/bar/myapp.ini#myapp') + self.assertEqual(shell.env, { + 'app':self.bootstrap.app, 'root':self.bootstrap.root, + 'registry':self.bootstrap.registry, + 'request':self.bootstrap.request, + 'root_factory':self.bootstrap.root_factory, + 'm':model, + 'User': user, + }) + self.assertTrue(self.bootstrap.closer.called) + self.assertTrue(shell.help) + + def test_command_setup(self): + command = self._makeOne() + def setup(env): + env['a'] = 1 + env['root'] = 'root override' + env['none'] = None + self.loader.settings = {'pshell': {'setup': setup}} + shell = dummy.DummyShell() + command.run(shell) + self.assertEqual(self.bootstrap.a[0], '/foo/bar/myapp.ini#myapp') + self.assertEqual(shell.env, { + 'app':self.bootstrap.app, 'root':'root override', + 'registry':self.bootstrap.registry, + 'request':self.bootstrap.request, + 'root_factory':self.bootstrap.root_factory, + 'a':1, + 'none': None, + }) + self.assertTrue(self.bootstrap.closer.called) + self.assertTrue(shell.help) + + def test_command_setup_generator(self): + command = self._makeOne() + did_resume_after_yield = {} + def setup(env): + env['a'] = 1 + env['root'] = 'root override' + env['none'] = None + request = env['request'] + yield + did_resume_after_yield['result'] = True + self.assertEqual(request.dummy_attr, 1) + self.loader.settings = {'pshell': {'setup': setup}} + shell = dummy.DummyShell() + command.run(shell) + self.assertEqual(self.bootstrap.a[0], '/foo/bar/myapp.ini#myapp') + self.assertEqual(shell.env, { + 'app':self.bootstrap.app, 'root':'root override', + 'registry':self.bootstrap.registry, + 'request':self.bootstrap.request, + 'root_factory':self.bootstrap.root_factory, + 'a':1, + 'none': None, + }) + self.assertTrue(did_resume_after_yield['result']) + self.assertTrue(self.bootstrap.closer.called) + self.assertTrue(shell.help) + + def test_command_default_shell_option(self): + command = self._makeOne() + ipshell = dummy.DummyShell() + dshell = dummy.DummyShell() + self._makeEntryPoints( + command, + { + 'ipython': ipshell, + 'python': dshell, + } + ) + self.loader.settings = {'pshell': { + 'default_shell': 'bpython python\nipython'}} + command.run() + self.assertEqual(self.bootstrap.a[0], '/foo/bar/myapp.ini#myapp') + self.assertTrue(dshell.called) + + def test_command_loads_check_variable_override_order(self): + command = self._makeOne() + model = dummy.Dummy() + def setup(env): + env['a'] = 1 + env['m'] = 'model override' + env['root'] = 'root override' + self.loader.settings = {'pshell': {'setup': setup, 'm': model}} + shell = dummy.DummyShell() + command.run(shell) + self.assertEqual(self.bootstrap.a[0], '/foo/bar/myapp.ini#myapp') + self.assertEqual(shell.env, { + 'app':self.bootstrap.app, 'root':'root override', + 'registry':self.bootstrap.registry, + 'request':self.bootstrap.request, + 'root_factory':self.bootstrap.root_factory, + 'a':1, 'm':'model override', + }) + self.assertTrue(self.bootstrap.closer.called) + self.assertTrue(shell.help) + + def test_command_loads_setup_from_options(self): + command = self._makeOne() + def setup(env): + env['a'] = 1 + env['root'] = 'root override' + model = dummy.Dummy() + self.loader.settings = {'pshell': {'setup': 'abc', 'm': model}} + command.args.setup = setup + shell = dummy.DummyShell() + command.run(shell) + self.assertEqual(self.bootstrap.a[0], '/foo/bar/myapp.ini#myapp') + self.assertEqual(shell.env, { + 'app':self.bootstrap.app, 'root':'root override', + 'registry':self.bootstrap.registry, + 'request':self.bootstrap.request, + 'root_factory':self.bootstrap.root_factory, + 'a':1, 'm':model, + }) + self.assertTrue(self.bootstrap.closer.called) + self.assertTrue(shell.help) + + def test_command_custom_section_override(self): + command = self._makeOne() + dummy_ = dummy.Dummy() + self.loader.settings = {'pshell': { + 'app': dummy_, 'root': dummy_, 'registry': dummy_, + 'request': dummy_}} + shell = dummy.DummyShell() + command.run(shell) + self.assertEqual(self.bootstrap.a[0], '/foo/bar/myapp.ini#myapp') + self.assertEqual(shell.env, { + 'app':dummy_, 'root':dummy_, 'registry':dummy_, 'request':dummy_, + 'root_factory':self.bootstrap.root_factory, + }) + self.assertTrue(self.bootstrap.closer.called) + self.assertTrue(shell.help) + + def test_command_loads_pythonstartup(self): + command = self._makeOne() + command.pystartup = ( + os.path.abspath( + os.path.join( + os.path.dirname(__file__), + 'pystartup.txt'))) + shell = dummy.DummyShell() + command.run(shell) + self.assertEqual(self.bootstrap.a[0], '/foo/bar/myapp.ini#myapp') + self.assertEqual(shell.env, { + 'app':self.bootstrap.app, 'root':self.bootstrap.root, + 'registry':self.bootstrap.registry, + 'request':self.bootstrap.request, + 'root_factory':self.bootstrap.root_factory, + 'foo':1, + }) + self.assertTrue(self.bootstrap.closer.called) + self.assertTrue(shell.help) + + def test_list_shells(self): + command = self._makeOne() + + dshell = dummy.DummyShell() + out_calls = [] + + def out(msg): + out_calls.append(msg) + + command.out = out + + self._makeEntryPoints( + command, + { + 'ipython': dshell, + 'python': dshell, + } + ) + + command.args.list = True + result = command.run() + self.assertEqual(result, 0) + self.assertEqual(out_calls, [ + 'Available shells:', + ' ipython', + ' python', + ]) + + +class Test_python_shell_runner(unittest.TestCase): + def _callFUT(self, env, help, interact): + from pyramid.scripts.pshell import python_shell_runner + return python_shell_runner(env, help, interact=interact) + + def test_it(self): + interact = dummy.DummyInteractor() + self._callFUT({'foo': 'bar'}, 'a help message', interact) + self.assertEqual(interact.local, {'foo': 'bar'}) + self.assertTrue('a help message' in interact.banner) + +class Test_main(unittest.TestCase): + def _callFUT(self, argv): + from pyramid.scripts.pshell import main + return main(argv, quiet=True) + + def test_it(self): + result = self._callFUT(['pshell']) + self.assertEqual(result, 2) diff --git a/src/pyramid/tests/test_scripts/test_ptweens.py b/src/pyramid/tests/test_scripts/test_ptweens.py new file mode 100644 index 000000000..6907b858d --- /dev/null +++ b/src/pyramid/tests/test_scripts/test_ptweens.py @@ -0,0 +1,62 @@ +import unittest +from pyramid.tests.test_scripts import dummy + +class TestPTweensCommand(unittest.TestCase): + def _getTargetClass(self): + from pyramid.scripts.ptweens import PTweensCommand + return PTweensCommand + + def _makeOne(self): + cmd = self._getTargetClass()([]) + cmd.bootstrap = dummy.DummyBootstrap() + cmd.setup_logging = dummy.dummy_setup_logging() + cmd.args.config_uri = '/foo/bar/myapp.ini#myapp' + return cmd + + def test_command_no_tweens(self): + command = self._makeOne() + command._get_tweens = lambda *arg: None + L = [] + command.out = L.append + result = command.run() + self.assertEqual(result, 0) + self.assertEqual(L, []) + + def test_command_implicit_tweens_only(self): + command = self._makeOne() + tweens = dummy.DummyTweens([('name', 'item')], None) + command._get_tweens = lambda *arg: tweens + L = [] + command.out = L.append + result = command.run() + self.assertEqual(result, 0) + self.assertEqual( + L[0], + '"pyramid.tweens" config value NOT set (implicitly ordered tweens ' + 'used)') + + def test_command_implicit_and_explicit_tweens(self): + command = self._makeOne() + tweens = dummy.DummyTweens([('name', 'item')], [('name2', 'item2')]) + command._get_tweens = lambda *arg: tweens + L = [] + command.out = L.append + result = command.run() + self.assertEqual(result, 0) + self.assertEqual( + L[0], + '"pyramid.tweens" config value set (explicitly ordered tweens used)') + + def test__get_tweens(self): + command = self._makeOne() + registry = dummy.DummyRegistry() + self.assertEqual(command._get_tweens(registry), None) + +class Test_main(unittest.TestCase): + def _callFUT(self, argv): + from pyramid.scripts.ptweens import main + return main(argv, quiet=True) + + def test_it(self): + result = self._callFUT(['ptweens']) + self.assertEqual(result, 2) diff --git a/src/pyramid/tests/test_scripts/test_pviews.py b/src/pyramid/tests/test_scripts/test_pviews.py new file mode 100644 index 000000000..6ec9defbd --- /dev/null +++ b/src/pyramid/tests/test_scripts/test_pviews.py @@ -0,0 +1,501 @@ +import unittest +from pyramid.tests.test_scripts import dummy + +class TestPViewsCommand(unittest.TestCase): + def _getTargetClass(self): + from pyramid.scripts.pviews import PViewsCommand + return PViewsCommand + + def _makeOne(self, registry=None): + cmd = self._getTargetClass()([]) + cmd.bootstrap = dummy.DummyBootstrap(registry=registry) + cmd.setup_logging = dummy.dummy_setup_logging() + cmd.args.config_uri = '/foo/bar/myapp.ini#myapp' + return cmd + + def _makeRequest(self, url, registry): + from pyramid.request import Request + request = Request.blank('/a') + request.registry = registry + return request + + def _register_mapper(self, registry, routes): + from pyramid.interfaces import IRoutesMapper + mapper = dummy.DummyMapper(*routes) + registry.registerUtility(mapper, IRoutesMapper) + + def test__find_view_no_match(self): + from pyramid.registry import Registry + registry = Registry() + self._register_mapper(registry, []) + command = self._makeOne(registry) + request = self._makeRequest('/a', registry) + result = command._find_view(request) + self.assertEqual(result, None) + + def test__find_view_no_match_multiview_registered(self): + from zope.interface import implementer + from zope.interface import providedBy + from pyramid.interfaces import IRequest + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IMultiView + from pyramid.traversal import DefaultRootFactory + from pyramid.registry import Registry + registry = Registry() + @implementer(IMultiView) + class View1(object): + pass + request = dummy.DummyRequest({'PATH_INFO':'/a'}) + root = DefaultRootFactory(request) + root_iface = providedBy(root) + registry.registerAdapter(View1(), + (IViewClassifier, IRequest, root_iface), + IMultiView) + self._register_mapper(registry, []) + command = self._makeOne(registry=registry) + request = self._makeRequest('/x', registry) + result = command._find_view(request) + self.assertEqual(result, None) + + def test__find_view_traversal(self): + from zope.interface import providedBy + from pyramid.interfaces import IRequest + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IView + from pyramid.traversal import DefaultRootFactory + from pyramid.registry import Registry + registry = Registry() + def view1(): pass + request = dummy.DummyRequest({'PATH_INFO':'/a'}) + root = DefaultRootFactory(request) + root_iface = providedBy(root) + registry.registerAdapter(view1, + (IViewClassifier, IRequest, root_iface), + IView, name='a') + self._register_mapper(registry, []) + command = self._makeOne(registry=registry) + request = self._makeRequest('/a', registry) + result = command._find_view(request) + self.assertEqual(result, view1) + + def test__find_view_traversal_multiview(self): + from zope.interface import implementer + from zope.interface import providedBy + from pyramid.interfaces import IRequest + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IMultiView + from pyramid.traversal import DefaultRootFactory + from pyramid.registry import Registry + registry = Registry() + @implementer(IMultiView) + class View1(object): + pass + request = dummy.DummyRequest({'PATH_INFO':'/a'}) + root = DefaultRootFactory(request) + root_iface = providedBy(root) + view = View1() + registry.registerAdapter(view, + (IViewClassifier, IRequest, root_iface), + IMultiView, name='a') + self._register_mapper(registry, []) + command = self._makeOne(registry=registry) + request = self._makeRequest('/a', registry) + result = command._find_view(request) + self.assertEqual(result, view) + + def test__find_view_route_no_multiview(self): + from zope.interface import Interface + from zope.interface import implementer + from pyramid.interfaces import IRouteRequest + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IView + from pyramid.registry import Registry + registry = Registry() + def view():pass + class IMyRoot(Interface): + pass + class IMyRoute(Interface): + pass + registry.registerAdapter(view, + (IViewClassifier, IMyRoute, IMyRoot), + IView, '') + registry.registerUtility(IMyRoute, IRouteRequest, name='a') + @implementer(IMyRoot) + class Factory(object): + def __init__(self, request): + pass + routes = [dummy.DummyRoute('a', '/a', factory=Factory, matchdict={}), + dummy.DummyRoute('b', '/b', factory=Factory)] + self._register_mapper(registry, routes) + command = self._makeOne(registry=registry) + request = self._makeRequest('/a', registry) + result = command._find_view(request) + self.assertEqual(result, view) + + def test__find_view_route_multiview_no_view_registered(self): + from zope.interface import Interface + from zope.interface import implementer + from pyramid.interfaces import IRouteRequest + from pyramid.interfaces import IMultiView + from pyramid.interfaces import IRootFactory + from pyramid.registry import Registry + registry = Registry() + def view1():pass + def view2():pass + class IMyRoot(Interface): + pass + class IMyRoute1(Interface): + pass + class IMyRoute2(Interface): + pass + registry.registerUtility(IMyRoute1, IRouteRequest, name='a') + registry.registerUtility(IMyRoute2, IRouteRequest, name='b') + @implementer(IMyRoot) + class Factory(object): + def __init__(self, request): + pass + registry.registerUtility(Factory, IRootFactory) + routes = [dummy.DummyRoute('a', '/a', matchdict={}), + dummy.DummyRoute('b', '/a', matchdict={})] + self._register_mapper(registry, routes) + command = self._makeOne(registry=registry) + request = self._makeRequest('/a', registry) + result = command._find_view(request) + self.assertTrue(IMultiView.providedBy(result)) + + def test__find_view_route_multiview(self): + from zope.interface import Interface + from zope.interface import implementer + from pyramid.interfaces import IRouteRequest + from pyramid.interfaces import IViewClassifier + from pyramid.interfaces import IView + from pyramid.interfaces import IMultiView + from pyramid.interfaces import IRootFactory + from pyramid.registry import Registry + registry = Registry() + def view1():pass + def view2():pass + class IMyRoot(Interface): + pass + class IMyRoute1(Interface): + pass + class IMyRoute2(Interface): + pass + registry.registerAdapter(view1, + (IViewClassifier, IMyRoute1, IMyRoot), + IView, '') + registry.registerAdapter(view2, + (IViewClassifier, IMyRoute2, IMyRoot), + IView, '') + registry.registerUtility(IMyRoute1, IRouteRequest, name='a') + registry.registerUtility(IMyRoute2, IRouteRequest, name='b') + @implementer(IMyRoot) + class Factory(object): + def __init__(self, request): + pass + registry.registerUtility(Factory, IRootFactory) + routes = [dummy.DummyRoute('a', '/a', matchdict={}), + dummy.DummyRoute('b', '/a', matchdict={})] + self._register_mapper(registry, routes) + command = self._makeOne(registry=registry) + request = self._makeRequest('/a', registry) + result = command._find_view(request) + self.assertTrue(IMultiView.providedBy(result)) + self.assertEqual(len(result.views), 2) + self.assertTrue((None, view1, None) in result.views) + self.assertTrue((None, view2, None) in result.views) + + def test__find_multi_routes_all_match(self): + command = self._makeOne() + def factory(request): pass + routes = [dummy.DummyRoute('a', '/a', factory=factory, matchdict={}), + dummy.DummyRoute('b', '/a', factory=factory, matchdict={})] + mapper = dummy.DummyMapper(*routes) + request = dummy.DummyRequest({'PATH_INFO':'/a'}) + result = command._find_multi_routes(mapper, request) + self.assertEqual(result, [{'match':{}, 'route':routes[0]}, + {'match':{}, 'route':routes[1]}]) + + def test__find_multi_routes_some_match(self): + command = self._makeOne() + def factory(request): pass + routes = [dummy.DummyRoute('a', '/a', factory=factory), + dummy.DummyRoute('b', '/a', factory=factory, matchdict={})] + mapper = dummy.DummyMapper(*routes) + request = dummy.DummyRequest({'PATH_INFO':'/a'}) + result = command._find_multi_routes(mapper, request) + self.assertEqual(result, [{'match':{}, 'route':routes[1]}]) + + def test__find_multi_routes_none_match(self): + command = self._makeOne() + def factory(request): pass + routes = [dummy.DummyRoute('a', '/a', factory=factory), + dummy.DummyRoute('b', '/a', factory=factory)] + mapper = dummy.DummyMapper(*routes) + request = dummy.DummyRequest({'PATH_INFO':'/a'}) + result = command._find_multi_routes(mapper, request) + self.assertEqual(result, []) + + def test_views_command_not_found(self): + from pyramid.registry import Registry + registry = Registry() + command = self._makeOne(registry=registry) + L = [] + command.out = L.append + command._find_view = lambda arg1: None + command.args.config_uri = '/foo/bar/myapp.ini#myapp' + command.args.url = '/a' + result = command.run() + self.assertEqual(result, 0) + self.assertEqual(L[1], 'URL = /a') + self.assertEqual(L[3], ' Not found.') + + def test_views_command_not_found_url_starts_without_slash(self): + from pyramid.registry import Registry + registry = Registry() + command = self._makeOne(registry=registry) + L = [] + command.out = L.append + command._find_view = lambda arg1: None + command.args.config_uri = '/foo/bar/myapp.ini#myapp' + command.args.url = 'a' + result = command.run() + self.assertEqual(result, 0) + self.assertEqual(L[1], 'URL = /a') + self.assertEqual(L[3], ' Not found.') + + def test_views_command_single_view_traversal(self): + from pyramid.registry import Registry + registry = Registry() + command = self._makeOne(registry=registry) + L = [] + command.out = L.append + view = dummy.DummyView(context='context', view_name='a') + command._find_view = lambda arg1: view + command.args.config_uri = '/foo/bar/myapp.ini#myapp' + command.args.url = '/a' + result = command.run() + self.assertEqual(result, 0) + self.assertEqual(L[1], 'URL = /a') + self.assertEqual(L[3], ' context: context') + self.assertEqual(L[4], ' view name: a') + self.assertEqual(L[8], + ' pyramid.tests.test_scripts.dummy.DummyView') + + def test_views_command_single_view_function_traversal(self): + from pyramid.registry import Registry + registry = Registry() + command = self._makeOne(registry=registry) + L = [] + command.out = L.append + def view(): pass + view.__request_attrs__ = {'context': 'context', 'view_name': 'a'} + command._find_view = lambda arg1: view + command.args.config_uri = '/foo/bar/myapp.ini#myapp' + command.args.url = '/a' + result = command.run() + self.assertEqual(result, 0) + self.assertEqual(L[1], 'URL = /a') + self.assertEqual(L[3], ' context: context') + self.assertEqual(L[4], ' view name: a') + self.assertEqual(L[8], + ' pyramid.tests.test_scripts.test_pviews.view') + + def test_views_command_single_view_traversal_with_permission(self): + from pyramid.registry import Registry + registry = Registry() + command = self._makeOne(registry=registry) + L = [] + command.out = L.append + view = dummy.DummyView(context='context', view_name='a') + view.__permission__ = 'test' + command._find_view = lambda arg1: view + command.args.config_uri = '/foo/bar/myapp.ini#myapp' + command.args.url = '/a' + result = command.run() + self.assertEqual(result, 0) + self.assertEqual(L[1], 'URL = /a') + self.assertEqual(L[3], ' context: context') + self.assertEqual(L[4], ' view name: a') + self.assertEqual(L[8], + ' pyramid.tests.test_scripts.dummy.DummyView') + self.assertEqual(L[9], ' required permission = test') + + def test_views_command_single_view_traversal_with_predicates(self): + from pyramid.registry import Registry + registry = Registry() + command = self._makeOne(registry=registry) + L = [] + command.out = L.append + def predicate(): pass + predicate.text = lambda *arg: "predicate = x" + view = dummy.DummyView(context='context', view_name='a') + view.__predicates__ = [predicate] + command._find_view = lambda arg1: view + command.args.config_uri = '/foo/bar/myapp.ini#myapp' + command.args.url = '/a' + result = command.run() + self.assertEqual(result, 0) + self.assertEqual(L[1], 'URL = /a') + self.assertEqual(L[3], ' context: context') + self.assertEqual(L[4], ' view name: a') + self.assertEqual(L[8], + ' pyramid.tests.test_scripts.dummy.DummyView') + self.assertEqual(L[9], ' view predicates (predicate = x)') + + def test_views_command_single_view_route(self): + from pyramid.registry import Registry + registry = Registry() + command = self._makeOne(registry=registry) + L = [] + command.out = L.append + route = dummy.DummyRoute('a', '/a', matchdict={}) + view = dummy.DummyView(context='context', view_name='a', + matched_route=route, subpath='') + command._find_view = lambda arg1: view + command.args.config_uri = '/foo/bar/myapp.ini#myapp' + command.args.url = '/a' + result = command.run() + self.assertEqual(result, 0) + self.assertEqual(L[1], 'URL = /a') + self.assertEqual(L[3], ' context: context') + self.assertEqual(L[4], ' view name: a') + self.assertEqual(L[6], ' Route:') + self.assertEqual(L[8], ' route name: a') + self.assertEqual(L[9], ' route pattern: /a') + self.assertEqual(L[10], ' route path: /a') + self.assertEqual(L[11], ' subpath: ') + self.assertEqual(L[15], + ' pyramid.tests.test_scripts.dummy.DummyView') + + def test_views_command_multi_view_nested(self): + from pyramid.registry import Registry + registry = Registry() + command = self._makeOne(registry=registry) + L = [] + command.out = L.append + view1 = dummy.DummyView(context='context', view_name='a1') + view1.__name__ = 'view1' + view1.__view_attr__ = 'call' + multiview1 = dummy.DummyMultiView(view1, context='context', + view_name='a1') + multiview2 = dummy.DummyMultiView(multiview1, context='context', + view_name='a') + command._find_view = lambda arg1: multiview2 + command.args.config_uri = '/foo/bar/myapp.ini#myapp' + command.args.url = '/a' + result = command.run() + self.assertEqual(result, 0) + self.assertEqual(L[1], 'URL = /a') + self.assertEqual(L[3], ' context: context') + self.assertEqual(L[4], ' view name: a') + self.assertEqual(L[8], + ' pyramid.tests.test_scripts.dummy.DummyMultiView') + self.assertEqual(L[12], + ' pyramid.tests.test_scripts.dummy.view1.call') + + def test_views_command_single_view_route_with_route_predicates(self): + from pyramid.registry import Registry + registry = Registry() + command = self._makeOne(registry=registry) + L = [] + command.out = L.append + def predicate(): pass + predicate.text = lambda *arg: "predicate = x" + route = dummy.DummyRoute('a', '/a', matchdict={}, predicate=predicate) + view = dummy.DummyView(context='context', view_name='a', + matched_route=route, subpath='') + command._find_view = lambda arg1: view + command.args.config_uri = '/foo/bar/myapp.ini#myapp' + command.args.url = '/a' + result = command.run() + self.assertEqual(result, 0) + self.assertEqual(L[1], 'URL = /a') + self.assertEqual(L[3], ' context: context') + self.assertEqual(L[4], ' view name: a') + self.assertEqual(L[6], ' Route:') + self.assertEqual(L[8], ' route name: a') + self.assertEqual(L[9], ' route pattern: /a') + self.assertEqual(L[10], ' route path: /a') + self.assertEqual(L[11], ' subpath: ') + self.assertEqual(L[12], ' route predicates (predicate = x)') + self.assertEqual(L[16], + ' pyramid.tests.test_scripts.dummy.DummyView') + + def test_views_command_multiview(self): + from pyramid.registry import Registry + registry = Registry() + command = self._makeOne(registry=registry) + L = [] + command.out = L.append + view = dummy.DummyView(context='context') + view.__name__ = 'view' + view.__view_attr__ = 'call' + multiview = dummy.DummyMultiView(view, context='context', view_name='a') + command._find_view = lambda arg1: multiview + command.args.config_uri = '/foo/bar/myapp.ini#myapp' + command.args.url = '/a' + result = command.run() + self.assertEqual(result, 0) + self.assertEqual(L[1], 'URL = /a') + self.assertEqual(L[3], ' context: context') + self.assertEqual(L[4], ' view name: a') + self.assertEqual(L[8], + ' pyramid.tests.test_scripts.dummy.view.call') + + def test_views_command_multiview_with_permission(self): + from pyramid.registry import Registry + registry = Registry() + command = self._makeOne(registry=registry) + L = [] + command.out = L.append + view = dummy.DummyView(context='context') + view.__name__ = 'view' + view.__view_attr__ = 'call' + view.__permission__ = 'test' + multiview = dummy.DummyMultiView(view, context='context', view_name='a') + command._find_view = lambda arg1: multiview + command.args.config_uri = '/foo/bar/myapp.ini#myapp' + command.args.url = '/a' + result = command.run() + self.assertEqual(result, 0) + self.assertEqual(L[1], 'URL = /a') + self.assertEqual(L[3], ' context: context') + self.assertEqual(L[4], ' view name: a') + self.assertEqual(L[8], + ' pyramid.tests.test_scripts.dummy.view.call') + self.assertEqual(L[9], ' required permission = test') + + def test_views_command_multiview_with_predicates(self): + from pyramid.registry import Registry + registry = Registry() + command = self._makeOne(registry=registry) + L = [] + command.out = L.append + def predicate(): pass + predicate.text = lambda *arg: "predicate = x" + view = dummy.DummyView(context='context') + view.__name__ = 'view' + view.__view_attr__ = 'call' + view.__predicates__ = [predicate] + multiview = dummy.DummyMultiView(view, context='context', view_name='a') + command._find_view = lambda arg1: multiview + command.args.config_uri = '/foo/bar/myapp.ini#myapp' + command.args.url = '/a' + result = command.run() + self.assertEqual(result, 0) + self.assertEqual(L[1], 'URL = /a') + self.assertEqual(L[3], ' context: context') + self.assertEqual(L[4], ' view name: a') + self.assertEqual(L[8], + ' pyramid.tests.test_scripts.dummy.view.call') + self.assertEqual(L[9], ' view predicates (predicate = x)') + +class Test_main(unittest.TestCase): + def _callFUT(self, argv): + from pyramid.scripts.pviews import main + return main(argv, quiet=True) + + def test_it(self): + result = self._callFUT(['pviews']) + self.assertEqual(result, 2) diff --git a/src/pyramid/tests/test_security.py b/src/pyramid/tests/test_security.py new file mode 100644 index 000000000..e5399ecdf --- /dev/null +++ b/src/pyramid/tests/test_security.py @@ -0,0 +1,549 @@ +import unittest + +from pyramid import testing + +class TestAllPermissionsList(unittest.TestCase): + def setUp(self): + testing.setUp() + + def tearDown(self): + testing.tearDown() + + def _getTargetClass(self): + from pyramid.security import AllPermissionsList + return AllPermissionsList + + def _makeOne(self): + return self._getTargetClass()() + + def test_equality_w_self(self): + thing = self._makeOne() + self.assertTrue(thing.__eq__(thing)) + + def test_equality_w_other_instances_of_class(self): + thing = self._makeOne() + other = self._makeOne() + self.assertTrue(thing.__eq__(other)) + + def test_equality_miss(self): + thing = self._makeOne() + other = object() + self.assertFalse(thing.__eq__(other)) + + def test_contains_w_string(self): + thing = self._makeOne() + self.assertTrue('anything' in thing) + + def test_contains_w_object(self): + thing = self._makeOne() + self.assertTrue(object() in thing) + + def test_iterable(self): + thing = self._makeOne() + self.assertEqual(list(thing), []) + + def test_singleton(self): + from pyramid.security import ALL_PERMISSIONS + self.assertEqual(ALL_PERMISSIONS.__class__, self._getTargetClass()) + +class TestAllowed(unittest.TestCase): + def _getTargetClass(self): + from pyramid.security import Allowed + return Allowed + + def _makeOne(self, *arg, **kw): + klass = self._getTargetClass() + return klass(*arg, **kw) + + def test_it(self): + allowed = self._makeOne('hello') + self.assertEqual(allowed.msg, 'hello') + self.assertEqual(allowed, True) + self.assertTrue(allowed) + self.assertEqual(str(allowed), 'hello') + self.assertTrue('" in repr(allowed)) + +class TestDenied(unittest.TestCase): + def _getTargetClass(self): + from pyramid.security import Denied + return Denied + + def _makeOne(self, *arg, **kw): + klass = self._getTargetClass() + return klass(*arg, **kw) + + def test_it(self): + denied = self._makeOne('hello') + self.assertEqual(denied.msg, 'hello') + self.assertEqual(denied, False) + self.assertFalse(denied) + self.assertEqual(str(denied), 'hello') + self.assertTrue('" in repr(denied)) + +class TestACLAllowed(unittest.TestCase): + def _getTargetClass(self): + from pyramid.security import ACLAllowed + return ACLAllowed + + def _makeOne(self, *arg, **kw): + klass = self._getTargetClass() + return klass(*arg, **kw) + + def test_it(self): + from pyramid.security import Allowed + msg = ("ACLAllowed permission 'permission' via ACE 'ace' in ACL 'acl' " + "on context 'ctx' for principals 'principals'") + allowed = self._makeOne('ace', 'acl', 'permission', 'principals', 'ctx') + self.assertIsInstance(allowed, Allowed) + self.assertTrue(msg in allowed.msg) + self.assertEqual(allowed, True) + self.assertTrue(allowed) + self.assertEqual(str(allowed), msg) + self.assertTrue('" % msg in repr(allowed)) + +class TestACLDenied(unittest.TestCase): + def _getTargetClass(self): + from pyramid.security import ACLDenied + return ACLDenied + + def _makeOne(self, *arg, **kw): + klass = self._getTargetClass() + return klass(*arg, **kw) + + def test_it(self): + from pyramid.security import Denied + msg = ("ACLDenied permission 'permission' via ACE 'ace' in ACL 'acl' " + "on context 'ctx' for principals 'principals'") + denied = self._makeOne('ace', 'acl', 'permission', 'principals', 'ctx') + self.assertIsInstance(denied, Denied) + self.assertTrue(msg in denied.msg) + self.assertEqual(denied, False) + self.assertFalse(denied) + self.assertEqual(str(denied), msg) + self.assertTrue('" % msg in repr(denied)) + +class TestPrincipalsAllowedByPermission(unittest.TestCase): + def setUp(self): + testing.setUp() + + def tearDown(self): + testing.tearDown() + + def _callFUT(self, *arg): + from pyramid.security import principals_allowed_by_permission + return principals_allowed_by_permission(*arg) + + def test_no_authorization_policy(self): + from pyramid.security import Everyone + context = DummyContext() + result = self._callFUT(context, 'view') + self.assertEqual(result, [Everyone]) + + def test_with_authorization_policy(self): + from pyramid.threadlocal import get_current_registry + registry = get_current_registry() + _registerAuthorizationPolicy(registry, 'yo') + context = DummyContext() + result = self._callFUT(context, 'view') + self.assertEqual(result, 'yo') + +class TestRemember(unittest.TestCase): + def setUp(self): + testing.setUp() + + def tearDown(self): + testing.tearDown() + + def _callFUT(self, *arg, **kwarg): + from pyramid.security import remember + return remember(*arg, **kwarg) + + def test_no_authentication_policy(self): + request = _makeRequest() + result = self._callFUT(request, 'me') + self.assertEqual(result, []) + + def test_with_authentication_policy(self): + request = _makeRequest() + registry = request.registry + _registerAuthenticationPolicy(registry, 'yo') + result = self._callFUT(request, 'me') + self.assertEqual(result, [('X-Pyramid-Test', 'me')]) + + def test_with_authentication_policy_no_reg_on_request(self): + from pyramid.threadlocal import get_current_registry + registry = get_current_registry() + request = _makeRequest() + del request.registry + _registerAuthenticationPolicy(registry, 'yo') + result = self._callFUT(request, 'me') + self.assertEqual(result, [('X-Pyramid-Test', 'me')]) + + def test_with_missing_arg(self): + request = _makeRequest() + registry = request.registry + _registerAuthenticationPolicy(registry, 'yo') + self.assertRaises(TypeError, lambda: self._callFUT(request)) + +class TestForget(unittest.TestCase): + def setUp(self): + testing.setUp() + + def tearDown(self): + testing.tearDown() + + def _callFUT(self, *arg): + from pyramid.security import forget + return forget(*arg) + + def test_no_authentication_policy(self): + request = _makeRequest() + result = self._callFUT(request) + self.assertEqual(result, []) + + def test_with_authentication_policy(self): + request = _makeRequest() + _registerAuthenticationPolicy(request.registry, 'yo') + result = self._callFUT(request) + self.assertEqual(result, [('X-Pyramid-Test', 'logout')]) + + def test_with_authentication_policy_no_reg_on_request(self): + from pyramid.threadlocal import get_current_registry + registry = get_current_registry() + request = _makeRequest() + del request.registry + _registerAuthenticationPolicy(registry, 'yo') + result = self._callFUT(request) + self.assertEqual(result, [('X-Pyramid-Test', 'logout')]) + +class TestViewExecutionPermitted(unittest.TestCase): + def setUp(self): + testing.setUp() + + def tearDown(self): + testing.tearDown() + + def _callFUT(self, *arg, **kw): + from pyramid.security import view_execution_permitted + return view_execution_permitted(*arg, **kw) + + def _registerSecuredView(self, view_name, allow=True): + from pyramid.threadlocal import get_current_registry + from zope.interface import Interface + from pyramid.interfaces import ISecuredView + from pyramid.interfaces import IViewClassifier + class Checker(object): + def __permitted__(self, context, request): + self.context = context + self.request = request + return allow + checker = Checker() + reg = get_current_registry() + reg.registerAdapter(checker, (IViewClassifier, Interface, Interface), + ISecuredView, view_name) + return checker + + def test_no_permission(self): + from zope.interface import Interface + from pyramid.threadlocal import get_current_registry + from pyramid.interfaces import ISettings + from pyramid.interfaces import IView + from pyramid.interfaces import IViewClassifier + settings = dict(debug_authorization=True) + reg = get_current_registry() + reg.registerUtility(settings, ISettings) + context = DummyContext() + request = testing.DummyRequest({}) + class DummyView(object): + pass + view = DummyView() + reg.registerAdapter(view, (IViewClassifier, Interface, Interface), + IView, '') + result = self._callFUT(context, request, '') + msg = result.msg + self.assertTrue("Allowed: view name '' in context" in msg) + self.assertTrue('(no permission defined)' in msg) + self.assertEqual(result, True) + + def test_no_view_registered(self): + from pyramid.threadlocal import get_current_registry + from pyramid.interfaces import ISettings + settings = dict(debug_authorization=True) + reg = get_current_registry() + reg.registerUtility(settings, ISettings) + context = DummyContext() + request = testing.DummyRequest({}) + self.assertRaises(TypeError, self._callFUT, context, request, '') + + def test_with_permission(self): + from zope.interface import Interface + from zope.interface import directlyProvides + from pyramid.interfaces import IRequest + class IContext(Interface): + pass + context = DummyContext() + directlyProvides(context, IContext) + self._registerSecuredView('', True) + request = testing.DummyRequest({}) + directlyProvides(request, IRequest) + result = self._callFUT(context, request, '') + self.assertTrue(result) + +class TestAuthenticatedUserId(unittest.TestCase): + def setUp(self): + testing.setUp() + + def tearDown(self): + testing.tearDown() + + def test_backward_compat_delegates_to_mixin(self): + from zope.deprecation import __show__ + try: + __show__.off() + request = _makeFakeRequest() + from pyramid.security import authenticated_userid + self.assertEqual( + authenticated_userid(request), + 'authenticated_userid' + ) + finally: + __show__.on() + + def test_no_authentication_policy(self): + request = _makeRequest() + self.assertEqual(request.authenticated_userid, None) + + def test_with_authentication_policy(self): + request = _makeRequest() + _registerAuthenticationPolicy(request.registry, 'yo') + self.assertEqual(request.authenticated_userid, 'yo') + + def test_with_authentication_policy_no_reg_on_request(self): + from pyramid.threadlocal import get_current_registry + registry = get_current_registry() + request = _makeRequest() + del request.registry + _registerAuthenticationPolicy(registry, 'yo') + self.assertEqual(request.authenticated_userid, 'yo') + +class TestUnAuthenticatedUserId(unittest.TestCase): + def setUp(self): + testing.setUp() + + def tearDown(self): + testing.tearDown() + + def test_backward_compat_delegates_to_mixin(self): + from zope.deprecation import __show__ + try: + __show__.off() + request = _makeFakeRequest() + from pyramid.security import unauthenticated_userid + self.assertEqual( + unauthenticated_userid(request), + 'unauthenticated_userid', + ) + finally: + __show__.on() + + def test_no_authentication_policy(self): + request = _makeRequest() + self.assertEqual(request.unauthenticated_userid, None) + + def test_with_authentication_policy(self): + request = _makeRequest() + _registerAuthenticationPolicy(request.registry, 'yo') + self.assertEqual(request.unauthenticated_userid, 'yo') + + def test_with_authentication_policy_no_reg_on_request(self): + from pyramid.threadlocal import get_current_registry + registry = get_current_registry() + request = _makeRequest() + del request.registry + _registerAuthenticationPolicy(registry, 'yo') + self.assertEqual(request.unauthenticated_userid, 'yo') + +class TestEffectivePrincipals(unittest.TestCase): + def setUp(self): + testing.setUp() + + def tearDown(self): + testing.tearDown() + + def test_backward_compat_delegates_to_mixin(self): + request = _makeFakeRequest() + from zope.deprecation import __show__ + try: + __show__.off() + from pyramid.security import effective_principals + self.assertEqual( + effective_principals(request), + 'effective_principals' + ) + finally: + __show__.on() + + def test_no_authentication_policy(self): + from pyramid.security import Everyone + request = _makeRequest() + self.assertEqual(request.effective_principals, [Everyone]) + + def test_with_authentication_policy(self): + request = _makeRequest() + _registerAuthenticationPolicy(request.registry, 'yo') + self.assertEqual(request.effective_principals, 'yo') + + def test_with_authentication_policy_no_reg_on_request(self): + from pyramid.threadlocal import get_current_registry + registry = get_current_registry() + request = _makeRequest() + del request.registry + _registerAuthenticationPolicy(registry, 'yo') + self.assertEqual(request.effective_principals, 'yo') + +class TestHasPermission(unittest.TestCase): + def setUp(self): + testing.setUp() + + def tearDown(self): + testing.tearDown() + + def _makeOne(self): + from pyramid.security import AuthorizationAPIMixin + from pyramid.registry import Registry + mixin = AuthorizationAPIMixin() + mixin.registry = Registry() + mixin.context = object() + return mixin + + def test_delegates_to_mixin(self): + from zope.deprecation import __show__ + try: + __show__.off() + mixin = self._makeOne() + from pyramid.security import has_permission + self.called_has_permission = False + + def mocked_has_permission(*args, **kw): + self.called_has_permission = True + + mixin.has_permission = mocked_has_permission + has_permission('view', object(), mixin) + self.assertTrue(self.called_has_permission) + finally: + __show__.on() + + def test_no_authentication_policy(self): + request = self._makeOne() + result = request.has_permission('view') + self.assertTrue(result) + self.assertEqual(result.msg, 'No authentication policy in use.') + + def test_with_no_authorization_policy(self): + request = self._makeOne() + _registerAuthenticationPolicy(request.registry, None) + self.assertRaises(ValueError, + request.has_permission, 'view', context=None) + + def test_with_authn_and_authz_policies_registered(self): + request = self._makeOne() + _registerAuthenticationPolicy(request.registry, None) + _registerAuthorizationPolicy(request.registry, 'yo') + self.assertEqual(request.has_permission('view', context=None), 'yo') + + def test_with_no_reg_on_request(self): + from pyramid.threadlocal import get_current_registry + registry = get_current_registry() + request = self._makeOne() + del request.registry + _registerAuthenticationPolicy(registry, None) + _registerAuthorizationPolicy(registry, 'yo') + self.assertEqual(request.has_permission('view'), 'yo') + + def test_with_no_context_passed(self): + request = self._makeOne() + self.assertTrue(request.has_permission('view')) + + def test_with_no_context_passed_or_on_request(self): + request = self._makeOne() + del request.context + self.assertRaises(AttributeError, request.has_permission, 'view') + +_TEST_HEADER = 'X-Pyramid-Test' + +class DummyContext: + def __init__(self, *arg, **kw): + self.__dict__.update(kw) + +class DummyAuthenticationPolicy: + def __init__(self, result): + self.result = result + + def effective_principals(self, request): + return self.result + + def unauthenticated_userid(self, request): + return self.result + + def authenticated_userid(self, request): + return self.result + + def remember(self, request, userid, **kw): + headers = [(_TEST_HEADER, userid)] + self._header_remembered = headers[0] + return headers + + def forget(self, request): + headers = [(_TEST_HEADER, 'logout')] + self._header_forgotten = headers[0] + return headers + +class DummyAuthorizationPolicy: + def __init__(self, result): + self.result = result + + def permits(self, context, principals, permission): + return self.result + + def principals_allowed_by_permission(self, context, permission): + return self.result + +def _registerAuthenticationPolicy(reg, result): + from pyramid.interfaces import IAuthenticationPolicy + policy = DummyAuthenticationPolicy(result) + reg.registerUtility(policy, IAuthenticationPolicy) + return policy + +def _registerAuthorizationPolicy(reg, result): + from pyramid.interfaces import IAuthorizationPolicy + policy = DummyAuthorizationPolicy(result) + reg.registerUtility(policy, IAuthorizationPolicy) + return policy + +def _makeRequest(): + from pyramid.registry import Registry + request = testing.DummyRequest(environ={}) + request.registry = Registry() + request.context = object() + return request + +def _makeFakeRequest(): + class FakeRequest(testing.DummyRequest): + @property + def authenticated_userid(req): + return 'authenticated_userid' + + @property + def unauthenticated_userid(req): + return 'unauthenticated_userid' + + @property + def effective_principals(req): + return 'effective_principals' + + return FakeRequest({}) + diff --git a/src/pyramid/tests/test_session.py b/src/pyramid/tests/test_session.py new file mode 100644 index 000000000..3585ed635 --- /dev/null +++ b/src/pyramid/tests/test_session.py @@ -0,0 +1,754 @@ +import base64 +import json +import unittest +from pyramid import testing +from pyramid.compat import pickle + +class SharedCookieSessionTests(object): + + def test_ctor_no_cookie(self): + request = testing.DummyRequest() + session = self._makeOne(request) + self.assertEqual(dict(session), {}) + + def test_instance_conforms(self): + from zope.interface.verify import verifyObject + from pyramid.interfaces import ISession + request = testing.DummyRequest() + session = self._makeOne(request) + verifyObject(ISession, session) + + def test_ctor_with_cookie_still_valid(self): + import time + request = testing.DummyRequest() + cookieval = self._serialize((time.time(), 0, {'state': 1})) + request.cookies['session'] = cookieval + session = self._makeOne(request) + self.assertEqual(dict(session), {'state':1}) + + def test_ctor_with_cookie_expired(self): + request = testing.DummyRequest() + cookieval = self._serialize((0, 0, {'state': 1})) + request.cookies['session'] = cookieval + session = self._makeOne(request) + self.assertEqual(dict(session), {}) + + def test_ctor_with_bad_cookie_cannot_deserialize(self): + request = testing.DummyRequest() + request.cookies['session'] = 'abc' + session = self._makeOne(request) + self.assertEqual(dict(session), {}) + + def test_ctor_with_bad_cookie_not_tuple(self): + request = testing.DummyRequest() + cookieval = self._serialize('abc') + request.cookies['session'] = cookieval + session = self._makeOne(request) + self.assertEqual(dict(session), {}) + + def test_timeout(self): + import time + request = testing.DummyRequest() + cookieval = self._serialize((time.time() - 5, 0, {'state': 1})) + request.cookies['session'] = cookieval + session = self._makeOne(request, timeout=1) + self.assertEqual(dict(session), {}) + + def test_timeout_never(self): + import time + request = testing.DummyRequest() + LONG_TIME = 31536000 + cookieval = self._serialize((time.time() + LONG_TIME, 0, {'state': 1})) + request.cookies['session'] = cookieval + session = self._makeOne(request, timeout=None) + self.assertEqual(dict(session), {'state': 1}) + + def test_timeout_str(self): + import time + request = testing.DummyRequest() + cookieval = self._serialize((time.time() - 5, 0, {'state': 1})) + request.cookies['session'] = cookieval + session = self._makeOne(request, timeout='1') + self.assertEqual(dict(session), {}) + + def test_timeout_invalid(self): + request = testing.DummyRequest() + self.assertRaises(ValueError, self._makeOne, request, timeout='Invalid value') + + def test_changed(self): + request = testing.DummyRequest() + session = self._makeOne(request) + self.assertEqual(session.changed(), None) + self.assertTrue(session._dirty) + + def test_invalidate(self): + request = testing.DummyRequest() + session = self._makeOne(request) + session['a'] = 1 + self.assertEqual(session.invalidate(), None) + self.assertFalse('a' in session) + + def test_reissue_triggered(self): + import time + request = testing.DummyRequest() + cookieval = self._serialize((time.time() - 2, 0, {'state': 1})) + request.cookies['session'] = cookieval + session = self._makeOne(request) + self.assertEqual(session['state'], 1) + self.assertTrue(session._dirty) + + def test__set_cookie_on_exception(self): + request = testing.DummyRequest() + request.exception = True + session = self._makeOne(request) + session._cookie_on_exception = False + response = DummyResponse() + self.assertEqual(session._set_cookie(response), False) + + def test__set_cookie_on_exception_no_request_exception(self): + import webob + request = testing.DummyRequest() + request.exception = None + session = self._makeOne(request) + session._cookie_on_exception = False + response = webob.Response() + self.assertEqual(session._set_cookie(response), True) + self.assertEqual(response.headerlist[-1][0], 'Set-Cookie') + + def test__set_cookie_cookieval_too_long(self): + request = testing.DummyRequest() + session = self._makeOne(request) + session['abc'] = 'x'*100000 + response = DummyResponse() + self.assertRaises(ValueError, session._set_cookie, response) + + def test__set_cookie_real_webob_response(self): + import webob + request = testing.DummyRequest() + session = self._makeOne(request) + session['abc'] = 'x' + response = webob.Response() + self.assertEqual(session._set_cookie(response), True) + self.assertEqual(response.headerlist[-1][0], 'Set-Cookie') + + def test__set_cookie_options(self): + from pyramid.response import Response + request = testing.DummyRequest() + request.exception = None + session = self._makeOne(request, + cookie_name='abc', + path='/foo', + domain='localhost', + secure=True, + httponly=True, + ) + session['abc'] = 'x' + response = Response() + self.assertEqual(session._set_cookie(response), True) + cookieval = response.headerlist[-1][1] + val, domain, path, secure, httponly, samesite = [x.strip() for x in + cookieval.split(';')] + self.assertTrue(val.startswith('abc=')) + self.assertEqual(domain, 'Domain=localhost') + self.assertEqual(path, 'Path=/foo') + self.assertEqual(secure, 'secure') + self.assertEqual(httponly, 'HttpOnly') + self.assertEqual(samesite, 'SameSite=Lax') + + def test_flash_default(self): + request = testing.DummyRequest() + session = self._makeOne(request) + session.flash('msg1') + session.flash('msg2') + self.assertEqual(session['_f_'], ['msg1', 'msg2']) + + def test_flash_allow_duplicate_false(self): + request = testing.DummyRequest() + session = self._makeOne(request) + session.flash('msg1') + session.flash('msg1', allow_duplicate=False) + self.assertEqual(session['_f_'], ['msg1']) + + def test_flash_allow_duplicate_true_and_msg_not_in_storage(self): + request = testing.DummyRequest() + session = self._makeOne(request) + session.flash('msg1', allow_duplicate=True) + self.assertEqual(session['_f_'], ['msg1']) + + def test_flash_allow_duplicate_false_and_msg_not_in_storage(self): + request = testing.DummyRequest() + session = self._makeOne(request) + session.flash('msg1', allow_duplicate=False) + self.assertEqual(session['_f_'], ['msg1']) + + def test_flash_mixed(self): + request = testing.DummyRequest() + session = self._makeOne(request) + session.flash('warn1', 'warn') + session.flash('warn2', 'warn') + session.flash('err1', 'error') + session.flash('err2', 'error') + self.assertEqual(session['_f_warn'], ['warn1', 'warn2']) + + def test_pop_flash_default_queue(self): + request = testing.DummyRequest() + session = self._makeOne(request) + queue = ['one', 'two'] + session['_f_'] = queue + result = session.pop_flash() + self.assertEqual(result, queue) + self.assertEqual(session.get('_f_'), None) + + def test_pop_flash_nodefault_queue(self): + request = testing.DummyRequest() + session = self._makeOne(request) + queue = ['one', 'two'] + session['_f_error'] = queue + result = session.pop_flash('error') + self.assertEqual(result, queue) + self.assertEqual(session.get('_f_error'), None) + + def test_peek_flash_default_queue(self): + request = testing.DummyRequest() + session = self._makeOne(request) + queue = ['one', 'two'] + session['_f_'] = queue + result = session.peek_flash() + self.assertEqual(result, queue) + self.assertEqual(session.get('_f_'), queue) + + def test_peek_flash_nodefault_queue(self): + request = testing.DummyRequest() + session = self._makeOne(request) + queue = ['one', 'two'] + session['_f_error'] = queue + result = session.peek_flash('error') + self.assertEqual(result, queue) + self.assertEqual(session.get('_f_error'), queue) + + def test_new_csrf_token(self): + request = testing.DummyRequest() + session = self._makeOne(request) + token = session.new_csrf_token() + self.assertEqual(token, session['_csrft_']) + + def test_get_csrf_token(self): + request = testing.DummyRequest() + session = self._makeOne(request) + session['_csrft_'] = 'token' + token = session.get_csrf_token() + self.assertEqual(token, 'token') + self.assertTrue('_csrft_' in session) + + def test_get_csrf_token_new(self): + request = testing.DummyRequest() + session = self._makeOne(request) + token = session.get_csrf_token() + self.assertTrue(token) + self.assertTrue('_csrft_' in session) + + def test_no_set_cookie_with_exception(self): + import webob + request = testing.DummyRequest() + request.exception = True + session = self._makeOne(request, set_on_exception=False) + session['a'] = 1 + callbacks = request.response_callbacks + self.assertEqual(len(callbacks), 1) + response = webob.Response() + result = callbacks[0](request, response) + self.assertEqual(result, None) + self.assertFalse('Set-Cookie' in dict(response.headerlist)) + + def test_set_cookie_with_exception(self): + import webob + request = testing.DummyRequest() + request.exception = True + session = self._makeOne(request) + session['a'] = 1 + callbacks = request.response_callbacks + self.assertEqual(len(callbacks), 1) + response = webob.Response() + result = callbacks[0](request, response) + self.assertEqual(result, None) + self.assertTrue('Set-Cookie' in dict(response.headerlist)) + + def test_cookie_is_set(self): + import webob + request = testing.DummyRequest() + session = self._makeOne(request) + session['a'] = 1 + callbacks = request.response_callbacks + self.assertEqual(len(callbacks), 1) + response = webob.Response() + result = callbacks[0](request, response) + self.assertEqual(result, None) + self.assertTrue('Set-Cookie' in dict(response.headerlist)) + +class TestBaseCookieSession(SharedCookieSessionTests, unittest.TestCase): + def _makeOne(self, request, **kw): + from pyramid.session import BaseCookieSessionFactory + serializer = DummySerializer() + return BaseCookieSessionFactory(serializer, **kw)(request) + + def _serialize(self, value): + return base64.b64encode(json.dumps(value).encode('utf-8')) + + def test_reissue_not_triggered(self): + import time + request = testing.DummyRequest() + cookieval = self._serialize((time.time(), 0, {'state': 1})) + request.cookies['session'] = cookieval + session = self._makeOne(request, reissue_time=1) + self.assertEqual(session['state'], 1) + self.assertFalse(session._dirty) + + def test_reissue_never(self): + request = testing.DummyRequest() + cookieval = self._serialize((0, 0, {'state': 1})) + request.cookies['session'] = cookieval + session = self._makeOne(request, reissue_time=None, timeout=None) + self.assertEqual(session['state'], 1) + self.assertFalse(session._dirty) + + def test_reissue_str_triggered(self): + import time + request = testing.DummyRequest() + cookieval = self._serialize((time.time() - 2, 0, {'state': 1})) + request.cookies['session'] = cookieval + session = self._makeOne(request, reissue_time='0') + self.assertEqual(session['state'], 1) + self.assertTrue(session._dirty) + + def test_reissue_invalid(self): + request = testing.DummyRequest() + self.assertRaises(ValueError, self._makeOne, request, reissue_time='invalid value') + + def test_cookie_max_age_invalid(self): + request = testing.DummyRequest() + self.assertRaises(ValueError, self._makeOne, request, max_age='invalid value') + +class TestSignedCookieSession(SharedCookieSessionTests, unittest.TestCase): + def _makeOne(self, request, **kw): + from pyramid.session import SignedCookieSessionFactory + kw.setdefault('secret', 'secret') + return SignedCookieSessionFactory(**kw)(request) + + def _serialize(self, value, salt=b'pyramid.session.', hashalg='sha512'): + import base64 + import hashlib + import hmac + import pickle + + digestmod = lambda: hashlib.new(hashalg) + cstruct = pickle.dumps(value, pickle.HIGHEST_PROTOCOL) + sig = hmac.new(salt + b'secret', cstruct, digestmod).digest() + return base64.urlsafe_b64encode(sig + cstruct).rstrip(b'=') + + def test_reissue_not_triggered(self): + import time + request = testing.DummyRequest() + cookieval = self._serialize((time.time(), 0, {'state': 1})) + request.cookies['session'] = cookieval + session = self._makeOne(request, reissue_time=1) + self.assertEqual(session['state'], 1) + self.assertFalse(session._dirty) + + def test_reissue_never(self): + request = testing.DummyRequest() + cookieval = self._serialize((0, 0, {'state': 1})) + request.cookies['session'] = cookieval + session = self._makeOne(request, reissue_time=None, timeout=None) + self.assertEqual(session['state'], 1) + self.assertFalse(session._dirty) + + def test_reissue_str_triggered(self): + import time + request = testing.DummyRequest() + cookieval = self._serialize((time.time() - 2, 0, {'state': 1})) + request.cookies['session'] = cookieval + session = self._makeOne(request, reissue_time='0') + self.assertEqual(session['state'], 1) + self.assertTrue(session._dirty) + + def test_reissue_invalid(self): + request = testing.DummyRequest() + self.assertRaises(ValueError, self._makeOne, request, reissue_time='invalid value') + + def test_cookie_max_age_invalid(self): + request = testing.DummyRequest() + self.assertRaises(ValueError, self._makeOne, request, max_age='invalid value') + + def test_custom_salt(self): + import time + request = testing.DummyRequest() + cookieval = self._serialize((time.time(), 0, {'state': 1}), salt=b'f.') + request.cookies['session'] = cookieval + session = self._makeOne(request, salt=b'f.') + self.assertEqual(session['state'], 1) + + def test_salt_mismatch(self): + import time + request = testing.DummyRequest() + cookieval = self._serialize((time.time(), 0, {'state': 1}), salt=b'f.') + request.cookies['session'] = cookieval + session = self._makeOne(request, salt=b'g.') + self.assertEqual(session, {}) + + def test_custom_hashalg(self): + import time + request = testing.DummyRequest() + cookieval = self._serialize((time.time(), 0, {'state': 1}), + hashalg='sha1') + request.cookies['session'] = cookieval + session = self._makeOne(request, hashalg='sha1') + self.assertEqual(session['state'], 1) + + def test_hashalg_mismatch(self): + import time + request = testing.DummyRequest() + cookieval = self._serialize((time.time(), 0, {'state': 1}), + hashalg='sha1') + request.cookies['session'] = cookieval + session = self._makeOne(request, hashalg='sha256') + self.assertEqual(session, {}) + + def test_secret_mismatch(self): + import time + request = testing.DummyRequest() + cookieval = self._serialize((time.time(), 0, {'state': 1})) + request.cookies['session'] = cookieval + session = self._makeOne(request, secret='evilsecret') + self.assertEqual(session, {}) + + def test_custom_serializer(self): + import base64 + from hashlib import sha512 + import hmac + import time + request = testing.DummyRequest() + serializer = DummySerializer() + cstruct = serializer.dumps((time.time(), 0, {'state': 1})) + sig = hmac.new(b'pyramid.session.secret', cstruct, sha512).digest() + cookieval = base64.urlsafe_b64encode(sig + cstruct).rstrip(b'=') + request.cookies['session'] = cookieval + session = self._makeOne(request, serializer=serializer) + self.assertEqual(session['state'], 1) + + def test_invalid_data_size(self): + from hashlib import sha512 + import base64 + request = testing.DummyRequest() + num_bytes = sha512().digest_size - 1 + cookieval = base64.b64encode(b' ' * num_bytes) + request.cookies['session'] = cookieval + session = self._makeOne(request) + self.assertEqual(session, {}) + + def test_very_long_key(self): + verylongkey = b'a' * 1024 + import webob + request = testing.DummyRequest() + session = self._makeOne(request, secret=verylongkey) + session['a'] = 1 + callbacks = request.response_callbacks + self.assertEqual(len(callbacks), 1) + response = webob.Response() + + try: + result = callbacks[0](request, response) + except TypeError: # pragma: no cover + self.fail('HMAC failed to initialize due to key length.') + + self.assertEqual(result, None) + self.assertTrue('Set-Cookie' in dict(response.headerlist)) + + def test_bad_pickle(self): + import base64 + import hashlib + import hmac + + digestmod = lambda: hashlib.new('sha512') + # generated from dumping an object that cannot be found anymore, eg: + # class Foo: pass + # print(pickle.dumps(Foo())) + cstruct = b'(i__main__\nFoo\np0\n(dp1\nb.' + sig = hmac.new(b'pyramid.session.secret', cstruct, digestmod).digest() + cookieval = base64.urlsafe_b64encode(sig + cstruct).rstrip(b'=') + + request = testing.DummyRequest() + request.cookies['session'] = cookieval + session = self._makeOne(request, secret='secret') + self.assertEqual(session, {}) + +class TestUnencryptedCookieSession(SharedCookieSessionTests, unittest.TestCase): + def setUp(self): + super(TestUnencryptedCookieSession, self).setUp() + from zope.deprecation import __show__ + __show__.off() + + def tearDown(self): + super(TestUnencryptedCookieSession, self).tearDown() + from zope.deprecation import __show__ + __show__.on() + + def _makeOne(self, request, **kw): + from pyramid.session import UnencryptedCookieSessionFactoryConfig + self._rename_cookie_var(kw, 'path', 'cookie_path') + self._rename_cookie_var(kw, 'domain', 'cookie_domain') + self._rename_cookie_var(kw, 'secure', 'cookie_secure') + self._rename_cookie_var(kw, 'httponly', 'cookie_httponly') + self._rename_cookie_var(kw, 'set_on_exception', 'cookie_on_exception') + return UnencryptedCookieSessionFactoryConfig('secret', **kw)(request) + + def _rename_cookie_var(self, kw, src, dest): + if src in kw: + kw.setdefault(dest, kw.pop(src)) + + def _serialize(self, value): + from pyramid.compat import bytes_ + from pyramid.session import signed_serialize + return bytes_(signed_serialize(value, 'secret')) + + def test_serialize_option(self): + from pyramid.response import Response + secret = 'secret' + request = testing.DummyRequest() + session = self._makeOne(request, + signed_serialize=dummy_signed_serialize) + session['key'] = 'value' + response = Response() + self.assertEqual(session._set_cookie(response), True) + cookie = response.headerlist[-1][1] + expected_cookieval = dummy_signed_serialize( + (session.accessed, session.created, {'key': 'value'}), secret) + response = Response() + response.set_cookie('session', expected_cookieval, samesite='Lax') + expected_cookie = response.headerlist[-1][1] + self.assertEqual(cookie, expected_cookie) + + def test_deserialize_option(self): + import time + secret = 'secret' + request = testing.DummyRequest() + accessed = time.time() + state = {'key': 'value'} + cookieval = dummy_signed_serialize((accessed, accessed, state), secret) + request.cookies['session'] = cookieval + session = self._makeOne(request, + signed_deserialize=dummy_signed_deserialize) + self.assertEqual(dict(session), state) + +def dummy_signed_serialize(data, secret): + import base64 + from pyramid.compat import pickle, bytes_ + pickled = pickle.dumps(data) + return base64.b64encode(bytes_(secret)) + base64.b64encode(pickled) + +def dummy_signed_deserialize(serialized, secret): + import base64 + from pyramid.compat import pickle, bytes_ + serialized_data = base64.b64decode( + serialized[len(base64.b64encode(bytes_(secret))):]) + return pickle.loads(serialized_data) + +class Test_manage_accessed(unittest.TestCase): + def _makeOne(self, wrapped): + from pyramid.session import manage_accessed + return manage_accessed(wrapped) + + def test_accessed_set(self): + request = testing.DummyRequest() + session = DummySessionFactory(request) + session.renewed = 0 + wrapper = self._makeOne(session.__class__.get) + wrapper(session, 'a') + self.assertNotEqual(session.accessed, None) + self.assertTrue(session._dirty) + + def test_accessed_without_renew(self): + import time + request = testing.DummyRequest() + session = DummySessionFactory(request) + session._reissue_time = 5 + session.renewed = time.time() + wrapper = self._makeOne(session.__class__.get) + wrapper(session, 'a') + self.assertNotEqual(session.accessed, None) + self.assertFalse(session._dirty) + + def test_already_dirty(self): + request = testing.DummyRequest() + session = DummySessionFactory(request) + session.renewed = 0 + session._dirty = True + session['a'] = 1 + wrapper = self._makeOne(session.__class__.get) + self.assertEqual(wrapper.__doc__, session.get.__doc__) + result = wrapper(session, 'a') + self.assertEqual(result, 1) + callbacks = request.response_callbacks + if callbacks is not None: self.assertEqual(len(callbacks), 0) + +class Test_manage_changed(unittest.TestCase): + def _makeOne(self, wrapped): + from pyramid.session import manage_changed + return manage_changed(wrapped) + + def test_it(self): + request = testing.DummyRequest() + session = DummySessionFactory(request) + wrapper = self._makeOne(session.__class__.__setitem__) + wrapper(session, 'a', 1) + self.assertNotEqual(session.accessed, None) + self.assertTrue(session._dirty) + +def serialize(data, secret): + import hmac + import base64 + from hashlib import sha1 + from pyramid.compat import bytes_ + from pyramid.compat import native_ + from pyramid.compat import pickle + pickled = pickle.dumps(data, pickle.HIGHEST_PROTOCOL) + sig = hmac.new(bytes_(secret, 'utf-8'), pickled, sha1).hexdigest() + return sig + native_(base64.b64encode(pickled)) + +class Test_signed_serialize(unittest.TestCase): + def _callFUT(self, data, secret): + from pyramid.session import signed_serialize + return signed_serialize(data, secret) + + def test_it(self): + expected = serialize('123', 'secret') + result = self._callFUT('123', 'secret') + self.assertEqual(result, expected) + + def test_it_with_highorder_secret(self): + secret = b'\xce\xb1\xce\xb2\xce\xb3\xce\xb4'.decode('utf-8') + expected = serialize('123', secret) + result = self._callFUT('123', secret) + self.assertEqual(result, expected) + + def test_it_with_latin1_secret(self): + secret = b'La Pe\xc3\xb1a' + expected = serialize('123', secret) + result = self._callFUT('123', secret.decode('latin-1')) + self.assertEqual(result, expected) + +class Test_signed_deserialize(unittest.TestCase): + def _callFUT(self, serialized, secret, hmac=None): + if hmac is None: + import hmac + from pyramid.session import signed_deserialize + return signed_deserialize(serialized, secret, hmac=hmac) + + def test_it(self): + serialized = serialize('123', 'secret') + result = self._callFUT(serialized, 'secret') + self.assertEqual(result, '123') + + def test_invalid_bits(self): + serialized = serialize('123', 'secret') + self.assertRaises(ValueError, self._callFUT, serialized, 'seekrit') + + def test_invalid_len(self): + class hmac(object): + def new(self, *arg): + return self + def hexdigest(self): + return '1234' + serialized = serialize('123', 'secret123') + self.assertRaises(ValueError, self._callFUT, serialized, 'secret', + hmac=hmac()) + + def test_it_bad_encoding(self): + serialized = 'bad' + serialize('123', 'secret') + self.assertRaises(ValueError, self._callFUT, serialized, 'secret') + + def test_it_with_highorder_secret(self): + secret = b'\xce\xb1\xce\xb2\xce\xb3\xce\xb4'.decode('utf-8') + serialized = serialize('123', secret) + result = self._callFUT(serialized, secret) + self.assertEqual(result, '123') + + # bwcompat with pyramid <= 1.5b1 where latin1 is the default + def test_it_with_latin1_secret(self): + secret = b'La Pe\xc3\xb1a' + serialized = serialize('123', secret) + result = self._callFUT(serialized, secret.decode('latin-1')) + self.assertEqual(result, '123') + + +class TestPickleSerializer(unittest.TestCase): + def _makeOne(self): + from pyramid.session import PickleSerializer + return PickleSerializer() + + def test_loads(self): + # generated from dumping Dummy() using protocol=2 + cstruct = b'\x80\x02cpyramid.tests.test_session\nDummy\nq\x00)\x81q\x01.' + serializer = self._makeOne() + result = serializer.loads(cstruct) + self.assertIsInstance(result, Dummy) + + def test_loads_raises_ValueError_on_invalid_data(self): + cstruct = b'not pickled' + serializer = self._makeOne() + self.assertRaises(ValueError, serializer.loads, cstruct) + + def test_loads_raises_ValueError_on_bad_import(self): + # generated from dumping an object that cannot be found anymore, eg: + # class Foo: pass + # print(pickle.dumps(Foo())) + cstruct = b'(i__main__\nFoo\np0\n(dp1\nb.' + serializer = self._makeOne() + self.assertRaises(ValueError, serializer.loads, cstruct) + + def test_dumps(self): + obj = Dummy() + serializer = self._makeOne() + result = serializer.dumps(obj) + expected_result = pickle.dumps(obj, protocol=pickle.HIGHEST_PROTOCOL) + self.assertEqual(result, expected_result) + self.assertIsInstance(result, bytes) + + +class Dummy(object): + pass + + +class DummySerializer(object): + def dumps(self, value): + return base64.b64encode(json.dumps(value).encode('utf-8')) + + def loads(self, value): + try: + return json.loads(base64.b64decode(value).decode('utf-8')) + + # base64.b64decode raises a TypeError on py2 instead of a ValueError + # and a ValueError is required for the session to handle it properly + except TypeError: + raise ValueError + +class DummySessionFactory(dict): + _dirty = False + _cookie_name = 'session' + _cookie_max_age = None + _cookie_path = '/' + _cookie_domain = None + _cookie_secure = False + _cookie_httponly = False + _timeout = 1200 + _reissue_time = 0 + + def __init__(self, request): + self.request = request + dict.__init__(self, {}) + + def changed(self): + self._dirty = True + +class DummyResponse(object): + def __init__(self): + self.headerlist = [] diff --git a/src/pyramid/tests/test_settings.py b/src/pyramid/tests/test_settings.py new file mode 100644 index 000000000..a586cb6fd --- /dev/null +++ b/src/pyramid/tests/test_settings.py @@ -0,0 +1,80 @@ +import unittest + +class Test_asbool(unittest.TestCase): + def _callFUT(self, s): + from pyramid.settings import asbool + return asbool(s) + + def test_s_is_None(self): + result = self._callFUT(None) + self.assertEqual(result, False) + + def test_s_is_True(self): + result = self._callFUT(True) + self.assertEqual(result, True) + + def test_s_is_False(self): + result = self._callFUT(False) + self.assertEqual(result, False) + + def test_s_is_true(self): + result = self._callFUT('True') + self.assertEqual(result, True) + + def test_s_is_false(self): + result = self._callFUT('False') + self.assertEqual(result, False) + + def test_s_is_yes(self): + result = self._callFUT('yes') + self.assertEqual(result, True) + + def test_s_is_on(self): + result = self._callFUT('on') + self.assertEqual(result, True) + + def test_s_is_1(self): + result = self._callFUT(1) + self.assertEqual(result, True) + +class Test_aslist_cronly(unittest.TestCase): + def _callFUT(self, val): + from pyramid.settings import aslist_cronly + return aslist_cronly(val) + + def test_with_list(self): + result = self._callFUT(['abc', 'def']) + self.assertEqual(result, ['abc', 'def']) + + def test_with_string(self): + result = self._callFUT('abc def') + self.assertEqual(result, ['abc def']) + + def test_with_string_crsep(self): + result = self._callFUT(' abc\n def') + self.assertEqual(result, ['abc', 'def']) + +class Test_aslist(unittest.TestCase): + def _callFUT(self, val, **kw): + from pyramid.settings import aslist + return aslist(val, **kw) + + def test_with_list(self): + result = self._callFUT(['abc', 'def']) + self.assertEqual(list(result), ['abc', 'def']) + + def test_with_string(self): + result = self._callFUT('abc def') + self.assertEqual(result, ['abc', 'def']) + + def test_with_string_crsep(self): + result = self._callFUT(' abc\n def') + self.assertEqual(result, ['abc', 'def']) + + def test_with_string_crsep_spacesep(self): + result = self._callFUT(' abc\n def ghi') + self.assertEqual(result, ['abc', 'def', 'ghi']) + + def test_with_string_crsep_spacesep_no_flatten(self): + result = self._callFUT(' abc\n def ghi ', flatten=False) + self.assertEqual(result, ['abc', 'def ghi']) diff --git a/src/pyramid/tests/test_static.py b/src/pyramid/tests/test_static.py new file mode 100644 index 000000000..f76cc5067 --- /dev/null +++ b/src/pyramid/tests/test_static.py @@ -0,0 +1,477 @@ +import datetime +import os.path +import unittest + +here = os.path.dirname(__file__) + +# 5 years from now (more or less) +fiveyrsfuture = datetime.datetime.utcnow() + datetime.timedelta(5*365) + +class Test_static_view_use_subpath_False(unittest.TestCase): + def _getTargetClass(self): + from pyramid.static import static_view + return static_view + + def _makeOne(self, *arg, **kw): + return self._getTargetClass()(*arg, **kw) + + def _makeRequest(self, kw=None): + from pyramid.request import Request + environ = { + 'wsgi.url_scheme':'http', + 'wsgi.version':(1,0), + 'SERVER_NAME':'example.com', + 'SERVER_PORT':'6543', + 'PATH_INFO':'/', + 'SCRIPT_NAME':'', + 'REQUEST_METHOD':'GET', + } + if kw is not None: + environ.update(kw) + return Request(environ=environ) + + def test_ctor_defaultargs(self): + inst = self._makeOne('package:resource_name') + self.assertEqual(inst.package_name, 'package') + self.assertEqual(inst.docroot, 'resource_name') + self.assertEqual(inst.cache_max_age, 3600) + self.assertEqual(inst.index, 'index.html') + + def test_call_adds_slash_path_info_empty(self): + inst = self._makeOne('pyramid.tests:fixtures/static') + request = self._makeRequest({'PATH_INFO':''}) + context = DummyContext() + from pyramid.httpexceptions import HTTPMovedPermanently + self.assertRaises(HTTPMovedPermanently, inst, context, request) + + def test_path_info_slash_means_index_html(self): + inst = self._makeOne('pyramid.tests:fixtures/static') + request = self._makeRequest() + context = DummyContext() + response = inst(context, request) + self.assertTrue(b'static' in response.body) + + def test_oob_singledot(self): + inst = self._makeOne('pyramid.tests:fixtures/static') + request = self._makeRequest({'PATH_INFO':'/./index.html'}) + context = DummyContext() + response = inst(context, request) + self.assertEqual(response.status, '200 OK') + self.assertTrue(b'static' in response.body) + + def test_oob_emptyelement(self): + inst = self._makeOne('pyramid.tests:fixtures/static') + request = self._makeRequest({'PATH_INFO':'//index.html'}) + context = DummyContext() + response = inst(context, request) + self.assertEqual(response.status, '200 OK') + self.assertTrue(b'static' in response.body) + + def test_oob_dotdotslash(self): + inst = self._makeOne('pyramid.tests:fixtures/static') + request = self._makeRequest({'PATH_INFO':'/subdir/../../minimal.pt'}) + context = DummyContext() + from pyramid.httpexceptions import HTTPNotFound + self.assertRaises(HTTPNotFound, inst, context, request) + + def test_oob_dotdotslash_encoded(self): + inst = self._makeOne('pyramid.tests:fixtures/static') + request = self._makeRequest( + {'PATH_INFO':'/subdir/%2E%2E%2F%2E%2E/minimal.pt'}) + context = DummyContext() + from pyramid.httpexceptions import HTTPNotFound + self.assertRaises(HTTPNotFound, inst, context, request) + + def test_oob_os_sep(self): + import os + inst = self._makeOne('pyramid.tests:fixtures/static') + dds = '..' + os.sep + request = self._makeRequest({'PATH_INFO':'/subdir/%s%sminimal.pt' % + (dds, dds)}) + context = DummyContext() + from pyramid.httpexceptions import HTTPNotFound + self.assertRaises(HTTPNotFound, inst, context, request) + + def test_resource_doesnt_exist(self): + inst = self._makeOne('pyramid.tests:fixtures/static') + request = self._makeRequest({'PATH_INFO':'/notthere'}) + context = DummyContext() + from pyramid.httpexceptions import HTTPNotFound + self.assertRaises(HTTPNotFound, inst, context, request) + + def test_resource_isdir(self): + inst = self._makeOne('pyramid.tests:fixtures/static') + request = self._makeRequest({'PATH_INFO':'/subdir/'}) + context = DummyContext() + response = inst(context, request) + self.assertTrue(b'subdir' in response.body) + + def test_resource_is_file(self): + inst = self._makeOne('pyramid.tests:fixtures/static') + request = self._makeRequest({'PATH_INFO':'/index.html'}) + context = DummyContext() + response = inst(context, request) + self.assertTrue(b'static' in response.body) + + def test_resource_is_file_with_wsgi_file_wrapper(self): + from pyramid.response import _BLOCK_SIZE + inst = self._makeOne('pyramid.tests:fixtures/static') + request = self._makeRequest({'PATH_INFO':'/index.html'}) + class _Wrapper(object): + def __init__(self, file, block_size=None): + self.file = file + self.block_size = block_size + request.environ['wsgi.file_wrapper'] = _Wrapper + context = DummyContext() + response = inst(context, request) + app_iter = response.app_iter + self.assertTrue(isinstance(app_iter, _Wrapper)) + self.assertTrue(b'static' in app_iter.file.read()) + self.assertEqual(app_iter.block_size, _BLOCK_SIZE) + app_iter.file.close() + + def test_resource_is_file_with_cache_max_age(self): + inst = self._makeOne('pyramid.tests:fixtures/static', cache_max_age=600) + request = self._makeRequest({'PATH_INFO':'/index.html'}) + context = DummyContext() + response = inst(context, request) + self.assertTrue(b'static' in response.body) + self.assertEqual(len(response.headerlist), 5) + header_names = [ x[0] for x in response.headerlist ] + header_names.sort() + self.assertEqual(header_names, + ['Cache-Control', 'Content-Length', 'Content-Type', + 'Expires', 'Last-Modified']) + + def test_resource_is_file_with_no_cache_max_age(self): + inst = self._makeOne('pyramid.tests:fixtures/static', + cache_max_age=None) + request = self._makeRequest({'PATH_INFO':'/index.html'}) + context = DummyContext() + response = inst(context, request) + self.assertTrue(b'static' in response.body) + self.assertEqual(len(response.headerlist), 3) + header_names = [ x[0] for x in response.headerlist ] + header_names.sort() + self.assertEqual( + header_names, + ['Content-Length', 'Content-Type', 'Last-Modified']) + + def test_resource_notmodified(self): + inst = self._makeOne('pyramid.tests:fixtures/static') + request = self._makeRequest({'PATH_INFO':'/index.html'}) + request.if_modified_since = fiveyrsfuture + context = DummyContext() + response = inst(context, request) + start_response = DummyStartResponse() + app_iter = response(request.environ, start_response) + try: + self.assertEqual(start_response.status, '304 Not Modified') + self.assertEqual(list(app_iter), []) + finally: + app_iter.close() + + def test_not_found(self): + inst = self._makeOne('pyramid.tests:fixtures/static') + request = self._makeRequest({'PATH_INFO':'/notthere.html'}) + context = DummyContext() + from pyramid.httpexceptions import HTTPNotFound + self.assertRaises(HTTPNotFound, inst, context, request) + + def test_gz_resource_no_content_encoding(self): + inst = self._makeOne('pyramid.tests:fixtures/static') + request = self._makeRequest({'PATH_INFO':'/arcs.svg.tgz'}) + context = DummyContext() + response = inst(context, request) + self.assertEqual(response.status, '200 OK') + self.assertEqual(response.content_type, 'application/x-tar') + self.assertEqual(response.content_encoding, None) + response.app_iter.close() + + def test_resource_no_content_encoding(self): + inst = self._makeOne('pyramid.tests:fixtures/static') + request = self._makeRequest({'PATH_INFO':'/index.html'}) + context = DummyContext() + response = inst(context, request) + self.assertEqual(response.status, '200 OK') + self.assertEqual(response.content_type, 'text/html') + self.assertEqual(response.content_encoding, None) + response.app_iter.close() + +class Test_static_view_use_subpath_True(unittest.TestCase): + def _getTargetClass(self): + from pyramid.static import static_view + return static_view + + def _makeOne(self, *arg, **kw): + kw['use_subpath'] = True + return self._getTargetClass()(*arg, **kw) + + def _makeRequest(self, kw=None): + from pyramid.request import Request + environ = { + 'wsgi.url_scheme':'http', + 'wsgi.version':(1,0), + 'SERVER_NAME':'example.com', + 'SERVER_PORT':'6543', + 'PATH_INFO':'/', + 'SCRIPT_NAME':'', + 'REQUEST_METHOD':'GET', + } + if kw is not None: + environ.update(kw) + return Request(environ=environ) + + def test_ctor_defaultargs(self): + inst = self._makeOne('package:resource_name') + self.assertEqual(inst.package_name, 'package') + self.assertEqual(inst.docroot, 'resource_name') + self.assertEqual(inst.cache_max_age, 3600) + self.assertEqual(inst.index, 'index.html') + + def test_call_adds_slash_path_info_empty(self): + inst = self._makeOne('pyramid.tests:fixtures/static') + request = self._makeRequest({'PATH_INFO':''}) + request.subpath = () + context = DummyContext() + from pyramid.httpexceptions import HTTPMovedPermanently + self.assertRaises(HTTPMovedPermanently, inst, context, request) + + def test_path_info_slash_means_index_html(self): + inst = self._makeOne('pyramid.tests:fixtures/static') + request = self._makeRequest() + request.subpath = () + context = DummyContext() + response = inst(context, request) + self.assertTrue(b'static' in response.body) + + def test_oob_singledot(self): + inst = self._makeOne('pyramid.tests:fixtures/static') + request = self._makeRequest() + request.subpath = ('.', 'index.html') + context = DummyContext() + from pyramid.httpexceptions import HTTPNotFound + self.assertRaises(HTTPNotFound, inst, context, request) + + def test_oob_emptyelement(self): + inst = self._makeOne('pyramid.tests:fixtures/static') + request = self._makeRequest() + request.subpath = ('', 'index.html') + context = DummyContext() + from pyramid.httpexceptions import HTTPNotFound + self.assertRaises(HTTPNotFound, inst, context, request) + + def test_oob_dotdotslash(self): + inst = self._makeOne('pyramid.tests:fixtures/static') + request = self._makeRequest() + request.subpath = ('subdir', '..', '..', 'minimal.pt') + context = DummyContext() + from pyramid.httpexceptions import HTTPNotFound + self.assertRaises(HTTPNotFound, inst, context, request) + + def test_oob_dotdotslash_encoded(self): + inst = self._makeOne('pyramid.tests:fixtures/static') + request = self._makeRequest() + request.subpath = ('subdir', '%2E%2E', '%2E%2E', 'minimal.pt') + context = DummyContext() + from pyramid.httpexceptions import HTTPNotFound + self.assertRaises(HTTPNotFound, inst, context, request) + + def test_oob_os_sep(self): + import os + inst = self._makeOne('pyramid.tests:fixtures/static') + dds = '..' + os.sep + request = self._makeRequest() + request.subpath = ('subdir', dds, dds, 'minimal.pt') + context = DummyContext() + from pyramid.httpexceptions import HTTPNotFound + self.assertRaises(HTTPNotFound, inst, context, request) + + def test_resource_doesnt_exist(self): + inst = self._makeOne('pyramid.tests:fixtures/static') + request = self._makeRequest() + request.subpath = ('notthere,') + context = DummyContext() + from pyramid.httpexceptions import HTTPNotFound + self.assertRaises(HTTPNotFound, inst, context, request) + + def test_resource_isdir(self): + inst = self._makeOne('pyramid.tests:fixtures/static') + request = self._makeRequest() + request.subpath = ('subdir',) + context = DummyContext() + response = inst(context, request) + self.assertTrue(b'subdir' in response.body) + + def test_resource_is_file(self): + inst = self._makeOne('pyramid.tests:fixtures/static') + request = self._makeRequest() + request.subpath = ('index.html',) + context = DummyContext() + response = inst(context, request) + self.assertTrue(b'static' in response.body) + + def test_resource_is_file_with_cache_max_age(self): + inst = self._makeOne('pyramid.tests:fixtures/static', cache_max_age=600) + request = self._makeRequest() + request.subpath = ('index.html',) + context = DummyContext() + response = inst(context, request) + self.assertTrue(b'static' in response.body) + self.assertEqual(len(response.headerlist), 5) + header_names = [ x[0] for x in response.headerlist ] + header_names.sort() + self.assertEqual(header_names, + ['Cache-Control', 'Content-Length', 'Content-Type', + 'Expires', 'Last-Modified']) + + def test_resource_is_file_with_no_cache_max_age(self): + inst = self._makeOne('pyramid.tests:fixtures/static', + cache_max_age=None) + request = self._makeRequest() + request.subpath = ('index.html',) + context = DummyContext() + response = inst(context, request) + self.assertTrue(b'static' in response.body) + self.assertEqual(len(response.headerlist), 3) + header_names = [ x[0] for x in response.headerlist ] + header_names.sort() + self.assertEqual( + header_names, + ['Content-Length', 'Content-Type', 'Last-Modified']) + + def test_resource_notmodified(self): + inst = self._makeOne('pyramid.tests:fixtures/static') + request = self._makeRequest() + request.if_modified_since = fiveyrsfuture + request.subpath = ('index.html',) + context = DummyContext() + response = inst(context, request) + start_response = DummyStartResponse() + app_iter = response(request.environ, start_response) + try: + self.assertEqual(start_response.status, '304 Not Modified') + self.assertEqual(list(app_iter), []) + finally: + app_iter.close() + + def test_not_found(self): + inst = self._makeOne('pyramid.tests:fixtures/static') + request = self._makeRequest() + request.subpath = ('notthere.html',) + context = DummyContext() + from pyramid.httpexceptions import HTTPNotFound + self.assertRaises(HTTPNotFound, inst, context, request) + +class TestQueryStringConstantCacheBuster(unittest.TestCase): + + def _makeOne(self, param=None): + from pyramid.static import QueryStringConstantCacheBuster as cls + if param: + inst = cls('foo', param) + else: + inst = cls('foo') + return inst + + def test_token(self): + fut = self._makeOne().tokenize + self.assertEqual(fut(None, 'whatever', None), 'foo') + + def test_it(self): + fut = self._makeOne() + self.assertEqual( + fut('foo', 'bar', {}), + ('bar', {'_query': {'x': 'foo'}})) + + def test_change_param(self): + fut = self._makeOne('y') + self.assertEqual( + fut('foo', 'bar', {}), + ('bar', {'_query': {'y': 'foo'}})) + + def test_query_is_already_tuples(self): + fut = self._makeOne() + self.assertEqual( + fut('foo', 'bar', {'_query': [('a', 'b')]}), + ('bar', {'_query': (('a', 'b'), ('x', 'foo'))})) + + def test_query_is_tuple_of_tuples(self): + fut = self._makeOne() + self.assertEqual( + fut('foo', 'bar', {'_query': (('a', 'b'),)}), + ('bar', {'_query': (('a', 'b'), ('x', 'foo'))})) + +class TestManifestCacheBuster(unittest.TestCase): + + def _makeOne(self, path, **kw): + from pyramid.static import ManifestCacheBuster as cls + return cls(path, **kw) + + def test_it(self): + manifest_path = os.path.join(here, 'fixtures', 'manifest.json') + fut = self._makeOne(manifest_path) + self.assertEqual(fut('foo', 'bar', {}), ('bar', {})) + self.assertEqual( + fut('foo', 'css/main.css', {}), + ('css/main-test.css', {})) + + def test_it_with_relspec(self): + fut = self._makeOne('fixtures/manifest.json') + self.assertEqual(fut('foo', 'bar', {}), ('bar', {})) + self.assertEqual( + fut('foo', 'css/main.css', {}), + ('css/main-test.css', {})) + + def test_it_with_absspec(self): + fut = self._makeOne('pyramid.tests:fixtures/manifest.json') + self.assertEqual(fut('foo', 'bar', {}), ('bar', {})) + self.assertEqual( + fut('foo', 'css/main.css', {}), + ('css/main-test.css', {})) + + def test_reload(self): + manifest_path = os.path.join(here, 'fixtures', 'manifest.json') + new_manifest_path = os.path.join(here, 'fixtures', 'manifest2.json') + inst = self._makeOne('foo', reload=True) + inst.getmtime = lambda *args, **kwargs: 0 + fut = inst + + # test without a valid manifest + self.assertEqual( + fut('foo', 'css/main.css', {}), + ('css/main.css', {})) + + # swap to a real manifest, setting mtime to 0 + inst.manifest_path = manifest_path + self.assertEqual( + fut('foo', 'css/main.css', {}), + ('css/main-test.css', {})) + + # ensure switching the path doesn't change the result + inst.manifest_path = new_manifest_path + self.assertEqual( + fut('foo', 'css/main.css', {}), + ('css/main-test.css', {})) + + # update mtime, should cause a reload + inst.getmtime = lambda *args, **kwargs: 1 + self.assertEqual( + fut('foo', 'css/main.css', {}), + ('css/main-678b7c80.css', {})) + + def test_invalid_manifest(self): + self.assertRaises(IOError, lambda: self._makeOne('foo')) + + def test_invalid_manifest_with_reload(self): + inst = self._makeOne('foo', reload=True) + self.assertEqual(inst.manifest, {}) + +class DummyContext: + pass + +class DummyStartResponse: + status = () + headers = () + def __call__(self, status, headers): + self.status = status + self.headers = headers diff --git a/src/pyramid/tests/test_testing.py b/src/pyramid/tests/test_testing.py new file mode 100644 index 000000000..86c219988 --- /dev/null +++ b/src/pyramid/tests/test_testing.py @@ -0,0 +1,689 @@ +import unittest +from zope.component import getSiteManager +from pyramid import testing + +class TestDummyRootFactory(unittest.TestCase): + def _makeOne(self, environ): + from pyramid.testing import DummyRootFactory + return DummyRootFactory(environ) + + def test_it(self): + environ = {'bfg.routes.matchdict':{'a':1}} + factory = self._makeOne(environ) + self.assertEqual(factory.a, 1) + +class TestDummySecurityPolicy(unittest.TestCase): + def _getTargetClass(self): + from pyramid.testing import DummySecurityPolicy + return DummySecurityPolicy + + def _makeOne(self, userid=None, groupids=(), permissive=True): + klass = self._getTargetClass() + return klass(userid, groupids, permissive) + + def test_authenticated_userid(self): + policy = self._makeOne('user') + self.assertEqual(policy.authenticated_userid(None), 'user') + + def test_unauthenticated_userid(self): + policy = self._makeOne('user') + self.assertEqual(policy.unauthenticated_userid(None), 'user') + + def test_effective_principals_userid(self): + policy = self._makeOne('user', ('group1',)) + from pyramid.security import Everyone + from pyramid.security import Authenticated + self.assertEqual(policy.effective_principals(None), + [Everyone, Authenticated, 'user', 'group1']) + + def test_effective_principals_nouserid(self): + policy = self._makeOne() + from pyramid.security import Everyone + self.assertEqual(policy.effective_principals(None), [Everyone]) + + def test_permits(self): + policy = self._makeOne() + self.assertEqual(policy.permits(None, None, None), True) + + def test_principals_allowed_by_permission(self): + policy = self._makeOne('user', ('group1',)) + from pyramid.security import Everyone + from pyramid.security import Authenticated + result = policy.principals_allowed_by_permission(None, None) + self.assertEqual(result, [Everyone, Authenticated, 'user', 'group1']) + + def test_forget(self): + policy = self._makeOne() + self.assertEqual(policy.forget(None), []) + + def test_remember(self): + policy = self._makeOne() + self.assertEqual(policy.remember(None, None), []) + + + +class TestDummyResource(unittest.TestCase): + def _getTargetClass(self): + from pyramid.testing import DummyResource + return DummyResource + + def _makeOne(self, name=None, parent=None, **kw): + klass = self._getTargetClass() + return klass(name, parent, **kw) + + def test__setitem__and__getitem__and__delitem__and__contains__and_get(self): + class Dummy: + pass + dummy = Dummy() + resource = self._makeOne() + resource['abc'] = dummy + self.assertEqual(dummy.__name__, 'abc') + self.assertEqual(dummy.__parent__, resource) + self.assertEqual(resource['abc'], dummy) + self.assertEqual(resource.get('abc'), dummy) + self.assertRaises(KeyError, resource.__getitem__, 'none') + self.assertTrue('abc' in resource) + del resource['abc'] + self.assertFalse('abc' in resource) + self.assertEqual(resource.get('abc', 'foo'), 'foo') + self.assertEqual(resource.get('abc'), None) + + def test_extra_params(self): + resource = self._makeOne(foo=1) + self.assertEqual(resource.foo, 1) + + def test_clone(self): + resource = self._makeOne('name', 'parent', foo=1, bar=2) + clone = resource.clone('name2', 'parent2', bar=1) + self.assertEqual(clone.bar, 1) + self.assertEqual(clone.__name__, 'name2') + self.assertEqual(clone.__parent__, 'parent2') + self.assertEqual(clone.foo, 1) + + def test_keys_items_values_len(self): + class Dummy: + pass + resource = self._makeOne() + resource['abc'] = Dummy() + resource['def'] = Dummy() + L = list + self.assertEqual(L(resource.values()), L(resource.subs.values())) + self.assertEqual(L(resource.items()), L(resource.subs.items())) + self.assertEqual(L(resource.keys()), L(resource.subs.keys())) + self.assertEqual(len(resource), 2) + + def test_nonzero(self): + resource = self._makeOne() + self.assertEqual(resource.__nonzero__(), True) + + def test_bool(self): + resource = self._makeOne() + self.assertEqual(resource.__bool__(), True) + + def test_ctor_with__provides__(self): + resource = self._makeOne(__provides__=IDummy) + self.assertTrue(IDummy.providedBy(resource)) + +class TestDummyRequest(unittest.TestCase): + def _getTargetClass(self): + from pyramid.testing import DummyRequest + return DummyRequest + + def _makeOne(self, *arg, **kw): + return self._getTargetClass()(*arg, **kw) + + def test_params(self): + request = self._makeOne(params = {'say':'Hello'}, + environ = {'PATH_INFO':'/foo'}, + headers = {'X-Foo':'YUP'}, + ) + self.assertEqual(request.params['say'], 'Hello') + self.assertEqual(request.GET['say'], 'Hello') + self.assertEqual(request.POST['say'], 'Hello') + self.assertEqual(request.headers['X-Foo'], 'YUP') + self.assertEqual(request.environ['PATH_INFO'], '/foo') + + def test_defaults(self): + from pyramid.threadlocal import get_current_registry + from pyramid.testing import DummySession + request = self._makeOne() + self.assertEqual(request.method, 'GET') + self.assertEqual(request.application_url, 'http://example.com') + self.assertEqual(request.host_url, 'http://example.com') + self.assertEqual(request.path_url, 'http://example.com') + self.assertEqual(request.url, 'http://example.com') + self.assertEqual(request.host, 'example.com:80') + self.assertEqual(request.content_length, 0) + self.assertEqual(request.environ.get('PATH_INFO'), None) + self.assertEqual(request.headers.get('X-Foo'), None) + self.assertEqual(request.params.get('foo'), None) + self.assertEqual(request.GET.get('foo'), None) + self.assertEqual(request.POST.get('foo'), None) + self.assertEqual(request.cookies.get('type'), None) + self.assertEqual(request.path, '/') + self.assertEqual(request.path_info, '/') + self.assertEqual(request.script_name, '') + self.assertEqual(request.path_qs, '') + self.assertEqual(request.view_name, '') + self.assertEqual(request.subpath, ()) + self.assertEqual(request.context, None) + self.assertEqual(request.root, None) + self.assertEqual(request.virtual_root, None) + self.assertEqual(request.virtual_root_path, ()) + self.assertEqual(request.registry, get_current_registry()) + self.assertEqual(request.session.__class__, DummySession) + + def test_params_explicit(self): + request = self._makeOne(params = {'foo':'bar'}) + self.assertEqual(request.params['foo'], 'bar') + self.assertEqual(request.GET['foo'], 'bar') + self.assertEqual(request.POST['foo'], 'bar') + + def test_environ_explicit(self): + request = self._makeOne(environ = {'PATH_INFO':'/foo'}) + self.assertEqual(request.environ['PATH_INFO'], '/foo') + + def test_headers_explicit(self): + request = self._makeOne(headers = {'X-Foo':'YUP'}) + self.assertEqual(request.headers['X-Foo'], 'YUP') + + def test_path_explicit(self): + request = self._makeOne(path = '/abc') + self.assertEqual(request.path, '/abc') + + def test_cookies_explicit(self): + request = self._makeOne(cookies = {'type': 'gingersnap'}) + self.assertEqual(request.cookies['type'], 'gingersnap') + + def test_post_explicit(self): + POST = {'foo': 'bar', 'baz': 'qux'} + request = self._makeOne(post=POST) + self.assertEqual(request.method, 'POST') + self.assertEqual(request.POST, POST) + # N.B.: Unlike a normal request, passing 'post' should *not* put + # explict POST data into params: doing so masks a possible + # XSS bug in the app. Tests for apps which don't care about + # the distinction should just use 'params'. + self.assertEqual(request.params, {}) + + def test_post_empty_shadows_params(self): + request = self._makeOne(params={'foo': 'bar'}, post={}) + self.assertEqual(request.method, 'POST') + self.assertEqual(request.params.get('foo'), 'bar') + self.assertEqual(request.POST.get('foo'), None) + + def test_kwargs(self): + request = self._makeOne(water = 1) + self.assertEqual(request.water, 1) + + def test_add_response_callback(self): + request = self._makeOne() + request.add_response_callback(1) + self.assertEqual(list(request.response_callbacks), [1]) + + def test_registry_is_config_registry_when_setup_is_called_after_ctor(self): + # see https://github.com/Pylons/pyramid/issues/165 + from pyramid.registry import Registry + from pyramid.config import Configurator + request = self._makeOne() + try: + registry = Registry('this_test') + config = Configurator(registry=registry) + config.begin() + self.assertTrue(request.registry is registry) + finally: + config.end() + + def test_set_registry(self): + request = self._makeOne() + request.registry = 'abc' + self.assertEqual(request.registry, 'abc') + + def test_del_registry(self): + # see https://github.com/Pylons/pyramid/issues/165 + from pyramid.registry import Registry + from pyramid.config import Configurator + request = self._makeOne() + request.registry = 'abc' + self.assertEqual(request.registry, 'abc') + del request.registry + try: + registry = Registry('this_test') + config = Configurator(registry=registry) + config.begin() + self.assertTrue(request.registry is registry) + finally: + config.end() + + def test_response_with_responsefactory(self): + from pyramid.registry import Registry + from pyramid.interfaces import IResponseFactory + registry = Registry('this_test') + class ResponseFactory(object): + pass + registry.registerUtility( + lambda r: ResponseFactory(), IResponseFactory + ) + request = self._makeOne() + request.registry = registry + resp = request.response + self.assertEqual(resp.__class__, ResponseFactory) + self.assertTrue(request.response is resp) # reified + + def test_response_without_responsefactory(self): + from pyramid.registry import Registry + from pyramid.response import Response + registry = Registry('this_test') + request = self._makeOne() + request.registry = registry + resp = request.response + self.assertEqual(resp.__class__, Response) + self.assertTrue(request.response is resp) # reified + + +class TestDummyTemplateRenderer(unittest.TestCase): + def _getTargetClass(self, ): + from pyramid.testing import DummyTemplateRenderer + return DummyTemplateRenderer + + def _makeOne(self, string_response=''): + return self._getTargetClass()(string_response=string_response) + + def test_implementation(self): + renderer = self._makeOne() + impl = renderer.implementation() + impl(a=1, b=2) + self.assertEqual(renderer._implementation._received['a'], 1) + self.assertEqual(renderer._implementation._received['b'], 2) + + def test_getattr(self): + renderer = self._makeOne() + renderer({'a':1}) + self.assertEqual(renderer.a, 1) + self.assertRaises(AttributeError, renderer.__getattr__, 'b') + + def test_assert_(self): + renderer = self._makeOne() + renderer({'a':1, 'b':2}) + self.assertRaises(AssertionError, renderer.assert_, c=1) + self.assertRaises(AssertionError, renderer.assert_, b=3) + self.assertTrue(renderer.assert_(a=1, b=2)) + + def test_nondefault_string_response(self): + renderer = self._makeOne('abc') + result = renderer({'a':1, 'b':2}) + self.assertEqual(result, 'abc') + +class Test_setUp(unittest.TestCase): + def _callFUT(self, **kw): + from pyramid.testing import setUp + return setUp(**kw) + + def tearDown(self): + from pyramid.threadlocal import manager + manager.clear() + getSiteManager.reset() + + def _assertSMHook(self, hook): + result = getSiteManager.sethook(None) + self.assertEqual(result, hook) + + def test_it_defaults(self): + from pyramid.threadlocal import manager + from pyramid.threadlocal import get_current_registry + from pyramid.registry import Registry + old = True + manager.push(old) + config = self._callFUT() + current = manager.get() + self.assertFalse(current is old) + self.assertEqual(config.registry, current['registry']) + self.assertEqual(current['registry'].__class__, Registry) + self.assertEqual(current['request'], None) + self.assertEqual(config.package.__name__, 'pyramid.tests') + self._assertSMHook(get_current_registry) + + def test_it_with_registry(self): + from pyramid.registry import Registry + from pyramid.threadlocal import manager + registry = Registry() + self._callFUT(registry=registry) + current = manager.get() + self.assertEqual(current['registry'], registry) + + def test_it_with_request(self): + from pyramid.threadlocal import manager + request = object() + self._callFUT(request=request) + current = manager.get() + self.assertEqual(current['request'], request) + + def test_it_with_package(self): + config = self._callFUT(package='pyramid') + self.assertEqual(config.package.__name__, 'pyramid') + + def test_it_with_hook_zca_false(self): + from pyramid.registry import Registry + registry = Registry() + self._callFUT(registry=registry, hook_zca=False) + sm = getSiteManager() + self.assertFalse(sm is registry) + + def test_it_with_settings_passed_explicit_registry(self): + from pyramid.registry import Registry + registry = Registry() + self._callFUT(registry=registry, hook_zca=False, settings=dict(a=1)) + self.assertEqual(registry.settings['a'], 1) + + def test_it_with_settings_passed_implicit_registry(self): + config = self._callFUT(hook_zca=False, settings=dict(a=1)) + self.assertEqual(config.registry.settings['a'], 1) + +class Test_cleanUp(Test_setUp): + def _callFUT(self, *arg, **kw): + from pyramid.testing import cleanUp + return cleanUp(*arg, **kw) + +class Test_tearDown(unittest.TestCase): + def _callFUT(self, **kw): + from pyramid.testing import tearDown + return tearDown(**kw) + + def tearDown(self): + from pyramid.threadlocal import manager + manager.clear() + getSiteManager.reset() + + def _assertSMHook(self, hook): + result = getSiteManager.sethook(None) + self.assertEqual(result, hook) + + def _setSMHook(self, hook): + getSiteManager.sethook(hook) + + def test_defaults(self): + from pyramid.threadlocal import manager + registry = DummyRegistry() + old = {'registry':registry} + hook = lambda *arg: None + try: + self._setSMHook(hook) + manager.push(old) + self._callFUT() + current = manager.get() + self.assertNotEqual(current, old) + self.assertEqual(registry.inited, 2) + finally: + result = getSiteManager.sethook(None) + self.assertNotEqual(result, hook) + + def test_registry_cannot_be_inited(self): + from pyramid.threadlocal import manager + registry = DummyRegistry() + def raiseit(name): + raise TypeError + registry.__init__ = raiseit + old = {'registry':registry} + try: + manager.push(old) + self._callFUT() # doesn't blow up + current = manager.get() + self.assertNotEqual(current, old) + self.assertEqual(registry.inited, 1) + finally: + manager.clear() + + def test_unhook_zc_false(self): + hook = lambda *arg: None + try: + self._setSMHook(hook) + self._callFUT(unhook_zca=False) + finally: + self._assertSMHook(hook) + +class TestDummyRendererFactory(unittest.TestCase): + def _makeOne(self, name, factory): + from pyramid.testing import DummyRendererFactory + return DummyRendererFactory(name, factory) + + def test_add_no_colon(self): + f = self._makeOne('name', None) + f.add('spec', 'renderer') + self.assertEqual(f.renderers['spec'], 'renderer') + + def test_add_with_colon(self): + f = self._makeOne('name', None) + f.add('spec:spec2', 'renderer') + self.assertEqual(f.renderers['spec:spec2'], 'renderer') + self.assertEqual(f.renderers['spec2'], 'renderer') + + def test_call(self): + f = self._makeOne('name', None) + f.renderers['spec'] = 'renderer' + info = DummyRendererInfo({'name':'spec'}) + self.assertEqual(f(info), 'renderer') + + def test_call2(self): + f = self._makeOne('name', None) + f.renderers['spec'] = 'renderer' + info = DummyRendererInfo({'name':'spec:spec'}) + self.assertEqual(f(info), 'renderer') + + def test_call3(self): + def factory(spec): + return 'renderer' + f = self._makeOne('name', factory) + info = DummyRendererInfo({'name':'spec'}) + self.assertEqual(f(info), 'renderer') + + def test_call_miss(self): + f = self._makeOne('name', None) + info = DummyRendererInfo({'name':'spec'}) + self.assertRaises(KeyError, f, info) + +class TestMockTemplate(unittest.TestCase): + def _makeOne(self, response): + from pyramid.testing import MockTemplate + return MockTemplate(response) + + def test_getattr(self): + template = self._makeOne(None) + self.assertEqual(template.foo, template) + + def test_getitem(self): + template = self._makeOne(None) + self.assertEqual(template['foo'], template) + + def test_call(self): + template = self._makeOne('123') + self.assertEqual(template(), '123') + +class Test_skip_on(unittest.TestCase): + def setUp(self): + from pyramid.testing import skip_on + self.os_name = skip_on.os_name + skip_on.os_name = 'wrong' + + def tearDown(self): + from pyramid.testing import skip_on + skip_on.os_name = self.os_name + + def _callFUT(self, *platforms): + from pyramid.testing import skip_on + return skip_on(*platforms) + + def test_wrong_platform(self): + def foo(): return True + decorated = self._callFUT('wrong')(foo) + self.assertEqual(decorated(), None) + + def test_ok_platform(self): + def foo(): return True + decorated = self._callFUT('ok')(foo) + self.assertEqual(decorated(), True) + +class TestDummySession(unittest.TestCase): + def _makeOne(self): + from pyramid.testing import DummySession + return DummySession() + + @testing.skip_on('pypy') # see https://github.com/Pylons/pyramid/issues/3237 + def test_instance_conforms(self): + from zope.interface.verify import verifyObject + from pyramid.interfaces import ISession + session = self._makeOne() + verifyObject(ISession, session) + + def test_changed(self): + session = self._makeOne() + self.assertEqual(session.changed(), None) + + def test_invalidate(self): + session = self._makeOne() + session['a'] = 1 + self.assertEqual(session.invalidate(), None) + self.assertFalse('a' in session) + + def test_flash_default(self): + session = self._makeOne() + session.flash('msg1') + session.flash('msg2') + self.assertEqual(session['_f_'], ['msg1', 'msg2']) + + def test_flash_mixed(self): + session = self._makeOne() + session.flash('warn1', 'warn') + session.flash('warn2', 'warn') + session.flash('err1', 'error') + session.flash('err2', 'error') + self.assertEqual(session['_f_warn'], ['warn1', 'warn2']) + + def test_pop_flash_default_queue(self): + session = self._makeOne() + queue = ['one', 'two'] + session['_f_'] = queue + result = session.pop_flash() + self.assertEqual(result, queue) + self.assertEqual(session.get('_f_'), None) + + def test_pop_flash_nodefault_queue(self): + session = self._makeOne() + queue = ['one', 'two'] + session['_f_error'] = queue + result = session.pop_flash('error') + self.assertEqual(result, queue) + self.assertEqual(session.get('_f_error'), None) + + def test_peek_flash_default_queue(self): + session = self._makeOne() + queue = ['one', 'two'] + session['_f_'] = queue + result = session.peek_flash() + self.assertEqual(result, queue) + self.assertEqual(session.get('_f_'), queue) + + def test_peek_flash_nodefault_queue(self): + session = self._makeOne() + queue = ['one', 'two'] + session['_f_error'] = queue + result = session.peek_flash('error') + self.assertEqual(result, queue) + self.assertEqual(session.get('_f_error'), queue) + + def test_new_csrf_token(self): + session = self._makeOne() + token = session.new_csrf_token() + self.assertEqual(token, session['_csrft_']) + + def test_get_csrf_token(self): + session = self._makeOne() + session['_csrft_'] = 'token' + token = session.get_csrf_token() + self.assertEqual(token, 'token') + self.assertTrue('_csrft_' in session) + + def test_get_csrf_token_generates_token(self): + session = self._makeOne() + token = session.get_csrf_token() + self.assertNotEqual(token, None) + self.assertTrue(len(token) >= 1) + +from zope.interface import Interface +from zope.interface import implementer + +class IDummy(Interface): + pass + +@implementer(IDummy) +class DummyEvent: + pass + +class DummyFactory: + def __init__(self, environ): + """ """ + +class DummyRegistry(object): + inited = 0 + __name__ = 'name' + def __init__(self, name=''): + self.inited = self.inited + 1 + +class DummyRendererInfo(object): + def __init__(self, kw): + self.__dict__.update(kw) + +class Test_testConfig(unittest.TestCase): + + def _setUp(self, **kw): + self._log.append(('setUp', kw)) + return 'fake config' + + def _tearDown(self, **kw): + self._log.append(('tearDown', kw)) + + def setUp(self): + from pyramid import testing + self._log = [] + self._orig_setUp = testing.setUp + testing.setUp = self._setUp + self._orig_tearDown = testing.tearDown + testing.tearDown = self._tearDown + + def tearDown(self): + from pyramid import testing + testing.setUp = self._orig_setUp + testing.tearDown = self._orig_tearDown + + def _callFUT(self, inner, **kw): + from pyramid.testing import testConfig + with testConfig(**kw) as config: + inner(config) + + def test_ok_calls(self): + self.assertEqual(self._log, []) + def inner(config): + self.assertEqual(self._log, + [('setUp', + {'autocommit': True, + 'hook_zca': True, + 'registry': None, + 'request': None, + 'settings': None})]) + self._log.pop() + self._callFUT(inner) + self.assertEqual(self._log, + [('tearDown', {'unhook_zca': True})]) + + def test_teardown_called_on_exception(self): + class TestException(Exception): + pass + def inner(config): + self._log = [] + raise TestException('oops') + self.assertRaises(TestException, self._callFUT, inner) + self.assertEqual(self._log[0][0], 'tearDown') + + def test_ok_get_config(self): + def inner(config): + self.assertEqual(config, 'fake config') + self._callFUT(inner) diff --git a/src/pyramid/tests/test_threadlocal.py b/src/pyramid/tests/test_threadlocal.py new file mode 100644 index 000000000..088156507 --- /dev/null +++ b/src/pyramid/tests/test_threadlocal.py @@ -0,0 +1,95 @@ +from pyramid import testing +import unittest + +class TestThreadLocalManager(unittest.TestCase): + def setUp(self): + testing.setUp() + + def tearDown(self): + testing.tearDown() + + def _getTargetClass(self): + from pyramid.threadlocal import ThreadLocalManager + return ThreadLocalManager + + def _makeOne(self, default=lambda *x: 1): + return self._getTargetClass()(default) + + def test_init(self): + local = self._makeOne() + self.assertEqual(local.stack, []) + self.assertEqual(local.get(), 1) + + def test_default(self): + def thedefault(): + return '123' + local = self._makeOne(thedefault) + self.assertEqual(local.stack, []) + self.assertEqual(local.get(), '123') + + def test_push_and_pop(self): + local = self._makeOne() + local.push(True) + self.assertEqual(local.get(), True) + self.assertEqual(local.pop(), True) + self.assertEqual(local.pop(), None) + self.assertEqual(local.get(), 1) + + def test_set_get_and_clear(self): + local = self._makeOne() + local.set(None) + self.assertEqual(local.stack, [None]) + self.assertEqual(local.get(), None) + local.clear() + self.assertEqual(local.get(), 1) + local.clear() + self.assertEqual(local.get(), 1) + + +class TestGetCurrentRequest(unittest.TestCase): + def _callFUT(self): + from pyramid.threadlocal import get_current_request + return get_current_request() + + def test_it_None(self): + request = self._callFUT() + self.assertEqual(request, None) + + def test_it(self): + from pyramid.threadlocal import manager + request = object() + try: + manager.push({'request':request}) + self.assertEqual(self._callFUT(), request) + finally: + manager.pop() + self.assertEqual(self._callFUT(), None) + +class GetCurrentRegistryTests(unittest.TestCase): + def setUp(self): + testing.setUp() + + def tearDown(self): + testing.tearDown() + + def _callFUT(self): + from pyramid.threadlocal import get_current_registry + return get_current_registry() + + def test_it(self): + from pyramid.threadlocal import manager + try: + manager.push({'registry':123}) + self.assertEqual(self._callFUT(), 123) + finally: + manager.pop() + +class GetCurrentRegistryWithoutTestingRegistry(unittest.TestCase): + def _callFUT(self): + from pyramid.threadlocal import get_current_registry + return get_current_registry() + + def test_it(self): + from pyramid.registry import global_registry + self.assertEqual(self._callFUT(), global_registry) + diff --git a/src/pyramid/tests/test_traversal.py b/src/pyramid/tests/test_traversal.py new file mode 100644 index 000000000..437fe46df --- /dev/null +++ b/src/pyramid/tests/test_traversal.py @@ -0,0 +1,1221 @@ +# -*- coding: utf-8 -*- +import unittest + +from pyramid.testing import cleanUp + +from pyramid.compat import ( + text_, + native_, + text_type, + url_quote, + PY2, + ) + +class TraversalPathTests(unittest.TestCase): + def _callFUT(self, path): + from pyramid.traversal import traversal_path + return traversal_path(path) + + def test_utf8(self): + la = b'La Pe\xc3\xb1a' + encoded = url_quote(la) + decoded = text_(la, 'utf-8') + path = '/'.join([encoded, encoded]) + result = self._callFUT(path) + self.assertEqual(result, (decoded, decoded)) + + def test_utf16(self): + from pyramid.exceptions import URLDecodeError + la = text_(b'La Pe\xc3\xb1a', 'utf-8').encode('utf-16') + encoded = url_quote(la) + path = '/'.join([encoded, encoded]) + self.assertRaises(URLDecodeError, self._callFUT, path) + + def test_unicode_highorder_chars(self): + path = text_('/%E6%B5%81%E8%A1%8C%E8%B6%8B%E5%8A%BF') + self.assertEqual(self._callFUT(path), + (text_('\u6d41\u884c\u8d8b\u52bf', 'unicode_escape'),)) + + def test_element_urllquoted(self): + self.assertEqual(self._callFUT('/foo/space%20thing/bar'), + (text_('foo'), text_('space thing'), text_('bar'))) + + def test_unicode_undecodeable_to_ascii(self): + path = text_(b'/La Pe\xc3\xb1a', 'utf-8') + self.assertRaises(UnicodeEncodeError, self._callFUT, path) + +class TraversalPathInfoTests(unittest.TestCase): + def _callFUT(self, path): + from pyramid.traversal import traversal_path_info + return traversal_path_info(path) + + def test_path_startswith_endswith(self): + self.assertEqual(self._callFUT('/foo/'), (text_('foo'),)) + + def test_empty_elements(self): + self.assertEqual(self._callFUT('foo///'), (text_('foo'),)) + + def test_onedot(self): + self.assertEqual(self._callFUT('foo/./bar'), + (text_('foo'), text_('bar'))) + + def test_twodots(self): + self.assertEqual(self._callFUT('foo/../bar'), (text_('bar'),)) + + def test_twodots_at_start(self): + self.assertEqual(self._callFUT('../../bar'), (text_('bar'),)) + + def test_segments_are_unicode(self): + result = self._callFUT('/foo/bar') + self.assertEqual(type(result[0]), text_type) + self.assertEqual(type(result[1]), text_type) + + def test_same_value_returned_if_cached(self): + result1 = self._callFUT('/foo/bar') + result2 = self._callFUT('/foo/bar') + self.assertEqual(result1, (text_('foo'), text_('bar'))) + self.assertEqual(result2, (text_('foo'), text_('bar'))) + + def test_unicode_simple(self): + path = text_('/abc') + self.assertEqual(self._callFUT(path), (text_('abc'),)) + + def test_highorder(self): + la = b'La Pe\xc3\xb1a' + latin1 = native_(la) + result = self._callFUT(latin1) + self.assertEqual(result, (text_(la, 'utf-8'),)) + + def test_highorder_undecodeable(self): + from pyramid.exceptions import URLDecodeError + la = text_(b'La Pe\xc3\xb1a', 'utf-8') + notlatin1 = native_(la) + self.assertRaises(URLDecodeError, self._callFUT, notlatin1) + +class ResourceTreeTraverserTests(unittest.TestCase): + def setUp(self): + cleanUp() + + def tearDown(self): + cleanUp() + + def _getTargetClass(self): + from pyramid.traversal import ResourceTreeTraverser + return ResourceTreeTraverser + + def _makeOne(self, *arg, **kw): + klass = self._getTargetClass() + return klass(*arg, **kw) + + def _getEnviron(self, **kw): + environ = {} + environ.update(kw) + return environ + + def test_class_conforms_to_ITraverser(self): + from zope.interface.verify import verifyClass + from pyramid.interfaces import ITraverser + verifyClass(ITraverser, self._getTargetClass()) + + def test_instance_conforms_to_ITraverser(self): + from zope.interface.verify import verifyObject + from pyramid.interfaces import ITraverser + context = DummyContext() + verifyObject(ITraverser, self._makeOne(context)) + + def test_call_with_empty_pathinfo(self): + policy = self._makeOne(None) + environ = self._getEnviron() + request = DummyRequest(environ, path_info='') + result = policy(request) + self.assertEqual(result['context'], None) + self.assertEqual(result['view_name'], '') + self.assertEqual(result['subpath'], ()) + self.assertEqual(result['traversed'], ()) + self.assertEqual(result['root'], policy.root) + self.assertEqual(result['virtual_root'], policy.root) + self.assertEqual(result['virtual_root_path'], ()) + + def test_call_with_pathinfo_KeyError(self): + policy = self._makeOne(None) + environ = self._getEnviron() + request = DummyRequest(environ, toraise=KeyError) + result = policy(request) + self.assertEqual(result['context'], None) + self.assertEqual(result['view_name'], '') + self.assertEqual(result['subpath'], ()) + self.assertEqual(result['traversed'], ()) + self.assertEqual(result['root'], policy.root) + self.assertEqual(result['virtual_root'], policy.root) + self.assertEqual(result['virtual_root_path'], ()) + + def test_call_with_pathinfo_highorder(self): + path = text_(b'/Qu\xc3\xa9bec', 'utf-8') + foo = DummyContext(None, path) + root = DummyContext(foo, 'root') + policy = self._makeOne(root) + environ = self._getEnviron() + request = DummyRequest(environ, path_info=path) + result = policy(request) + self.assertEqual(result['context'], foo) + self.assertEqual(result['view_name'], '') + self.assertEqual(result['subpath'], ()) + self.assertEqual(result['traversed'], (path[1:],)) + self.assertEqual(result['root'], policy.root) + self.assertEqual(result['virtual_root'], policy.root) + self.assertEqual(result['virtual_root_path'], ()) + + def test_call_pathel_with_no_getitem(self): + policy = self._makeOne(None) + environ = self._getEnviron() + request = DummyRequest(environ, path_info=text_('/foo/bar')) + result = policy(request) + self.assertEqual(result['context'], None) + self.assertEqual(result['view_name'], 'foo') + self.assertEqual(result['subpath'], ('bar',)) + self.assertEqual(result['traversed'], ()) + self.assertEqual(result['root'], policy.root) + self.assertEqual(result['virtual_root'], policy.root) + self.assertEqual(result['virtual_root_path'], ()) + + def test_call_withconn_getitem_emptypath_nosubpath(self): + root = DummyContext() + policy = self._makeOne(root) + environ = self._getEnviron() + request = DummyRequest(environ, path_info=text_('')) + result = policy(request) + self.assertEqual(result['context'], root) + self.assertEqual(result['view_name'], '') + self.assertEqual(result['subpath'], ()) + self.assertEqual(result['traversed'], ()) + self.assertEqual(result['root'], root) + self.assertEqual(result['virtual_root'], root) + self.assertEqual(result['virtual_root_path'], ()) + + def test_call_withconn_getitem_withpath_nosubpath(self): + foo = DummyContext() + root = DummyContext(foo) + policy = self._makeOne(root) + environ = self._getEnviron() + request = DummyRequest(environ, path_info=text_('/foo/bar')) + result = policy(request) + self.assertEqual(result['context'], foo) + self.assertEqual(result['view_name'], 'bar') + self.assertEqual(result['subpath'], ()) + self.assertEqual(result['traversed'], (text_('foo'),)) + self.assertEqual(result['root'], root) + self.assertEqual(result['virtual_root'], root) + self.assertEqual(result['virtual_root_path'], ()) + + def test_call_withconn_getitem_withpath_withsubpath(self): + foo = DummyContext() + root = DummyContext(foo) + policy = self._makeOne(root) + environ = self._getEnviron() + request = DummyRequest(environ, path_info=text_('/foo/bar/baz/buz')) + result = policy(request) + self.assertEqual(result['context'], foo) + self.assertEqual(result['view_name'], 'bar') + self.assertEqual(result['subpath'], ('baz', 'buz')) + self.assertEqual(result['traversed'], (text_('foo'),)) + self.assertEqual(result['root'], root) + self.assertEqual(result['virtual_root'], root) + self.assertEqual(result['virtual_root_path'], ()) + + def test_call_with_explicit_viewname(self): + foo = DummyContext() + root = DummyContext(foo) + policy = self._makeOne(root) + environ = self._getEnviron() + request = DummyRequest(environ, path_info=text_('/@@foo')) + result = policy(request) + self.assertEqual(result['context'], root) + self.assertEqual(result['view_name'], 'foo') + self.assertEqual(result['subpath'], ()) + self.assertEqual(result['traversed'], ()) + self.assertEqual(result['root'], root) + self.assertEqual(result['virtual_root'], root) + self.assertEqual(result['virtual_root_path'], ()) + + def test_call_with_vh_root(self): + environ = self._getEnviron(HTTP_X_VHM_ROOT='/foo/bar') + baz = DummyContext(None, 'baz') + bar = DummyContext(baz, 'bar') + foo = DummyContext(bar, 'foo') + root = DummyContext(foo, 'root') + policy = self._makeOne(root) + request = DummyRequest(environ, path_info=text_('/baz')) + result = policy(request) + self.assertEqual(result['context'], baz) + self.assertEqual(result['view_name'], '') + self.assertEqual(result['subpath'], ()) + self.assertEqual(result['traversed'], + (text_('foo'), text_('bar'), text_('baz'))) + self.assertEqual(result['root'], root) + self.assertEqual(result['virtual_root'], bar) + self.assertEqual(result['virtual_root_path'], + (text_('foo'), text_('bar'))) + + def test_call_with_vh_root2(self): + environ = self._getEnviron(HTTP_X_VHM_ROOT='/foo') + baz = DummyContext(None, 'baz') + bar = DummyContext(baz, 'bar') + foo = DummyContext(bar, 'foo') + root = DummyContext(foo, 'root') + policy = self._makeOne(root) + request = DummyRequest(environ, path_info=text_('/bar/baz')) + result = policy(request) + self.assertEqual(result['context'], baz) + self.assertEqual(result['view_name'], '') + self.assertEqual(result['subpath'], ()) + self.assertEqual(result['traversed'], + (text_('foo'), text_('bar'), text_('baz'))) + self.assertEqual(result['root'], root) + self.assertEqual(result['virtual_root'], foo) + self.assertEqual(result['virtual_root_path'], (text_('foo'),)) + + def test_call_with_vh_root3(self): + environ = self._getEnviron(HTTP_X_VHM_ROOT='/') + baz = DummyContext() + bar = DummyContext(baz) + foo = DummyContext(bar) + root = DummyContext(foo) + policy = self._makeOne(root) + request = DummyRequest(environ, path_info=text_('/foo/bar/baz')) + result = policy(request) + self.assertEqual(result['context'], baz) + self.assertEqual(result['view_name'], '') + self.assertEqual(result['subpath'], ()) + self.assertEqual(result['traversed'], + (text_('foo'), text_('bar'), text_('baz'))) + self.assertEqual(result['root'], root) + self.assertEqual(result['virtual_root'], root) + self.assertEqual(result['virtual_root_path'], ()) + + def test_call_with_vh_root4(self): + environ = self._getEnviron(HTTP_X_VHM_ROOT='/foo/bar/baz') + baz = DummyContext(None, 'baz') + bar = DummyContext(baz, 'bar') + foo = DummyContext(bar, 'foo') + root = DummyContext(foo, 'root') + policy = self._makeOne(root) + request = DummyRequest(environ, path_info=text_('/')) + result = policy(request) + self.assertEqual(result['context'], baz) + self.assertEqual(result['view_name'], '') + self.assertEqual(result['subpath'], ()) + self.assertEqual(result['traversed'], + (text_('foo'), text_('bar'), text_('baz'))) + self.assertEqual(result['root'], root) + self.assertEqual(result['virtual_root'], baz) + self.assertEqual(result['virtual_root_path'], + (text_('foo'), text_('bar'), text_('baz'))) + + def test_call_with_vh_root_path_root(self): + policy = self._makeOne(None) + environ = self._getEnviron(HTTP_X_VHM_ROOT='/') + request = DummyRequest(environ, path_info=text_('/')) + result = policy(request) + self.assertEqual(result['context'], None) + self.assertEqual(result['view_name'], '') + self.assertEqual(result['subpath'], ()) + self.assertEqual(result['traversed'], ()) + self.assertEqual(result['root'], policy.root) + self.assertEqual(result['virtual_root'], policy.root) + self.assertEqual(result['virtual_root_path'], ()) + + def test_call_with_vh_root_highorder(self): + path = text_(b'Qu\xc3\xa9bec', 'utf-8') + bar = DummyContext(None, 'bar') + foo = DummyContext(bar, path) + root = DummyContext(foo, 'root') + policy = self._makeOne(root) + if PY2: + vhm_root = b'/Qu\xc3\xa9bec' + else: + vhm_root = b'/Qu\xc3\xa9bec'.decode('latin-1') + environ = self._getEnviron(HTTP_X_VHM_ROOT=vhm_root) + request = DummyRequest(environ, path_info=text_('/bar')) + result = policy(request) + self.assertEqual(result['context'], bar) + self.assertEqual(result['view_name'], '') + self.assertEqual(result['subpath'], ()) + self.assertEqual( + result['traversed'], + (path, text_('bar')) + ) + self.assertEqual(result['root'], policy.root) + self.assertEqual(result['virtual_root'], foo) + self.assertEqual( + result['virtual_root_path'], + (path,) + ) + + def test_path_info_raises_unicodedecodeerror(self): + from pyramid.exceptions import URLDecodeError + foo = DummyContext() + root = DummyContext(foo) + policy = self._makeOne(root) + environ = self._getEnviron() + toraise = UnicodeDecodeError('ascii', b'a', 2, 3, '5') + request = DummyRequest(environ, toraise=toraise) + request.matchdict = None + self.assertRaises(URLDecodeError, policy, request) + + def test_withroute_nothingfancy(self): + resource = DummyContext() + traverser = self._makeOne(resource) + request = DummyRequest({}) + request.matchdict = {} + result = traverser(request) + self.assertEqual(result['context'], resource) + self.assertEqual(result['view_name'], '') + self.assertEqual(result['subpath'], ()) + self.assertEqual(result['traversed'], ()) + self.assertEqual(result['root'], resource) + self.assertEqual(result['virtual_root'], resource) + self.assertEqual(result['virtual_root_path'], ()) + + def test_withroute_with_subpath_string(self): + resource = DummyContext() + traverser = self._makeOne(resource) + matchdict = {'subpath':'/a/b/c'} + request = DummyRequest({}) + request.matchdict = matchdict + result = traverser(request) + self.assertEqual(result['context'], resource) + self.assertEqual(result['view_name'], '') + self.assertEqual(result['subpath'], ('a', 'b','c')) + self.assertEqual(result['traversed'], ()) + self.assertEqual(result['root'], resource) + self.assertEqual(result['virtual_root'], resource) + self.assertEqual(result['virtual_root_path'], ()) + + def test_withroute_with_subpath_tuple(self): + resource = DummyContext() + traverser = self._makeOne(resource) + matchdict = {'subpath':('a', 'b', 'c')} + request = DummyRequest({}) + request.matchdict = matchdict + result = traverser(request) + self.assertEqual(result['context'], resource) + self.assertEqual(result['view_name'], '') + self.assertEqual(result['subpath'], ('a', 'b','c')) + self.assertEqual(result['traversed'], ()) + self.assertEqual(result['root'], resource) + self.assertEqual(result['virtual_root'], resource) + self.assertEqual(result['virtual_root_path'], ()) + + def test_withroute_and_traverse_string(self): + resource = DummyContext() + traverser = self._makeOne(resource) + matchdict = {'traverse':text_('foo/bar')} + request = DummyRequest({}) + request.matchdict = matchdict + result = traverser(request) + self.assertEqual(result['context'], resource) + self.assertEqual(result['view_name'], 'foo') + self.assertEqual(result['subpath'], ('bar',)) + self.assertEqual(result['traversed'], ()) + self.assertEqual(result['root'], resource) + self.assertEqual(result['virtual_root'], resource) + self.assertEqual(result['virtual_root_path'], ()) + + def test_withroute_and_traverse_tuple(self): + resource = DummyContext() + traverser = self._makeOne(resource) + matchdict = {'traverse':('foo', 'bar')} + request = DummyRequest({}) + request.matchdict = matchdict + result = traverser(request) + self.assertEqual(result['context'], resource) + self.assertEqual(result['view_name'], 'foo') + self.assertEqual(result['subpath'], ('bar',)) + self.assertEqual(result['traversed'], ()) + self.assertEqual(result['root'], resource) + self.assertEqual(result['virtual_root'], resource) + self.assertEqual(result['virtual_root_path'], ()) + + def test_withroute_and_traverse_empty(self): + resource = DummyContext() + traverser = self._makeOne(resource) + matchdict = {'traverse':''} + request = DummyRequest({}) + request.matchdict = matchdict + result = traverser(request) + self.assertEqual(result['context'], resource) + self.assertEqual(result['view_name'], '') + self.assertEqual(result['subpath'], ()) + self.assertEqual(result['traversed'], ()) + self.assertEqual(result['root'], resource) + self.assertEqual(result['virtual_root'], resource) + self.assertEqual(result['virtual_root_path'], ()) + + def test_withroute_and_traverse_and_vroot(self): + abc = DummyContext() + resource = DummyContext(next=abc) + environ = self._getEnviron(HTTP_X_VHM_ROOT='/abc') + request = DummyRequest(environ) + traverser = self._makeOne(resource) + matchdict = {'traverse':text_('/foo/bar')} + request.matchdict = matchdict + result = traverser(request) + self.assertEqual(result['context'], abc) + self.assertEqual(result['view_name'], 'foo') + self.assertEqual(result['subpath'], ('bar',)) + self.assertEqual(result['traversed'], ('abc', 'foo')) + self.assertEqual(result['root'], resource) + self.assertEqual(result['virtual_root'], abc) + self.assertEqual(result['virtual_root_path'], ('abc',)) + +class FindInterfaceTests(unittest.TestCase): + def _callFUT(self, context, iface): + from pyramid.traversal import find_interface + return find_interface(context, iface) + + def test_it_interface(self): + baz = DummyContext() + bar = DummyContext(baz) + foo = DummyContext(bar) + root = DummyContext(foo) + root.__parent__ = None + root.__name__ = 'root' + foo.__parent__ = root + foo.__name__ = 'foo' + bar.__parent__ = foo + bar.__name__ = 'bar' + baz.__parent__ = bar + baz.__name__ = 'baz' + from zope.interface import directlyProvides + from zope.interface import Interface + class IFoo(Interface): + pass + directlyProvides(root, IFoo) + result = self._callFUT(baz, IFoo) + self.assertEqual(result.__name__, 'root') + + def test_it_class(self): + class DummyRoot(object): + def __init__(self, child): + self.child = child + baz = DummyContext() + bar = DummyContext(baz) + foo = DummyContext(bar) + root = DummyRoot(foo) + root.__parent__ = None + root.__name__ = 'root' + foo.__parent__ = root + foo.__name__ = 'foo' + bar.__parent__ = foo + bar.__name__ = 'bar' + baz.__parent__ = bar + baz.__name__ = 'baz' + result = self._callFUT(baz, DummyRoot) + self.assertEqual(result.__name__, 'root') + +class FindRootTests(unittest.TestCase): + def _callFUT(self, context): + from pyramid.traversal import find_root + return find_root(context) + + def test_it(self): + dummy = DummyContext() + baz = DummyContext() + baz.__parent__ = dummy + baz.__name__ = 'baz' + dummy.__parent__ = None + dummy.__name__ = None + result = self._callFUT(baz) + self.assertEqual(result, dummy) + +class FindResourceTests(unittest.TestCase): + def _callFUT(self, context, name): + from pyramid.traversal import find_resource + return find_resource(context, name) + + def _registerTraverser(self, traverser): + from pyramid.threadlocal import get_current_registry + reg = get_current_registry() + from pyramid.interfaces import ITraverser + from zope.interface import Interface + reg.registerAdapter(traverser, (Interface,), ITraverser) + + def test_list(self): + resource = DummyContext() + traverser = make_traverser({'context':resource, 'view_name':''}) + self._registerTraverser(traverser) + result = self._callFUT(resource, ['']) + self.assertEqual(result, resource) + self.assertEqual(resource.request.environ['PATH_INFO'], '/') + + def test_generator(self): + resource = DummyContext() + traverser = make_traverser({'context':resource, 'view_name':''}) + self._registerTraverser(traverser) + def foo(): + yield '' + result = self._callFUT(resource, foo()) + self.assertEqual(result, resource) + self.assertEqual(resource.request.environ['PATH_INFO'], '/') + + def test_self_string_found(self): + resource = DummyContext() + traverser = make_traverser({'context':resource, 'view_name':''}) + self._registerTraverser(traverser) + result = self._callFUT(resource, '') + self.assertEqual(result, resource) + self.assertEqual(resource.request.environ['PATH_INFO'], '') + + def test_self_tuple_found(self): + resource = DummyContext() + traverser = make_traverser({'context':resource, 'view_name':''}) + self._registerTraverser(traverser) + result = self._callFUT(resource, ()) + self.assertEqual(result, resource) + self.assertEqual(resource.request.environ['PATH_INFO'], '') + + def test_relative_string_found(self): + resource = DummyContext() + baz = DummyContext() + traverser = make_traverser({'context':baz, 'view_name':''}) + self._registerTraverser(traverser) + result = self._callFUT(resource, 'baz') + self.assertEqual(result, baz) + self.assertEqual(resource.request.environ['PATH_INFO'], 'baz') + + def test_relative_tuple_found(self): + resource = DummyContext() + baz = DummyContext() + traverser = make_traverser({'context':baz, 'view_name':''}) + self._registerTraverser(traverser) + result = self._callFUT(resource, ('baz',)) + self.assertEqual(result, baz) + self.assertEqual(resource.request.environ['PATH_INFO'], 'baz') + + def test_relative_string_notfound(self): + resource = DummyContext() + baz = DummyContext() + traverser = make_traverser({'context':baz, 'view_name':'bar'}) + self._registerTraverser(traverser) + self.assertRaises(KeyError, self._callFUT, resource, 'baz') + self.assertEqual(resource.request.environ['PATH_INFO'], 'baz') + + def test_relative_tuple_notfound(self): + resource = DummyContext() + baz = DummyContext() + traverser = make_traverser({'context':baz, 'view_name':'bar'}) + self._registerTraverser(traverser) + self.assertRaises(KeyError, self._callFUT, resource, ('baz',)) + self.assertEqual(resource.request.environ['PATH_INFO'], 'baz') + + def test_absolute_string_found(self): + root = DummyContext() + resource = DummyContext() + resource.__parent__ = root + resource.__name__ = 'baz' + traverser = make_traverser({'context':root, 'view_name':''}) + self._registerTraverser(traverser) + result = self._callFUT(resource, '/') + self.assertEqual(result, root) + self.assertEqual(root.wascontext, True) + self.assertEqual(root.request.environ['PATH_INFO'], '/') + + def test_absolute_tuple_found(self): + root = DummyContext() + resource = DummyContext() + resource.__parent__ = root + resource.__name__ = 'baz' + traverser = make_traverser({'context':root, 'view_name':''}) + self._registerTraverser(traverser) + result = self._callFUT(resource, ('',)) + self.assertEqual(result, root) + self.assertEqual(root.wascontext, True) + self.assertEqual(root.request.environ['PATH_INFO'], '/') + + def test_absolute_string_notfound(self): + root = DummyContext() + resource = DummyContext() + resource.__parent__ = root + resource.__name__ = 'baz' + traverser = make_traverser({'context':root, 'view_name':'fuz'}) + self._registerTraverser(traverser) + self.assertRaises(KeyError, self._callFUT, resource, '/') + self.assertEqual(root.wascontext, True) + self.assertEqual(root.request.environ['PATH_INFO'], '/') + + def test_absolute_tuple_notfound(self): + root = DummyContext() + resource = DummyContext() + resource.__parent__ = root + resource.__name__ = 'baz' + traverser = make_traverser({'context':root, 'view_name':'fuz'}) + self._registerTraverser(traverser) + self.assertRaises(KeyError, self._callFUT, resource, ('',)) + self.assertEqual(root.wascontext, True) + self.assertEqual(root.request.environ['PATH_INFO'], '/') + + def test_absolute_unicode_found(self): + # test for bug wiggy found in wild, traceback stack: + # root = u'/%E6%B5%81%E8%A1%8C%E8%B6%8B%E5%8A%BF' + # wiggy's code: section=find_resource(page, root) + # find_resource L76: D = traverse(resource, path) + # traverse L291: return traverser(request) + # __call__ line 568: vpath_tuple = traversal_path(vpath) + # lru_cached line 91: f(*arg) + # traversal_path line 443: path.encode('ascii') + # UnicodeEncodeError: 'ascii' codec can't encode characters in + # position 1-12: ordinal not in range(128) + # + # solution: encode string to ascii in pyramid.traversal.traverse + # before passing it along to webob as path_info + from pyramid.traversal import ResourceTreeTraverser + unprintable = DummyContext() + root = DummyContext(unprintable) + unprintable.__parent__ = root + unprintable.__name__ = text_( + b'/\xe6\xb5\x81\xe8\xa1\x8c\xe8\xb6\x8b\xe5\x8a\xbf', 'utf-8') + root.__parent__ = None + root.__name__ = None + traverser = ResourceTreeTraverser + self._registerTraverser(traverser) + result = self._callFUT( + root, + text_(b'/%E6%B5%81%E8%A1%8C%E8%B6%8B%E5%8A%BF') + ) + self.assertEqual(result, unprintable) + +class ResourcePathTests(unittest.TestCase): + def _callFUT(self, resource, *elements): + from pyramid.traversal import resource_path + return resource_path(resource, *elements) + + def test_it(self): + baz = DummyContext() + bar = DummyContext(baz) + foo = DummyContext(bar) + root = DummyContext(foo) + root.__parent__ = None + root.__name__ = None + foo.__parent__ = root + foo.__name__ = 'foo ' + bar.__parent__ = foo + bar.__name__ = 'bar' + baz.__parent__ = bar + baz.__name__ = 'baz' + result = self._callFUT(baz, 'this/theotherthing', 'that') + self.assertEqual(result, '/foo%20/bar/baz/this%2Ftheotherthing/that') + + def test_root_default(self): + root = DummyContext() + root.__parent__ = None + root.__name__ = None + result = self._callFUT(root) + self.assertEqual(result, '/') + + def test_root_default_emptystring(self): + root = DummyContext() + root.__parent__ = None + root.__name__ = '' + result = self._callFUT(root) + self.assertEqual(result, '/') + + def test_root_object_nonnull_name_direct(self): + root = DummyContext() + root.__parent__ = None + root.__name__ = 'flubadub' + result = self._callFUT(root) + self.assertEqual(result, 'flubadub') # insane case + + def test_root_object_nonnull_name_indirect(self): + root = DummyContext() + root.__parent__ = None + root.__name__ = 'flubadub' + other = DummyContext() + other.__parent__ = root + other.__name__ = 'barker' + result = self._callFUT(other) + self.assertEqual(result, 'flubadub/barker') # insane case + + def test_nonroot_default(self): + root = DummyContext() + root.__parent__ = None + root.__name__ = None + other = DummyContext() + other.__parent__ = root + other.__name__ = 'other' + result = self._callFUT(other) + self.assertEqual(result, '/other') + + def test_path_with_None_itermediate_names(self): + root = DummyContext() + root.__parent__ = None + root.__name__ = None + other = DummyContext() + other.__parent__ = root + other.__name__ = None + other2 = DummyContext() + other2.__parent__ = other + other2.__name__ = 'other2' + result = self._callFUT(other2) + self.assertEqual(result, '//other2') + +class ResourcePathTupleTests(unittest.TestCase): + def _callFUT(self, resource, *elements): + from pyramid.traversal import resource_path_tuple + return resource_path_tuple(resource, *elements) + + def test_it(self): + baz = DummyContext() + bar = DummyContext(baz) + foo = DummyContext(bar) + root = DummyContext(foo) + root.__parent__ = None + root.__name__ = None + foo.__parent__ = root + foo.__name__ = 'foo ' + bar.__parent__ = foo + bar.__name__ = 'bar' + baz.__parent__ = bar + baz.__name__ = 'baz' + result = self._callFUT(baz, 'this/theotherthing', 'that') + self.assertEqual(result, ('','foo ', 'bar', 'baz', 'this/theotherthing', + 'that')) + + def test_root_default(self): + root = DummyContext() + root.__parent__ = None + root.__name__ = None + result = self._callFUT(root) + self.assertEqual(result, ('',)) + + def test_root_default_emptystring_name(self): + root = DummyContext() + root.__parent__ = None + root.__name__ = '' + other = DummyContext() + other.__parent__ = root + other.__name__ = 'other' + result = self._callFUT(other) + self.assertEqual(result, ('', 'other',)) + + def test_nonroot_default(self): + root = DummyContext() + root.__parent__ = None + root.__name__ = None + other = DummyContext() + other.__parent__ = root + other.__name__ = 'other' + result = self._callFUT(other) + self.assertEqual(result, ('', 'other')) + + def test_path_with_None_itermediate_names(self): + root = DummyContext() + root.__parent__ = None + root.__name__ = None + other = DummyContext() + other.__parent__ = root + other.__name__ = None + other2 = DummyContext() + other2.__parent__ = other + other2.__name__ = 'other2' + result = self._callFUT(other2) + self.assertEqual(result, ('', '', 'other2')) + +class QuotePathSegmentTests(unittest.TestCase): + def _callFUT(self, s): + from pyramid.traversal import quote_path_segment + return quote_path_segment(s) + + def test_unicode(self): + la = text_(b'/La Pe\xc3\xb1a', 'utf-8') + result = self._callFUT(la) + self.assertEqual(result, '%2FLa%20Pe%C3%B1a') + + def test_string(self): + s = '/ hello!' + result = self._callFUT(s) + self.assertEqual(result, '%2F%20hello!') + + def test_int(self): + s = 12345 + result = self._callFUT(s) + self.assertEqual(result, '12345') + + def test_long(self): + from pyramid.compat import long + import sys + s = long(sys.maxsize + 1) + result = self._callFUT(s) + expected = str(s) + self.assertEqual(result, expected) + + def test_other(self): + class Foo(object): + def __str__(self): + return 'abc' + s = Foo() + result = self._callFUT(s) + self.assertEqual(result, 'abc') + +class ResourceURLTests(unittest.TestCase): + def _makeOne(self, context, url): + return self._getTargetClass()(context, url) + + def _getTargetClass(self): + from pyramid.traversal import ResourceURL + return ResourceURL + + def test_instance_conforms_to_IResourceURL(self): + from pyramid.interfaces import IResourceURL + from zope.interface.verify import verifyObject + context = DummyContext() + request = DummyRequest() + verifyObject(IResourceURL, self._makeOne(context, request)) + + def test_IResourceURL_attributes_with_vroot(self): + from pyramid.interfaces import VH_ROOT_KEY + root = DummyContext() + root.__parent__ = None + root.__name__ = None + one = DummyContext() + one.__parent__ = root + one.__name__ = 'one' + two = DummyContext() + two.__parent__ = one + two.__name__ = 'two' + environ = {VH_ROOT_KEY:'/one'} + request = DummyRequest(environ) + context_url = self._makeOne(two, request) + self.assertEqual(context_url.physical_path, '/one/two/') + self.assertEqual(context_url.virtual_path, '/two/') + self.assertEqual(context_url.physical_path_tuple, ('', 'one', 'two','')) + self.assertEqual(context_url.virtual_path_tuple, ('', 'two', '')) + + def test_IResourceURL_attributes_vroot_ends_with_slash(self): + from pyramid.interfaces import VH_ROOT_KEY + root = DummyContext() + root.__parent__ = None + root.__name__ = None + one = DummyContext() + one.__parent__ = root + one.__name__ = 'one' + two = DummyContext() + two.__parent__ = one + two.__name__ = 'two' + environ = {VH_ROOT_KEY:'/one/'} + request = DummyRequest(environ) + context_url = self._makeOne(two, request) + self.assertEqual(context_url.physical_path, '/one/two/') + self.assertEqual(context_url.virtual_path, '/two/') + self.assertEqual(context_url.physical_path_tuple, ('', 'one', 'two','')) + self.assertEqual(context_url.virtual_path_tuple, ('', 'two', '')) + + def test_IResourceURL_attributes_no_vroot(self): + root = DummyContext() + root.__parent__ = None + root.__name__ = None + one = DummyContext() + one.__parent__ = root + one.__name__ = 'one' + two = DummyContext() + two.__parent__ = one + two.__name__ = 'two' + environ = {} + request = DummyRequest(environ) + context_url = self._makeOne(two, request) + self.assertEqual(context_url.physical_path, '/one/two/') + self.assertEqual(context_url.virtual_path, '/one/two/') + self.assertEqual(context_url.physical_path_tuple, ('', 'one', 'two','')) + self.assertEqual(context_url.virtual_path_tuple, ('', 'one', 'two', '')) + +class TestVirtualRoot(unittest.TestCase): + def setUp(self): + cleanUp() + + def tearDown(self): + cleanUp() + + def _callFUT(self, resource, request): + from pyramid.traversal import virtual_root + return virtual_root(resource, request) + + def _registerTraverser(self, traverser): + from pyramid.threadlocal import get_current_registry + reg = get_current_registry() + from pyramid.interfaces import ITraverser + from zope.interface import Interface + reg.registerAdapter(traverser, (Interface,), ITraverser) + + def test_virtual_root_no_virtual_root_path(self): + root = DummyContext() + root.__name__ = None + root.__parent__ = None + one = DummyContext() + one.__name__ = 'one' + one.__parent__ = root + request = DummyRequest() + result = self._callFUT(one, request) + self.assertEqual(result, root) + + def test_virtual_root_no_virtual_root_path_with_root_on_request(self): + context = DummyContext() + context.__parent__ = None + request = DummyRequest() + request.root = DummyContext() + result = self._callFUT(context, request) + self.assertEqual(result, request.root) + + def test_virtual_root_with_virtual_root_path(self): + from pyramid.interfaces import VH_ROOT_KEY + root = DummyContext() + root.__parent__ = None + context = DummyContext() + context.__name__ = 'one' + context.__parent__ = root + traversed_to = DummyContext() + environ = {VH_ROOT_KEY:'/one'} + request = DummyRequest(environ) + traverser = make_traverser({'context':traversed_to, 'view_name':''}) + self._registerTraverser(traverser) + result = self._callFUT(context, request) + self.assertEqual(result, traversed_to) + self.assertEqual(root.request.environ['PATH_INFO'], '/one') + + def test_default(self): + context = DummyContext() + request = _makeRequest() + request.environ['PATH_INFO'] = '/' + result = self._callFUT(context, request) + self.assertEqual(result, context) + + def test_default_no_registry_on_request(self): + context = DummyContext() + request = _makeRequest() + del request.registry + request.environ['PATH_INFO'] = '/' + result = self._callFUT(context, request) + self.assertEqual(result, context) + +class TraverseTests(unittest.TestCase): + def setUp(self): + cleanUp() + + def tearDown(self): + cleanUp() + + def _callFUT(self, context, name): + from pyramid.traversal import traverse + return traverse(context, name) + + def _registerTraverser(self, traverser): + from pyramid.threadlocal import get_current_registry + reg = get_current_registry() + from pyramid.interfaces import ITraverser + from zope.interface import Interface + reg.registerAdapter(traverser, (Interface,), ITraverser) + + def test_request_has_registry(self): + from pyramid.threadlocal import get_current_registry + resource = DummyContext() + traverser = make_traverser({'context':resource, 'view_name':''}) + self._registerTraverser(traverser) + self._callFUT(resource, ['']) + self.assertEqual(resource.request.registry, get_current_registry()) + + def test_list(self): + resource = DummyContext() + traverser = make_traverser({'context':resource, 'view_name':''}) + self._registerTraverser(traverser) + self._callFUT(resource, ['']) + self.assertEqual(resource.request.environ['PATH_INFO'], '/') + + def test_generator(self): + resource = DummyContext() + traverser = make_traverser({'context':resource, 'view_name':''}) + self._registerTraverser(traverser) + def foo(): + yield '' + self._callFUT(resource, foo()) + self.assertEqual(resource.request.environ['PATH_INFO'], '/') + + def test_self_string_found(self): + resource = DummyContext() + traverser = make_traverser({'context':resource, 'view_name':''}) + self._registerTraverser(traverser) + self._callFUT(resource, '') + self.assertEqual(resource.request.environ['PATH_INFO'], '') + + def test_self_unicode_found(self): + resource = DummyContext() + traverser = make_traverser({'context':resource, 'view_name':''}) + self._registerTraverser(traverser) + self._callFUT(resource, text_('')) + self.assertEqual(resource.request.environ['PATH_INFO'], '') + + def test_self_tuple_found(self): + resource = DummyContext() + traverser = make_traverser({'context':resource, 'view_name':''}) + self._registerTraverser(traverser) + self._callFUT(resource, ()) + self.assertEqual(resource.request.environ['PATH_INFO'], '') + + def test_relative_string_found(self): + resource = DummyContext() + baz = DummyContext() + traverser = make_traverser({'context':baz, 'view_name':''}) + self._registerTraverser(traverser) + self._callFUT(resource, 'baz') + self.assertEqual(resource.request.environ['PATH_INFO'], 'baz') + + def test_relative_tuple_found(self): + resource = DummyContext() + baz = DummyContext() + traverser = make_traverser({'context':baz, 'view_name':''}) + self._registerTraverser(traverser) + self._callFUT(resource, ('baz',)) + self.assertEqual(resource.request.environ['PATH_INFO'], 'baz') + + def test_absolute_string_found(self): + root = DummyContext() + resource = DummyContext() + resource.__parent__ = root + resource.__name__ = 'baz' + traverser = make_traverser({'context':root, 'view_name':''}) + self._registerTraverser(traverser) + self._callFUT(resource, '/') + self.assertEqual(root.wascontext, True) + self.assertEqual(root.request.environ['PATH_INFO'], '/') + + def test_absolute_tuple_found(self): + root = DummyContext() + resource = DummyContext() + resource.__parent__ = root + resource.__name__ = 'baz' + traverser = make_traverser({'context':root, 'view_name':''}) + self._registerTraverser(traverser) + self._callFUT(resource, ('',)) + self.assertEqual(root.wascontext, True) + self.assertEqual(root.request.environ['PATH_INFO'], '/') + + def test_empty_sequence(self): + root = DummyContext() + resource = DummyContext() + resource.__parent__ = root + resource.__name__ = 'baz' + traverser = make_traverser({'context':root, 'view_name':''}) + self._registerTraverser(traverser) + self._callFUT(resource, []) + self.assertEqual(resource.wascontext, True) + self.assertEqual(resource.request.environ['PATH_INFO'], '') + + def test_default_traverser(self): + resource = DummyContext() + result = self._callFUT(resource, '') + self.assertEqual(result['view_name'], '') + self.assertEqual(result['context'], resource) + + def test_requestfactory_overridden(self): + from pyramid.interfaces import IRequestFactory + from pyramid.request import Request + from pyramid.threadlocal import get_current_registry + reg = get_current_registry() + class MyRequest(Request): + pass + reg.registerUtility(MyRequest, IRequestFactory) + resource = DummyContext() + traverser = make_traverser({'context':resource, 'view_name':''}) + self._registerTraverser(traverser) + self._callFUT(resource, ['']) + self.assertEqual(resource.request.__class__, MyRequest) + +class TestDefaultRootFactory(unittest.TestCase): + def _getTargetClass(self): + from pyramid.traversal import DefaultRootFactory + return DefaultRootFactory + + def _makeOne(self, environ): + return self._getTargetClass()(environ) + + def test_it(self): + class DummyRequest(object): + pass + root = self._makeOne(DummyRequest()) + self.assertEqual(root.__parent__, None) + self.assertEqual(root.__name__, None) + +class Test__join_path_tuple(unittest.TestCase): + def _callFUT(self, tup): + from pyramid.traversal import _join_path_tuple + return _join_path_tuple(tup) + + def test_empty_tuple(self): + # tests "or '/'" case + result = self._callFUT(()) + self.assertEqual(result, '/') + + def test_nonempty_tuple(self): + result = self._callFUT(('x',)) + self.assertEqual(result, 'x') + + def test_segments_with_unsafes(self): + safe_segments = tuple(u"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-._~!$&'()*+,;=:@") + result = self._callFUT(safe_segments) + self.assertEqual(result, u'/'.join(safe_segments)) + unsafe_segments = tuple(chr(i) for i in range(0x20, 0x80) if not chr(i) in safe_segments) + (u'あ',) + result = self._callFUT(unsafe_segments) + self.assertEqual(result, u'/'.join(''.join('%%%02X' % (ord(c) if isinstance(c, str) else c) for c in unsafe_segment.encode('utf-8')) for unsafe_segment in unsafe_segments)) + + +def make_traverser(result): + class DummyTraverser(object): + def __init__(self, context): + self.context = context + context.wascontext = True + def __call__(self, request): + self.context.request = request + return result + return DummyTraverser + +class DummyContext(object): + __parent__ = None + def __init__(self, next=None, name=None): + self.next = next + self.__name__ = name + + def __getitem__(self, name): + if self.next is None: + raise KeyError(name) + return self.next + + def __repr__(self): + return ''%(self.__name__, id(self)) + +class DummyRequest: + + application_url = 'http://example.com:5432' # app_url never ends with slash + matchdict = None + matched_route = None + + def __init__(self, environ=None, path_info=text_('/'), toraise=None): + if environ is None: + environ = {} + self.environ = environ + self._set_path_info(path_info) + self.toraise = toraise + + def _get_path_info(self): + if self.toraise: + raise self.toraise + return self._path_info + + def _set_path_info(self, v): + self._path_info = v + + path_info = property(_get_path_info, _set_path_info) + + +def _makeRequest(environ=None): + from pyramid.registry import Registry + request = DummyRequest() + request.registry = Registry() + return request diff --git a/src/pyramid/tests/test_tweens.py b/src/pyramid/tests/test_tweens.py new file mode 100644 index 000000000..2e74ad7cf --- /dev/null +++ b/src/pyramid/tests/test_tweens.py @@ -0,0 +1,88 @@ +import unittest +from pyramid import testing + +class Test_excview_tween_factory(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def _makeOne(self, handler, registry=None): + from pyramid.tweens import excview_tween_factory + if registry is None: + registry = self.config.registry + return excview_tween_factory(handler, registry) + + def test_it_passthrough_no_exception(self): + dummy_response = DummyResponse() + def handler(request): + return dummy_response + tween = self._makeOne(handler) + request = DummyRequest() + result = tween(request) + self.assertTrue(result is dummy_response) + self.assertIsNone(request.exception) + self.assertIsNone(request.exc_info) + + def test_it_catches_notfound(self): + from pyramid.request import Request + from pyramid.httpexceptions import HTTPNotFound + self.config.add_notfound_view(lambda exc, request: exc) + def handler(request): + raise HTTPNotFound + tween = self._makeOne(handler) + request = Request.blank('/') + request.registry = self.config.registry + result = tween(request) + self.assertEqual(result.status, '404 Not Found') + self.assertIsInstance(request.exception, HTTPNotFound) + self.assertEqual(request.exception, request.exc_info[1]) + + def test_it_catches_with_predicate(self): + from pyramid.request import Request + from pyramid.response import Response + def excview(request): + return Response('foo') + self.config.add_view(excview, context=ValueError, request_method='GET') + def handler(request): + raise ValueError + tween = self._makeOne(handler) + request = Request.blank('/') + request.registry = self.config.registry + result = tween(request) + self.assertTrue(b'foo' in result.body) + self.assertIsInstance(request.exception, ValueError) + self.assertEqual(request.exception, request.exc_info[1]) + + def test_it_reraises_on_mismatch(self): + from pyramid.request import Request + def excview(request): pass + self.config.add_view(excview, context=ValueError, request_method='GET') + def handler(request): + raise ValueError + tween = self._makeOne(handler) + request = Request.blank('/') + request.registry = self.config.registry + request.method = 'POST' + self.assertRaises(ValueError, lambda: tween(request)) + self.assertIsNone(request.exception) + self.assertIsNone(request.exc_info) + + def test_it_reraises_on_no_match(self): + from pyramid.request import Request + def handler(request): + raise ValueError + tween = self._makeOne(handler) + request = Request.blank('/') + request.registry = self.config.registry + self.assertRaises(ValueError, lambda: tween(request)) + self.assertIsNone(request.exception) + self.assertIsNone(request.exc_info) + +class DummyRequest: + exception = None + exc_info = None + +class DummyResponse: + pass diff --git a/src/pyramid/tests/test_url.py b/src/pyramid/tests/test_url.py new file mode 100644 index 000000000..31b3dd571 --- /dev/null +++ b/src/pyramid/tests/test_url.py @@ -0,0 +1,1352 @@ +import os +import unittest +import warnings + +from pyramid import testing + +from pyramid.compat import ( + text_, + WIN, + ) + +class TestURLMethodsMixin(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def _makeOne(self, environ=None): + from pyramid.url import URLMethodsMixin + if environ is None: + environ = {} + class Request(URLMethodsMixin): + application_url = 'http://example.com:5432' + script_name = '' + def __init__(self, environ): + self.environ = environ + request = Request(environ) + request.registry = self.config.registry + return request + + def _registerResourceURL(self, reg): + from pyramid.interfaces import IResourceURL + from zope.interface import Interface + class DummyResourceURL(object): + physical_path = '/context/' + virtual_path = '/context/' + def __init__(self, context, request): pass + reg.registerAdapter(DummyResourceURL, (Interface, Interface), + IResourceURL) + return DummyResourceURL + + def test_resource_url_root_default(self): + request = self._makeOne() + self._registerResourceURL(request.registry) + root = DummyContext() + result = request.resource_url(root) + self.assertEqual(result, 'http://example.com:5432/context/') + + def test_resource_url_extra_args(self): + request = self._makeOne() + self._registerResourceURL(request.registry) + context = DummyContext() + result = request.resource_url(context, 'this/theotherthing', 'that') + self.assertEqual( + result, + 'http://example.com:5432/context/this%2Ftheotherthing/that') + + def test_resource_url_unicode_in_element_names(self): + request = self._makeOne() + self._registerResourceURL(request.registry) + uc = text_(b'La Pe\xc3\xb1a', 'utf-8') + context = DummyContext() + result = request.resource_url(context, uc) + self.assertEqual(result, + 'http://example.com:5432/context/La%20Pe%C3%B1a') + + def test_resource_url_at_sign_in_element_names(self): + request = self._makeOne() + self._registerResourceURL(request.registry) + context = DummyContext() + result = request.resource_url(context, '@@myview') + self.assertEqual(result, + 'http://example.com:5432/context/@@myview') + + def test_resource_url_element_names_url_quoted(self): + request = self._makeOne() + self._registerResourceURL(request.registry) + context = DummyContext() + result = request.resource_url(context, 'a b c') + self.assertEqual(result, 'http://example.com:5432/context/a%20b%20c') + + def test_resource_url_with_query_str(self): + request = self._makeOne() + self._registerResourceURL(request.registry) + context = DummyContext() + result = request.resource_url(context, 'a', query='(openlayers)') + self.assertEqual(result, + 'http://example.com:5432/context/a?(openlayers)') + + def test_resource_url_with_query_dict(self): + request = self._makeOne() + self._registerResourceURL(request.registry) + context = DummyContext() + uc = text_(b'La Pe\xc3\xb1a', 'utf-8') + result = request.resource_url(context, 'a', query={'a':uc}) + self.assertEqual(result, + 'http://example.com:5432/context/a?a=La+Pe%C3%B1a') + + def test_resource_url_with_query_seq(self): + request = self._makeOne() + self._registerResourceURL(request.registry) + context = DummyContext() + uc = text_(b'La Pe\xc3\xb1a', 'utf-8') + result = request.resource_url(context, 'a', query=[('a', 'hi there'), + ('b', uc)]) + self.assertEqual(result, + 'http://example.com:5432/context/a?a=hi+there&b=La+Pe%C3%B1a') + + def test_resource_url_with_query_empty(self): + request = self._makeOne() + self._registerResourceURL(request.registry) + context = DummyContext() + result = request.resource_url(context, 'a', query=[]) + self.assertEqual(result, + 'http://example.com:5432/context/a') + + def test_resource_url_with_query_None(self): + request = self._makeOne() + self._registerResourceURL(request.registry) + context = DummyContext() + result = request.resource_url(context, 'a', query=None) + self.assertEqual(result, + 'http://example.com:5432/context/a') + + def test_resource_url_anchor_is_after_root_when_no_elements(self): + request = self._makeOne() + self._registerResourceURL(request.registry) + context = DummyContext() + result = request.resource_url(context, anchor='a') + self.assertEqual(result, + 'http://example.com:5432/context/#a') + + def test_resource_url_anchor_is_after_elements_when_no_qs(self): + request = self._makeOne() + self._registerResourceURL(request.registry) + context = DummyContext() + result = request.resource_url(context, 'a', anchor='b') + self.assertEqual(result, + 'http://example.com:5432/context/a#b') + + def test_resource_url_anchor_is_after_qs_when_qs_is_present(self): + request = self._makeOne() + self._registerResourceURL(request.registry) + context = DummyContext() + result = request.resource_url(context, 'a', + query={'b':'c'}, anchor='d') + self.assertEqual(result, + 'http://example.com:5432/context/a?b=c#d') + + def test_resource_url_anchor_is_encoded_utf8_if_unicode(self): + request = self._makeOne() + self._registerResourceURL(request.registry) + context = DummyContext() + uc = text_(b'La Pe\xc3\xb1a', 'utf-8') + result = request.resource_url(context, anchor=uc) + self.assertEqual(result, + 'http://example.com:5432/context/#La%20Pe%C3%B1a') + + def test_resource_url_anchor_is_urlencoded_safe(self): + request = self._makeOne() + self._registerResourceURL(request.registry) + context = DummyContext() + result = request.resource_url(context, anchor=' /#?&+') + self.assertEqual(result, + 'http://example.com:5432/context/#%20/%23?&+') + + def test_resource_url_anchor_is_None(self): + request = self._makeOne() + self._registerResourceURL(request.registry) + context = DummyContext() + result = request.resource_url(context, anchor=None) + self.assertEqual(result, 'http://example.com:5432/context/') + + def test_resource_url_no_IResourceURL_registered(self): + # falls back to ResourceURL + root = DummyContext() + root.__name__ = '' + root.__parent__ = None + request = self._makeOne() + request.environ = {} + result = request.resource_url(root) + self.assertEqual(result, 'http://example.com:5432/') + + def test_resource_url_no_registry_on_request(self): + request = self._makeOne() + self._registerResourceURL(request.registry) + del request.registry + root = DummyContext() + result = request.resource_url(root) + self.assertEqual(result, 'http://example.com:5432/context/') + + def test_resource_url_with_app_url(self): + request = self._makeOne() + self._registerResourceURL(request.registry) + root = DummyContext() + result = request.resource_url(root, app_url='http://somewhere.com') + self.assertEqual(result, 'http://somewhere.com/context/') + + def test_resource_url_with_scheme(self): + environ = { + 'wsgi.url_scheme':'http', + 'SERVER_PORT':'8080', + 'SERVER_NAME':'example.com', + } + request = self._makeOne(environ) + self._registerResourceURL(request.registry) + root = DummyContext() + result = request.resource_url(root, scheme='https') + self.assertEqual(result, 'https://example.com/context/') + + def test_resource_url_with_host(self): + environ = { + 'wsgi.url_scheme':'http', + 'SERVER_PORT':'8080', + 'SERVER_NAME':'example.com', + } + request = self._makeOne(environ) + self._registerResourceURL(request.registry) + root = DummyContext() + result = request.resource_url(root, host='someotherhost.com') + self.assertEqual(result, 'http://someotherhost.com:8080/context/') + + def test_resource_url_with_port(self): + environ = { + 'wsgi.url_scheme':'http', + 'SERVER_PORT':'8080', + 'SERVER_NAME':'example.com', + } + request = self._makeOne(environ) + self._registerResourceURL(request.registry) + root = DummyContext() + result = request.resource_url(root, port='8181') + self.assertEqual(result, 'http://example.com:8181/context/') + + def test_resource_url_with_local_url(self): + environ = { + 'wsgi.url_scheme':'http', + 'SERVER_PORT':'8080', + 'SERVER_NAME':'example.com', + } + request = self._makeOne(environ) + self._registerResourceURL(request.registry) + root = DummyContext() + def resource_url(req, info): + self.assertEqual(req, request) + self.assertEqual(info['virtual_path'], '/context/') + self.assertEqual(info['physical_path'], '/context/') + self.assertEqual(info['app_url'], 'http://example.com:5432') + return 'http://example.com/contextabc/' + root.__resource_url__ = resource_url + result = request.resource_url(root) + self.assertEqual(result, 'http://example.com/contextabc/') + + def test_resource_url_with_route_name_no_remainder_on_adapter(self): + from pyramid.interfaces import IRoutesMapper + environ = { + 'wsgi.url_scheme':'http', + 'SERVER_PORT':'8080', + 'SERVER_NAME':'example.com', + } + request = self._makeOne(environ) + adapter = self._registerResourceURL(request.registry) + # no virtual_path_tuple on adapter + adapter.virtual_path = '/a/b/c/' + route = DummyRoute('/1/2/3') + mapper = DummyRoutesMapper(route) + request.registry.registerUtility(mapper, IRoutesMapper) + root = DummyContext() + result = request.resource_url(root, route_name='foo') + self.assertEqual(result, 'http://example.com:5432/1/2/3') + self.assertEqual(route.kw, {'traverse': ('', 'a', 'b', 'c', '')}) + + def test_resource_url_with_route_name_remainder_on_adapter(self): + from pyramid.interfaces import IRoutesMapper + environ = { + 'wsgi.url_scheme':'http', + 'SERVER_PORT':'8080', + 'SERVER_NAME':'example.com', + } + request = self._makeOne(environ) + adapter = self._registerResourceURL(request.registry) + # virtual_path_tuple on adapter + adapter.virtual_path_tuple = ('', 'a', 'b', 'c', '') + route = DummyRoute('/1/2/3') + mapper = DummyRoutesMapper(route) + request.registry.registerUtility(mapper, IRoutesMapper) + root = DummyContext() + result = request.resource_url(root, route_name='foo') + self.assertEqual(result, 'http://example.com:5432/1/2/3') + self.assertEqual(route.kw, {'traverse': ('', 'a', 'b', 'c', '')}) + + def test_resource_url_with_route_name_and_app_url(self): + from pyramid.interfaces import IRoutesMapper + environ = { + 'wsgi.url_scheme':'http', + 'SERVER_PORT':'8080', + 'SERVER_NAME':'example.com', + } + request = self._makeOne(environ) + adapter = self._registerResourceURL(request.registry) + # virtual_path_tuple on adapter + adapter.virtual_path_tuple = ('', 'a', 'b', 'c', '') + route = DummyRoute('/1/2/3') + mapper = DummyRoutesMapper(route) + request.registry.registerUtility(mapper, IRoutesMapper) + root = DummyContext() + result = request.resource_url(root, route_name='foo', app_url='app_url') + self.assertEqual(result, 'app_url/1/2/3') + self.assertEqual(route.kw, {'traverse': ('', 'a', 'b', 'c', '')}) + + def test_resource_url_with_route_name_and_scheme_host_port_etc(self): + from pyramid.interfaces import IRoutesMapper + environ = { + 'wsgi.url_scheme':'http', + 'SERVER_PORT':'8080', + 'SERVER_NAME':'example.com', + } + request = self._makeOne(environ) + adapter = self._registerResourceURL(request.registry) + # virtual_path_tuple on adapter + adapter.virtual_path_tuple = ('', 'a', 'b', 'c', '') + route = DummyRoute('/1/2/3') + mapper = DummyRoutesMapper(route) + request.registry.registerUtility(mapper, IRoutesMapper) + root = DummyContext() + result = request.resource_url(root, route_name='foo', scheme='scheme', + host='host', port='port', query={'a':'1'}, + anchor='anchor') + self.assertEqual(result, 'scheme://host:port/1/2/3?a=1#anchor') + self.assertEqual(route.kw, {'traverse': ('', 'a', 'b', 'c', '')}) + + def test_resource_url_with_route_name_and_route_kwargs(self): + from pyramid.interfaces import IRoutesMapper + environ = { + 'wsgi.url_scheme':'http', + 'SERVER_PORT':'8080', + 'SERVER_NAME':'example.com', + } + request = self._makeOne(environ) + adapter = self._registerResourceURL(request.registry) + # virtual_path_tuple on adapter + adapter.virtual_path_tuple = ('', 'a', 'b', 'c', '') + route = DummyRoute('/1/2/3') + mapper = DummyRoutesMapper(route) + request.registry.registerUtility(mapper, IRoutesMapper) + root = DummyContext() + result = request.resource_url( + root, route_name='foo', route_kw={'a':'1', 'b':'2'}) + self.assertEqual(result, 'http://example.com:5432/1/2/3') + self.assertEqual( + route.kw, + {'traverse': ('', 'a', 'b', 'c', ''), + 'a':'1', + 'b':'2'} + ) + + def test_resource_url_with_route_name_and_elements(self): + from pyramid.interfaces import IRoutesMapper + environ = { + 'wsgi.url_scheme':'http', + 'SERVER_PORT':'8080', + 'SERVER_NAME':'example.com', + } + request = self._makeOne(environ) + adapter = self._registerResourceURL(request.registry) + # virtual_path_tuple on adapter + adapter.virtual_path_tuple = ('', 'a', 'b', 'c', '') + route = DummyRoute('/1/2/3') + mapper = DummyRoutesMapper(route) + request.registry.registerUtility(mapper, IRoutesMapper) + root = DummyContext() + result = request.resource_url(root, 'e1', 'e2', route_name='foo') + self.assertEqual(result, 'http://example.com:5432/1/2/3/e1/e2') + self.assertEqual(route.kw, {'traverse': ('', 'a', 'b', 'c', '')}) + + def test_resource_url_with_route_name_and_remainder_name(self): + from pyramid.interfaces import IRoutesMapper + environ = { + 'wsgi.url_scheme':'http', + 'SERVER_PORT':'8080', + 'SERVER_NAME':'example.com', + } + request = self._makeOne(environ) + adapter = self._registerResourceURL(request.registry) + # virtual_path_tuple on adapter + adapter.virtual_path_tuple = ('', 'a', 'b', 'c', '') + route = DummyRoute('/1/2/3') + mapper = DummyRoutesMapper(route) + request.registry.registerUtility(mapper, IRoutesMapper) + root = DummyContext() + result = request.resource_url(root, route_name='foo', + route_remainder_name='fred') + self.assertEqual(result, 'http://example.com:5432/1/2/3') + self.assertEqual(route.kw, {'fred': ('', 'a', 'b', 'c', '')}) + + def test_resource_path(self): + request = self._makeOne() + self._registerResourceURL(request.registry) + root = DummyContext() + result = request.resource_path(root) + self.assertEqual(result, '/context/') + + def test_resource_path_kwarg(self): + request = self._makeOne() + self._registerResourceURL(request.registry) + root = DummyContext() + result = request.resource_path(root, anchor='abc') + self.assertEqual(result, '/context/#abc') + + def test_route_url_with_elements(self): + from pyramid.interfaces import IRoutesMapper + request = self._makeOne() + mapper = DummyRoutesMapper(route=DummyRoute('/1/2/3')) + request.registry.registerUtility(mapper, IRoutesMapper) + result = request.route_url('flub', 'extra1', 'extra2') + self.assertEqual(result, + 'http://example.com:5432/1/2/3/extra1/extra2') + + def test_route_url_with_elements_path_endswith_slash(self): + from pyramid.interfaces import IRoutesMapper + request = self._makeOne() + mapper = DummyRoutesMapper(route=DummyRoute('/1/2/3/')) + request.registry.registerUtility(mapper, IRoutesMapper) + result = request.route_url('flub', 'extra1', 'extra2') + self.assertEqual(result, + 'http://example.com:5432/1/2/3/extra1/extra2') + + def test_route_url_no_elements(self): + from pyramid.interfaces import IRoutesMapper + request = self._makeOne() + mapper = DummyRoutesMapper(route=DummyRoute('/1/2/3')) + request.registry.registerUtility(mapper, IRoutesMapper) + result = request.route_url('flub', a=1, b=2, c=3, _query={'a':1}, + _anchor=text_(b"foo")) + self.assertEqual(result, + 'http://example.com:5432/1/2/3?a=1#foo') + + def test_route_url_with_query_None(self): + from pyramid.interfaces import IRoutesMapper + request = self._makeOne() + mapper = DummyRoutesMapper(route=DummyRoute('/1/2/3')) + request.registry.registerUtility(mapper, IRoutesMapper) + result = request.route_url('flub', a=1, b=2, c=3, _query=None) + self.assertEqual(result, 'http://example.com:5432/1/2/3') + + def test_route_url_with_anchor_binary(self): + from pyramid.interfaces import IRoutesMapper + request = self._makeOne() + mapper = DummyRoutesMapper(route=DummyRoute('/1/2/3')) + request.registry.registerUtility(mapper, IRoutesMapper) + result = request.route_url('flub', _anchor=b"La Pe\xc3\xb1a") + + self.assertEqual(result, + 'http://example.com:5432/1/2/3#La%20Pe%C3%B1a') + + def test_route_url_with_anchor_unicode(self): + from pyramid.interfaces import IRoutesMapper + request = self._makeOne() + mapper = DummyRoutesMapper(route=DummyRoute('/1/2/3')) + request.registry.registerUtility(mapper, IRoutesMapper) + anchor = text_(b'La Pe\xc3\xb1a', 'utf-8') + result = request.route_url('flub', _anchor=anchor) + + self.assertEqual(result, + 'http://example.com:5432/1/2/3#La%20Pe%C3%B1a') + + def test_route_url_with_anchor_None(self): + from pyramid.interfaces import IRoutesMapper + request = self._makeOne() + mapper = DummyRoutesMapper(route=DummyRoute('/1/2/3')) + request.registry.registerUtility(mapper, IRoutesMapper) + result = request.route_url('flub', _anchor=None) + + self.assertEqual(result, 'http://example.com:5432/1/2/3') + + def test_route_url_with_query(self): + from pyramid.interfaces import IRoutesMapper + request = self._makeOne() + mapper = DummyRoutesMapper(route=DummyRoute('/1/2/3')) + request.registry.registerUtility(mapper, IRoutesMapper) + result = request.route_url('flub', _query={'q':'1'}) + self.assertEqual(result, + 'http://example.com:5432/1/2/3?q=1') + + def test_route_url_with_query_str(self): + from pyramid.interfaces import IRoutesMapper + request = self._makeOne() + mapper = DummyRoutesMapper(route=DummyRoute('/1/2/3')) + request.registry.registerUtility(mapper, IRoutesMapper) + result = request.route_url('flub', _query='(openlayers)') + self.assertEqual(result, + 'http://example.com:5432/1/2/3?(openlayers)') + + def test_route_url_with_empty_query(self): + from pyramid.interfaces import IRoutesMapper + request = self._makeOne() + mapper = DummyRoutesMapper(route=DummyRoute('/1/2/3')) + request.registry.registerUtility(mapper, IRoutesMapper) + result = request.route_url('flub', _query={}) + self.assertEqual(result, + 'http://example.com:5432/1/2/3') + + def test_route_url_with_app_url(self): + from pyramid.interfaces import IRoutesMapper + request = self._makeOne() + mapper = DummyRoutesMapper(route=DummyRoute('/1/2/3')) + request.registry.registerUtility(mapper, IRoutesMapper) + result = request.route_url('flub', _app_url='http://example2.com') + self.assertEqual(result, + 'http://example2.com/1/2/3') + + def test_route_url_with_host(self): + from pyramid.interfaces import IRoutesMapper + environ = { + 'wsgi.url_scheme':'http', + 'SERVER_PORT':'5432', + } + request = self._makeOne(environ) + mapper = DummyRoutesMapper(route=DummyRoute('/1/2/3')) + request.registry.registerUtility(mapper, IRoutesMapper) + result = request.route_url('flub', _host='someotherhost.com') + self.assertEqual(result, + 'http://someotherhost.com:5432/1/2/3') + + def test_route_url_with_port(self): + from pyramid.interfaces import IRoutesMapper + environ = { + 'wsgi.url_scheme':'http', + 'SERVER_PORT':'5432', + 'SERVER_NAME':'example.com', + } + request = self._makeOne(environ) + mapper = DummyRoutesMapper(route=DummyRoute('/1/2/3')) + request.registry.registerUtility(mapper, IRoutesMapper) + result = request.route_url('flub', _port='8080') + self.assertEqual(result, + 'http://example.com:8080/1/2/3') + + def test_route_url_with_scheme(self): + from pyramid.interfaces import IRoutesMapper + environ = { + 'wsgi.url_scheme':'http', + 'SERVER_PORT':'5432', + 'SERVER_NAME':'example.com', + } + request = self._makeOne(environ) + mapper = DummyRoutesMapper(route=DummyRoute('/1/2/3')) + request.registry.registerUtility(mapper, IRoutesMapper) + result = request.route_url('flub', _scheme='https') + self.assertEqual(result, + 'https://example.com/1/2/3') + + def test_route_url_generation_error(self): + from pyramid.interfaces import IRoutesMapper + request = self._makeOne() + mapper = DummyRoutesMapper(raise_exc=KeyError) + request.registry.registerUtility(mapper, IRoutesMapper) + mapper.raise_exc = KeyError + self.assertRaises(KeyError, request.route_url, 'flub', request, a=1) + + def test_route_url_generate_doesnt_receive_query_or_anchor(self): + from pyramid.interfaces import IRoutesMapper + request = self._makeOne() + route = DummyRoute(result='') + mapper = DummyRoutesMapper(route=route) + request.registry.registerUtility(mapper, IRoutesMapper) + result = request.route_url('flub', _query=dict(name='some_name')) + self.assertEqual(route.kw, {}) # shouldnt have anchor/query + self.assertEqual(result, 'http://example.com:5432?name=some_name') + + def test_route_url_with_pregenerator(self): + from pyramid.interfaces import IRoutesMapper + request = self._makeOne() + route = DummyRoute(result='/1/2/3') + def pregenerator(request, elements, kw): + return ('a',), {'_app_url':'http://example2.com'} + route.pregenerator = pregenerator + mapper = DummyRoutesMapper(route=route) + request.registry.registerUtility(mapper, IRoutesMapper) + result = request.route_url('flub') + self.assertEqual(result, 'http://example2.com/1/2/3/a') + self.assertEqual(route.kw, {}) # shouldnt have anchor/query + + def test_route_url_with_anchor_app_url_elements_and_query(self): + from pyramid.interfaces import IRoutesMapper + request = self._makeOne() + mapper = DummyRoutesMapper(route=DummyRoute(result='/1/2/3')) + request.registry.registerUtility(mapper, IRoutesMapper) + result = request.route_url('flub', 'element1', + _app_url='http://example2.com', + _anchor='anchor', _query={'q':'1'}) + self.assertEqual(result, + 'http://example2.com/1/2/3/element1?q=1#anchor') + + def test_route_url_integration_with_real_request(self): + # to try to replicate https://github.com/Pylons/pyramid/issues/213 + from pyramid.interfaces import IRoutesMapper + from pyramid.request import Request + request = Request.blank('/') + request.registry = self.config.registry + mapper = DummyRoutesMapper(route=DummyRoute('/1/2/3')) + request.registry.registerUtility(mapper, IRoutesMapper) + result = request.route_url('flub', 'extra1', 'extra2') + self.assertEqual(result, + 'http://localhost/1/2/3/extra1/extra2') + + + def test_current_route_url_current_request_has_no_route(self): + request = self._makeOne() + self.assertRaises(ValueError, request.current_route_url) + + def test_current_route_url_with_elements_query_and_anchor(self): + from pyramid.interfaces import IRoutesMapper + request = self._makeOne() + route = DummyRoute('/1/2/3') + mapper = DummyRoutesMapper(route=route) + request.matched_route = route + request.matchdict = {} + request.registry.registerUtility(mapper, IRoutesMapper) + result = request.current_route_url('extra1', 'extra2', _query={'a':1}, + _anchor=text_(b"foo")) + self.assertEqual(result, + 'http://example.com:5432/1/2/3/extra1/extra2?a=1#foo') + + def test_current_route_url_with_route_name(self): + from pyramid.interfaces import IRoutesMapper + request = self._makeOne() + route = DummyRoute('/1/2/3') + mapper = DummyRoutesMapper(route=route) + request.matched_route = route + request.matchdict = {} + request.registry.registerUtility(mapper, IRoutesMapper) + result = request.current_route_url('extra1', 'extra2', _query={'a':1}, + _anchor=text_(b"foo"), + _route_name='bar') + self.assertEqual(result, + 'http://example.com:5432/1/2/3/extra1/extra2?a=1#foo') + + def test_current_route_url_with_request_query(self): + from pyramid.interfaces import IRoutesMapper + from webob.multidict import GetDict + request = self._makeOne() + request.GET = GetDict([('q', '123')], {}) + route = DummyRoute('/1/2/3') + mapper = DummyRoutesMapper(route=route) + request.matched_route = route + request.matchdict = {} + request.registry.registerUtility(mapper, IRoutesMapper) + result = request.current_route_url() + self.assertEqual(result, + 'http://example.com:5432/1/2/3?q=123') + + def test_current_route_url_with_request_query_duplicate_entries(self): + from pyramid.interfaces import IRoutesMapper + from webob.multidict import GetDict + request = self._makeOne() + request.GET = GetDict( + [('q', '123'), ('b', '2'), ('b', '2'), ('q', '456')], {}) + route = DummyRoute('/1/2/3') + mapper = DummyRoutesMapper(route=route) + request.matched_route = route + request.matchdict = {} + request.registry.registerUtility(mapper, IRoutesMapper) + result = request.current_route_url() + self.assertEqual(result, + 'http://example.com:5432/1/2/3?q=123&b=2&b=2&q=456') + + def test_current_route_url_with_query_override(self): + from pyramid.interfaces import IRoutesMapper + from webob.multidict import GetDict + request = self._makeOne() + request.GET = GetDict([('q', '123')], {}) + route = DummyRoute('/1/2/3') + mapper = DummyRoutesMapper(route=route) + request.matched_route = route + request.matchdict = {} + request.registry.registerUtility(mapper, IRoutesMapper) + result = request.current_route_url(_query={'a':1}) + self.assertEqual(result, + 'http://example.com:5432/1/2/3?a=1') + + def test_current_route_path(self): + from pyramid.interfaces import IRoutesMapper + request = self._makeOne() + route = DummyRoute('/1/2/3') + mapper = DummyRoutesMapper(route=route) + request.matched_route = route + request.matchdict = {} + request.script_name = '/script_name' + request.registry.registerUtility(mapper, IRoutesMapper) + result = request.current_route_path('extra1', 'extra2', _query={'a':1}, + _anchor=text_(b"foo")) + self.assertEqual(result, '/script_name/1/2/3/extra1/extra2?a=1#foo') + + def test_route_path_with_elements(self): + from pyramid.interfaces import IRoutesMapper + request = self._makeOne() + mapper = DummyRoutesMapper(route=DummyRoute('/1/2/3')) + request.registry.registerUtility(mapper, IRoutesMapper) + request.script_name = '' + result = request.route_path('flub', 'extra1', 'extra2', + a=1, b=2, c=3, _query={'a':1}, + _anchor=text_(b"foo")) + self.assertEqual(result, '/1/2/3/extra1/extra2?a=1#foo') + + def test_route_path_with_script_name(self): + from pyramid.interfaces import IRoutesMapper + request = self._makeOne() + request.script_name = '/foo' + mapper = DummyRoutesMapper(route=DummyRoute('/1/2/3')) + request.registry.registerUtility(mapper, IRoutesMapper) + result = request.route_path('flub', 'extra1', 'extra2', + a=1, b=2, c=3, _query={'a':1}, + _anchor=text_(b"foo")) + self.assertEqual(result, '/foo/1/2/3/extra1/extra2?a=1#foo') + + def test_static_url_staticurlinfo_notfound(self): + request = self._makeOne() + self.assertRaises(ValueError, request.static_url, 'static/foo.css') + + def test_static_url_abspath(self): + from pyramid.interfaces import IStaticURLInfo + request = self._makeOne() + info = DummyStaticURLInfo('abc') + registry = request.registry + registry.registerUtility(info, IStaticURLInfo) + abspath = makeabs('static', 'foo.css') + result = request.static_url(abspath) + self.assertEqual(result, 'abc') + self.assertEqual(info.args, (makeabs('static', 'foo.css'), request, {})) + request = self._makeOne() + + def test_static_url_found_rel(self): + from pyramid.interfaces import IStaticURLInfo + request = self._makeOne() + info = DummyStaticURLInfo('abc') + request.registry.registerUtility(info, IStaticURLInfo) + result = request.static_url('static/foo.css') + self.assertEqual(result, 'abc') + self.assertEqual(info.args, + ('pyramid.tests:static/foo.css', request, {}) ) + + def test_static_url_abs(self): + from pyramid.interfaces import IStaticURLInfo + request = self._makeOne() + info = DummyStaticURLInfo('abc') + request.registry.registerUtility(info, IStaticURLInfo) + result = request.static_url('pyramid.tests:static/foo.css') + self.assertEqual(result, 'abc') + self.assertEqual(info.args, + ('pyramid.tests:static/foo.css', request, {}) ) + + def test_static_url_found_abs_no_registry_on_request(self): + from pyramid.interfaces import IStaticURLInfo + request = self._makeOne() + registry = request.registry + info = DummyStaticURLInfo('abc') + registry.registerUtility(info, IStaticURLInfo) + del request.registry + result = request.static_url('pyramid.tests:static/foo.css') + self.assertEqual(result, 'abc') + self.assertEqual(info.args, + ('pyramid.tests:static/foo.css', request, {}) ) + + def test_static_url_abspath_integration_with_staticurlinfo(self): + from pyramid.interfaces import IStaticURLInfo + from pyramid.config.views import StaticURLInfo + info = StaticURLInfo() + here = os.path.abspath(os.path.dirname(__file__)) + info.add(self.config, 'absstatic', here) + request = self._makeOne() + registry = request.registry + registry.registerUtility(info, IStaticURLInfo) + abspath = os.path.join(here, 'test_url.py') + result = request.static_url(abspath) + self.assertEqual(result, + 'http://example.com:5432/absstatic/test_url.py') + + def test_static_url_noscheme_uses_scheme_from_request(self): + from pyramid.interfaces import IStaticURLInfo + from pyramid.config.views import StaticURLInfo + info = StaticURLInfo() + here = os.path.abspath(os.path.dirname(__file__)) + info.add(self.config, '//subdomain.example.com/static', here) + request = self._makeOne({'wsgi.url_scheme': 'https'}) + registry = request.registry + registry.registerUtility(info, IStaticURLInfo) + abspath = os.path.join(here, 'test_url.py') + result = request.static_url(abspath) + self.assertEqual(result, + 'https://subdomain.example.com/static/test_url.py') + + def test_static_path_abspath(self): + from pyramid.interfaces import IStaticURLInfo + request = self._makeOne() + request.script_name = '/foo' + info = DummyStaticURLInfo('abc') + registry = request.registry + registry.registerUtility(info, IStaticURLInfo) + abspath = makeabs('static', 'foo.css') + result = request.static_path(abspath) + self.assertEqual(result, 'abc') + self.assertEqual(info.args, (makeabs('static', 'foo.css'), request, + {'_app_url':'/foo'}) + ) + + def test_static_path_found_rel(self): + from pyramid.interfaces import IStaticURLInfo + request = self._makeOne() + request.script_name = '/foo' + info = DummyStaticURLInfo('abc') + request.registry.registerUtility(info, IStaticURLInfo) + result = request.static_path('static/foo.css') + self.assertEqual(result, 'abc') + self.assertEqual(info.args, + ('pyramid.tests:static/foo.css', request, + {'_app_url':'/foo'}) + ) + + def test_static_path_abs(self): + from pyramid.interfaces import IStaticURLInfo + request = self._makeOne() + request.script_name = '/foo' + info = DummyStaticURLInfo('abc') + request.registry.registerUtility(info, IStaticURLInfo) + result = request.static_path('pyramid.tests:static/foo.css') + self.assertEqual(result, 'abc') + self.assertEqual(info.args, + ('pyramid.tests:static/foo.css', request, + {'_app_url':'/foo'}) + ) + + def test_static_path(self): + from pyramid.interfaces import IStaticURLInfo + request = self._makeOne() + request.script_name = '/foo' + info = DummyStaticURLInfo('abc') + request.registry.registerUtility(info, IStaticURLInfo) + result = request.static_path('static/foo.css') + self.assertEqual(result, 'abc') + self.assertEqual(info.args, + ('pyramid.tests:static/foo.css', request, + {'_app_url':'/foo'}) + ) + + def test_partial_application_url_with_http_host_default_port_http(self): + environ = { + 'wsgi.url_scheme':'http', + 'HTTP_HOST':'example.com:80', + } + request = self._makeOne(environ) + result = request._partial_application_url() + self.assertEqual(result, 'http://example.com') + + def test_partial_application_url_with_http_host_default_port_https(self): + environ = { + 'wsgi.url_scheme':'https', + 'HTTP_HOST':'example.com:443', + } + request = self._makeOne(environ) + result = request._partial_application_url() + self.assertEqual(result, 'https://example.com') + + def test_partial_application_url_with_http_host_nondefault_port_http(self): + environ = { + 'wsgi.url_scheme':'http', + 'HTTP_HOST':'example.com:8080', + } + request = self._makeOne(environ) + result = request._partial_application_url() + self.assertEqual(result, 'http://example.com:8080') + + def test_partial_application_url_with_http_host_nondefault_port_https(self): + environ = { + 'wsgi.url_scheme':'https', + 'HTTP_HOST':'example.com:4443', + } + request = self._makeOne(environ) + result = request._partial_application_url() + self.assertEqual(result, 'https://example.com:4443') + + def test_partial_application_url_with_http_host_no_colon(self): + environ = { + 'wsgi.url_scheme':'http', + 'HTTP_HOST':'example.com', + 'SERVER_PORT':'80', + } + request = self._makeOne(environ) + result = request._partial_application_url() + self.assertEqual(result, 'http://example.com') + + def test_partial_application_url_no_http_host(self): + environ = { + 'wsgi.url_scheme':'http', + 'SERVER_NAME':'example.com', + 'SERVER_PORT':'80', + } + request = self._makeOne(environ) + result = request._partial_application_url() + self.assertEqual(result, 'http://example.com') + + def test_partial_application_replace_port(self): + environ = { + 'wsgi.url_scheme':'http', + 'SERVER_NAME':'example.com', + 'SERVER_PORT':'80', + } + request = self._makeOne(environ) + result = request._partial_application_url(port=8080) + self.assertEqual(result, 'http://example.com:8080') + + def test_partial_application_replace_scheme_https_special_case(self): + environ = { + 'wsgi.url_scheme':'http', + 'SERVER_NAME':'example.com', + 'SERVER_PORT':'80', + } + request = self._makeOne(environ) + result = request._partial_application_url(scheme='https') + self.assertEqual(result, 'https://example.com') + + def test_partial_application_replace_scheme_https_special_case_avoid(self): + environ = { + 'wsgi.url_scheme':'http', + 'SERVER_NAME':'example.com', + 'SERVER_PORT':'80', + } + request = self._makeOne(environ) + result = request._partial_application_url(scheme='https', port='8080') + self.assertEqual(result, 'https://example.com:8080') + + def test_partial_application_replace_scheme_http_special_case(self): + environ = { + 'wsgi.url_scheme':'https', + 'SERVER_NAME':'example.com', + 'SERVER_PORT':'8080', + } + request = self._makeOne(environ) + result = request._partial_application_url(scheme='http') + self.assertEqual(result, 'http://example.com') + + def test_partial_application_replace_scheme_http_special_case_avoid(self): + environ = { + 'wsgi.url_scheme':'https', + 'SERVER_NAME':'example.com', + 'SERVER_PORT':'8000', + } + request = self._makeOne(environ) + result = request._partial_application_url(scheme='http', port='8080') + self.assertEqual(result, 'http://example.com:8080') + + def test_partial_application_replace_host_no_port(self): + environ = { + 'wsgi.url_scheme':'http', + 'SERVER_NAME':'example.com', + 'SERVER_PORT':'80', + } + request = self._makeOne(environ) + result = request._partial_application_url(host='someotherhost.com') + self.assertEqual(result, 'http://someotherhost.com') + + def test_partial_application_replace_host_with_port(self): + environ = { + 'wsgi.url_scheme':'http', + 'SERVER_NAME':'example.com', + 'SERVER_PORT':'8000', + } + request = self._makeOne(environ) + result = request._partial_application_url(host='someotherhost.com:8080') + self.assertEqual(result, 'http://someotherhost.com:8080') + + def test_partial_application_replace_host_and_port(self): + environ = { + 'wsgi.url_scheme':'http', + 'SERVER_NAME':'example.com', + 'SERVER_PORT':'80', + } + request = self._makeOne(environ) + result = request._partial_application_url(host='someotherhost.com:8080', + port='8000') + self.assertEqual(result, 'http://someotherhost.com:8000') + + def test_partial_application_replace_host_port_and_scheme(self): + environ = { + 'wsgi.url_scheme':'http', + 'SERVER_NAME':'example.com', + 'SERVER_PORT':'80', + } + request = self._makeOne(environ) + result = request._partial_application_url( + host='someotherhost.com:8080', + port='8000', + scheme='https', + ) + self.assertEqual(result, 'https://someotherhost.com:8000') + + def test_partial_application_url_with_custom_script_name(self): + environ = { + 'wsgi.url_scheme':'http', + 'SERVER_NAME':'example.com', + 'SERVER_PORT':'8000', + } + request = self._makeOne(environ) + request.script_name = '/abc' + result = request._partial_application_url() + self.assertEqual(result, 'http://example.com:8000/abc') + +class Test_route_url(unittest.TestCase): + def _callFUT(self, route_name, request, *elements, **kw): + from pyramid.url import route_url + return route_url(route_name, request, *elements, **kw) + + def _makeRequest(self): + class Request(object): + def route_url(self, route_name, *elements, **kw): + self.route_name = route_name + self.elements = elements + self.kw = kw + return 'route url' + return Request() + + def test_it(self): + request = self._makeRequest() + result = self._callFUT('abc', request, 'a', _app_url='') + self.assertEqual(result, 'route url') + self.assertEqual(request.route_name, 'abc') + self.assertEqual(request.elements, ('a',)) + self.assertEqual(request.kw, {'_app_url':''}) + +class Test_route_path(unittest.TestCase): + def _callFUT(self, route_name, request, *elements, **kw): + from pyramid.url import route_path + return route_path(route_name, request, *elements, **kw) + + def _makeRequest(self): + class Request(object): + def route_path(self, route_name, *elements, **kw): + self.route_name = route_name + self.elements = elements + self.kw = kw + return 'route path' + return Request() + + def test_it(self): + request = self._makeRequest() + result = self._callFUT('abc', request, 'a', _app_url='') + self.assertEqual(result, 'route path') + self.assertEqual(request.route_name, 'abc') + self.assertEqual(request.elements, ('a',)) + self.assertEqual(request.kw, {'_app_url':''}) + +class Test_resource_url(unittest.TestCase): + def _callFUT(self, resource, request, *elements, **kw): + from pyramid.url import resource_url + return resource_url(resource, request, *elements, **kw) + + def _makeRequest(self): + class Request(object): + def resource_url(self, resource, *elements, **kw): + self.resource = resource + self.elements = elements + self.kw = kw + return 'resource url' + return Request() + + def test_it(self): + request = self._makeRequest() + result = self._callFUT('abc', request, 'a', _app_url='') + self.assertEqual(result, 'resource url') + self.assertEqual(request.resource, 'abc') + self.assertEqual(request.elements, ('a',)) + self.assertEqual(request.kw, {'_app_url':''}) + +class Test_static_url(unittest.TestCase): + def _callFUT(self, path, request, **kw): + from pyramid.url import static_url + return static_url(path, request, **kw) + + def _makeRequest(self): + class Request(object): + def static_url(self, path, **kw): + self.path = path + self.kw = kw + return 'static url' + return Request() + + def test_it_abs(self): + request = self._makeRequest() + result = self._callFUT('/foo/bar/abc', request, _app_url='') + self.assertEqual(result, 'static url') + self.assertEqual(request.path, '/foo/bar/abc') + self.assertEqual(request.kw, {'_app_url':''}) + + def test_it_absspec(self): + request = self._makeRequest() + result = self._callFUT('foo:abc', request, _anchor='anchor') + self.assertEqual(result, 'static url') + self.assertEqual(request.path, 'foo:abc') + self.assertEqual(request.kw, {'_anchor':'anchor'}) + + def test_it_rel(self): + request = self._makeRequest() + result = self._callFUT('abc', request, _app_url='') + self.assertEqual(result, 'static url') + self.assertEqual(request.path, 'pyramid.tests:abc') + self.assertEqual(request.kw, {'_app_url':''}) + +class Test_static_path(unittest.TestCase): + def _callFUT(self, path, request, **kw): + from pyramid.url import static_path + return static_path(path, request, **kw) + + def _makeRequest(self): + class Request(object): + def static_path(self, path, **kw): + self.path = path + self.kw = kw + return 'static path' + return Request() + + def test_it_abs(self): + request = self._makeRequest() + result = self._callFUT('/foo/bar/abc', request, _anchor='anchor') + self.assertEqual(result, 'static path') + self.assertEqual(request.path, '/foo/bar/abc') + self.assertEqual(request.kw, {'_anchor':'anchor'}) + + def test_it_absspec(self): + request = self._makeRequest() + result = self._callFUT('foo:abc', request, _anchor='anchor') + self.assertEqual(result, 'static path') + self.assertEqual(request.path, 'foo:abc') + self.assertEqual(request.kw, {'_anchor':'anchor'}) + + def test_it_rel(self): + request = self._makeRequest() + result = self._callFUT('abc', request, _app_url='') + self.assertEqual(result, 'static path') + self.assertEqual(request.path, 'pyramid.tests:abc') + self.assertEqual(request.kw, {'_app_url':''}) + +class Test_current_route_url(unittest.TestCase): + def _callFUT(self, request, *elements, **kw): + from pyramid.url import current_route_url + return current_route_url(request, *elements, **kw) + + def _makeRequest(self): + class Request(object): + def current_route_url(self, *elements, **kw): + self.elements = elements + self.kw = kw + return 'current route url' + return Request() + + def test_it(self): + request = self._makeRequest() + result = self._callFUT(request, 'abc', _app_url='') + self.assertEqual(result, 'current route url') + self.assertEqual(request.elements, ('abc',)) + self.assertEqual(request.kw, {'_app_url':''}) + +class Test_current_route_path(unittest.TestCase): + def _callFUT(self, request, *elements, **kw): + from pyramid.url import current_route_path + return current_route_path(request, *elements, **kw) + + def _makeRequest(self): + class Request(object): + def current_route_path(self, *elements, **kw): + self.elements = elements + self.kw = kw + return 'current route path' + return Request() + + def test_it(self): + request = self._makeRequest() + result = self._callFUT(request, 'abc', _anchor='abc') + self.assertEqual(result, 'current route path') + self.assertEqual(request.elements, ('abc',)) + self.assertEqual(request.kw, {'_anchor':'abc'}) + +class Test_external_static_url_integration(unittest.TestCase): + + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def _makeRequest(self): + from pyramid.request import Request + return Request.blank('/') + + def test_generate_external_url(self): + self.config.add_route('acme', 'https://acme.org/path/{foo}') + request = self._makeRequest() + request.registry = self.config.registry + self.assertEqual( + request.route_url('acme', foo='bar'), + 'https://acme.org/path/bar') + + def test_generate_external_url_without_scheme(self): + self.config.add_route('acme', '//acme.org/path/{foo}') + request = self._makeRequest() + request.registry = self.config.registry + self.assertEqual( + request.route_url('acme', foo='bar'), + 'http://acme.org/path/bar') + + def test_generate_external_url_with_explicit_scheme(self): + self.config.add_route('acme', '//acme.org/path/{foo}') + request = self._makeRequest() + request.registry = self.config.registry + self.assertEqual( + request.route_url('acme', foo='bar', _scheme='https'), + 'https://acme.org/path/bar') + + def test_generate_external_url_with_explicit_app_url(self): + self.config.add_route('acme', 'http://acme.org/path/{foo}') + request = self._makeRequest() + request.registry = self.config.registry + self.assertRaises(ValueError, + request.route_url, 'acme', foo='bar', _app_url='http://fakeme.com') + + def test_generate_external_url_route_path(self): + self.config.add_route('acme', 'https://acme.org/path/{foo}') + request = self._makeRequest() + request.registry = self.config.registry + self.assertRaises(ValueError, request.route_path, 'acme', foo='bar') + + def test_generate_external_url_with_pregenerator(self): + def pregenerator(request, elements, kw): + kw['_query'] = {'q': 'foo'} + return elements, kw + self.config.add_route('acme', 'https://acme.org/path/{foo}', + pregenerator=pregenerator) + request = self._makeRequest() + request.registry = self.config.registry + self.assertEqual( + request.route_url('acme', foo='bar'), + 'https://acme.org/path/bar?q=foo') + + def test_external_url_with_route_prefix(self): + def includeme(config): + config.add_route('acme', '//acme.org/{foo}') + self.config.include(includeme, route_prefix='some_prefix') + request = self._makeRequest() + request.registry = self.config.registry + self.assertEqual( + request.route_url('acme', foo='bar'), + 'http://acme.org/bar') + +class Test_with_route_prefix(unittest.TestCase): + + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def _makeRequest(self, route): + from pyramid.request import Request + return Request.blank(route) + + def test_old_route_is_preserved(self): + self.config.route_prefix = 'old_prefix' + with self.config.route_prefix_context('new_addon'): + assert 'new_addon' in self.config.route_prefix + + assert 'old_prefix' == self.config.route_prefix + + def test_route_prefix_none(self): + self.config.route_prefix = 'old_prefix' + with self.config.route_prefix_context(None): + assert 'old_prefix' == self.config.route_prefix + + assert 'old_prefix' == self.config.route_prefix + + def test_route_prefix_empty(self): + self.config.route_prefix = 'old_prefix' + with self.config.route_prefix_context(''): + assert 'old_prefix' == self.config.route_prefix + + assert 'old_prefix' == self.config.route_prefix + + def test_route_has_prefix(self): + with self.config.route_prefix_context('bar'): + self.config.add_route('acme', '/foo') + request = self._makeRequest('/') + self.assertEqual( + request.route_url('acme'), + 'http://localhost/bar/foo', + ) + + def test_route_does_not_have_prefix(self): + with self.config.route_prefix_context('bar'): + pass + + self.config.add_route('acme', '/foo') + request = self._makeRequest('/') + self.assertEqual( + request.route_url('acme'), + 'http://localhost/foo', + ) + + def test_error_reset_prefix(self): + self.config.route_prefix = 'old_prefix' + + try: + with self.config.route_prefix_context('new_prefix'): + raise RuntimeError + except RuntimeError: + pass + + assert self.config.route_prefix == 'old_prefix' + +class DummyContext(object): + def __init__(self, next=None): + self.next = next + +class DummyRoutesMapper: + raise_exc = None + def __init__(self, route=None, raise_exc=False): + self.route = route + + def get_route(self, route_name): + return self.route + +class DummyRoute: + pregenerator = None + name = 'route' + def __init__(self, result='/1/2/3'): + self.result = result + + def generate(self, kw): + self.kw = kw + return self.result + +class DummyStaticURLInfo: + def __init__(self, result): + self.result = result + + def generate(self, path, request, **kw): + self.args = path, request, kw + return self.result + +def makeabs(*elements): + if WIN: # pragma: no cover + return r'c:\\' + os.path.sep.join(elements) + else: + return os.path.sep + os.path.sep.join(elements) diff --git a/src/pyramid/tests/test_urldispatch.py b/src/pyramid/tests/test_urldispatch.py new file mode 100644 index 000000000..06f4ad793 --- /dev/null +++ b/src/pyramid/tests/test_urldispatch.py @@ -0,0 +1,539 @@ +import unittest +from pyramid import testing +from pyramid.compat import ( + text_, + PY2, + ) + +class TestRoute(unittest.TestCase): + def _getTargetClass(self): + from pyramid.urldispatch import Route + return Route + + def _makeOne(self, *arg): + return self._getTargetClass()(*arg) + + def test_provides_IRoute(self): + from pyramid.interfaces import IRoute + from zope.interface.verify import verifyObject + verifyObject(IRoute, self._makeOne('name', 'pattern')) + + def test_ctor(self): + import types + route = self._makeOne('name', ':path', 'factory') + self.assertEqual(route.pattern, ':path') + self.assertEqual(route.path, ':path') + self.assertEqual(route.name, 'name') + self.assertEqual(route.factory, 'factory') + self.assertTrue(route.generate.__class__ is types.FunctionType) + self.assertTrue(route.match.__class__ is types.FunctionType) + + def test_ctor_defaults(self): + import types + route = self._makeOne('name', ':path') + self.assertEqual(route.pattern, ':path') + self.assertEqual(route.path, ':path') + self.assertEqual(route.name, 'name') + self.assertEqual(route.factory, None) + self.assertTrue(route.generate.__class__ is types.FunctionType) + self.assertTrue(route.match.__class__ is types.FunctionType) + + def test_match(self): + route = self._makeOne('name', ':path') + self.assertEqual(route.match('/whatever'), {'path':'whatever'}) + + def test_generate(self): + route = self._makeOne('name', ':path') + self.assertEqual(route.generate({'path':'abc'}), '/abc') + +class RoutesMapperTests(unittest.TestCase): + def setUp(self): + testing.setUp() + + def tearDown(self): + testing.tearDown() + + def _getRequest(self, **kw): + from pyramid.threadlocal import get_current_registry + environ = {'SERVER_NAME':'localhost', + 'wsgi.url_scheme':'http'} + environ.update(kw) + request = DummyRequest(environ) + reg = get_current_registry() + request.registry = reg + return request + + def _getTargetClass(self): + from pyramid.urldispatch import RoutesMapper + return RoutesMapper + + def _makeOne(self): + klass = self._getTargetClass() + return klass() + + def test_provides_IRoutesMapper(self): + from pyramid.interfaces import IRoutesMapper + from zope.interface.verify import verifyObject + verifyObject(IRoutesMapper, self._makeOne()) + + def test_no_route_matches(self): + mapper = self._makeOne() + request = self._getRequest(PATH_INFO='/') + result = mapper(request) + self.assertEqual(result['match'], None) + self.assertEqual(result['route'], None) + + def test_connect_name_exists_removes_old(self): + mapper = self._makeOne() + mapper.connect('foo', 'archives/:action/:article') + mapper.connect('foo', 'archives/:action/:article2') + self.assertEqual(len(mapper.routelist), 1) + self.assertEqual(len(mapper.routes), 1) + self.assertEqual(mapper.routes['foo'].pattern, + 'archives/:action/:article2') + self.assertEqual(mapper.routelist[0].pattern, + 'archives/:action/:article2') + + def test_connect_static(self): + mapper = self._makeOne() + mapper.connect('foo', 'archives/:action/:article', static=True) + self.assertEqual(len(mapper.routelist), 0) + self.assertEqual(len(mapper.routes), 1) + self.assertEqual(mapper.routes['foo'].pattern, + 'archives/:action/:article') + + def test_connect_static_overridden(self): + mapper = self._makeOne() + mapper.connect('foo', 'archives/:action/:article', static=True) + self.assertEqual(len(mapper.routelist), 0) + self.assertEqual(len(mapper.routes), 1) + self.assertEqual(mapper.routes['foo'].pattern, + 'archives/:action/:article') + mapper.connect('foo', 'archives/:action/:article2') + self.assertEqual(len(mapper.routelist), 1) + self.assertEqual(len(mapper.routes), 1) + self.assertEqual(mapper.routes['foo'].pattern, + 'archives/:action/:article2') + self.assertEqual(mapper.routelist[0].pattern, + 'archives/:action/:article2') + + def test___call__pathinfo_cant_be_decoded(self): + from pyramid.exceptions import URLDecodeError + mapper = self._makeOne() + if PY2: + path_info = b'\xff\xfe\xe6\x00' + else: + path_info = b'\xff\xfe\xe6\x00'.decode('latin-1') + request = self._getRequest(PATH_INFO=path_info) + self.assertRaises(URLDecodeError, mapper, request) + + def test___call__route_matches(self): + mapper = self._makeOne() + mapper.connect('foo', 'archives/:action/:article') + request = self._getRequest(PATH_INFO='/archives/action1/article1') + result = mapper(request) + self.assertEqual(result['route'], mapper.routes['foo']) + self.assertEqual(result['match']['action'], 'action1') + self.assertEqual(result['match']['article'], 'article1') + + def test___call__route_matches_with_predicates(self): + mapper = self._makeOne() + mapper.connect('foo', 'archives/:action/:article', + predicates=[lambda *arg: True]) + request = self._getRequest(PATH_INFO='/archives/action1/article1') + result = mapper(request) + self.assertEqual(result['route'], mapper.routes['foo']) + self.assertEqual(result['match']['action'], 'action1') + self.assertEqual(result['match']['article'], 'article1') + + def test___call__route_fails_to_match_with_predicates(self): + mapper = self._makeOne() + mapper.connect('foo', 'archives/:action/article1', + predicates=[lambda *arg: True, lambda *arg: False]) + mapper.connect('bar', 'archives/:action/:article') + request = self._getRequest(PATH_INFO='/archives/action1/article1') + result = mapper(request) + self.assertEqual(result['route'], mapper.routes['bar']) + self.assertEqual(result['match']['action'], 'action1') + self.assertEqual(result['match']['article'], 'article1') + + def test___call__custom_predicate_gets_info(self): + mapper = self._makeOne() + def pred(info, request): + self.assertEqual(info['match'], {'action':'action1'}) + self.assertEqual(info['route'], mapper.routes['foo']) + return True + mapper.connect('foo', 'archives/:action/article1', predicates=[pred]) + request = self._getRequest(PATH_INFO='/archives/action1/article1') + mapper(request) + + def test_cc_bug(self): + # "unordered" as reported in IRC by author of + # http://labs.creativecommons.org/2010/01/13/cc-engine-and-web-non-frameworks/ + mapper = self._makeOne() + mapper.connect('rdf', 'licenses/:license_code/:license_version/rdf') + mapper.connect('juri', + 'licenses/:license_code/:license_version/:jurisdiction') + + request = self._getRequest(PATH_INFO='/licenses/1/v2/rdf') + result = mapper(request) + self.assertEqual(result['route'], mapper.routes['rdf']) + self.assertEqual(result['match']['license_code'], '1') + self.assertEqual(result['match']['license_version'], 'v2') + + request = self._getRequest(PATH_INFO='/licenses/1/v2/usa') + result = mapper(request) + self.assertEqual(result['route'], mapper.routes['juri']) + self.assertEqual(result['match']['license_code'], '1') + self.assertEqual(result['match']['license_version'], 'v2') + self.assertEqual(result['match']['jurisdiction'], 'usa') + + def test___call__root_route_matches(self): + mapper = self._makeOne() + mapper.connect('root', '') + request = self._getRequest(PATH_INFO='/') + result = mapper(request) + self.assertEqual(result['route'], mapper.routes['root']) + self.assertEqual(result['match'], {}) + + def test___call__root_route_matches2(self): + mapper = self._makeOne() + mapper.connect('root', '/') + request = self._getRequest(PATH_INFO='/') + result = mapper(request) + self.assertEqual(result['route'], mapper.routes['root']) + self.assertEqual(result['match'], {}) + + def test___call__root_route_when_path_info_empty(self): + mapper = self._makeOne() + mapper.connect('root', '/') + request = self._getRequest(PATH_INFO='') + result = mapper(request) + self.assertEqual(result['route'], mapper.routes['root']) + self.assertEqual(result['match'], {}) + + def test___call__root_route_when_path_info_notempty(self): + mapper = self._makeOne() + mapper.connect('root', '/') + request = self._getRequest(PATH_INFO='/') + result = mapper(request) + self.assertEqual(result['route'], mapper.routes['root']) + self.assertEqual(result['match'], {}) + + def test___call__no_path_info(self): + mapper = self._makeOne() + mapper.connect('root', '/') + request = self._getRequest() + result = mapper(request) + self.assertEqual(result['route'], mapper.routes['root']) + self.assertEqual(result['match'], {}) + + def test_has_routes(self): + mapper = self._makeOne() + self.assertEqual(mapper.has_routes(), False) + mapper.connect('whatever', 'archives/:action/:article') + self.assertEqual(mapper.has_routes(), True) + + def test_get_routes(self): + from pyramid.urldispatch import Route + mapper = self._makeOne() + self.assertEqual(mapper.get_routes(), []) + mapper.connect('whatever', 'archives/:action/:article') + routes = mapper.get_routes() + self.assertEqual(len(routes), 1) + self.assertEqual(routes[0].__class__, Route) + + def test_get_route_matches(self): + mapper = self._makeOne() + mapper.connect('whatever', 'archives/:action/:article') + result = mapper.get_route('whatever') + self.assertEqual(result.pattern, 'archives/:action/:article') + + def test_get_route_misses(self): + mapper = self._makeOne() + result = mapper.get_route('whatever') + self.assertEqual(result, None) + + def test_generate(self): + mapper = self._makeOne() + def generator(kw): + return 123 + route = DummyRoute(generator) + mapper.routes['abc'] = route + self.assertEqual(mapper.generate('abc', {}), 123) + +class TestCompileRoute(unittest.TestCase): + def _callFUT(self, pattern): + from pyramid.urldispatch import _compile_route + return _compile_route(pattern) + + def test_no_star(self): + matcher, generator = self._callFUT('/foo/:baz/biz/:buz/bar') + self.assertEqual(matcher('/foo/baz/biz/buz/bar'), + {'baz':'baz', 'buz':'buz'}) + self.assertEqual(matcher('foo/baz/biz/buz/bar'), None) + self.assertEqual(generator({'baz':1, 'buz':2}), '/foo/1/biz/2/bar') + + def test_with_star(self): + matcher, generator = self._callFUT('/foo/:baz/biz/:buz/bar*traverse') + self.assertEqual(matcher('/foo/baz/biz/buz/bar'), + {'baz':'baz', 'buz':'buz', 'traverse':()}) + self.assertEqual(matcher('/foo/baz/biz/buz/bar/everything/else/here'), + {'baz':'baz', 'buz':'buz', + 'traverse':('everything', 'else', 'here')}) + self.assertEqual(matcher('foo/baz/biz/buz/bar'), None) + self.assertEqual(generator( + {'baz':1, 'buz':2, 'traverse':'/a/b'}), '/foo/1/biz/2/bar/a/b') + + def test_with_bracket_star(self): + matcher, generator = self._callFUT( + '/foo/{baz}/biz/{buz}/bar{remainder:.*}') + self.assertEqual(matcher('/foo/baz/biz/buz/bar'), + {'baz':'baz', 'buz':'buz', 'remainder':''}) + self.assertEqual(matcher('/foo/baz/biz/buz/bar/everything/else/here'), + {'baz':'baz', 'buz':'buz', + 'remainder':'/everything/else/here'}) + self.assertEqual(matcher('foo/baz/biz/buz/bar'), None) + self.assertEqual(generator( + {'baz':1, 'buz':2, 'remainder':'/a/b'}), '/foo/1/biz/2/bar/a/b') + + def test_no_beginning_slash(self): + matcher, generator = self._callFUT('foo/:baz/biz/:buz/bar') + self.assertEqual(matcher('/foo/baz/biz/buz/bar'), + {'baz':'baz', 'buz':'buz'}) + self.assertEqual(matcher('foo/baz/biz/buz/bar'), None) + self.assertEqual(generator({'baz':1, 'buz':2}), '/foo/1/biz/2/bar') + + def test_custom_regex(self): + matcher, generator = self._callFUT('foo/{baz}/biz/{buz:[^/\.]+}.{bar}') + self.assertEqual(matcher('/foo/baz/biz/buz.bar'), + {'baz':'baz', 'buz':'buz', 'bar':'bar'}) + self.assertEqual(matcher('foo/baz/biz/buz/bar'), None) + self.assertEqual(generator({'baz':1, 'buz':2, 'bar': 'html'}), + '/foo/1/biz/2.html') + + def test_custom_regex_with_colons(self): + matcher, generator = self._callFUT('foo/{baz}/biz/{buz:(?:[^/\.]+)}.{bar}') + self.assertEqual(matcher('/foo/baz/biz/buz.bar'), + {'baz':'baz', 'buz':'buz', 'bar':'bar'}) + self.assertEqual(matcher('foo/baz/biz/buz/bar'), None) + self.assertEqual(generator({'baz':1, 'buz':2, 'bar': 'html'}), + '/foo/1/biz/2.html') + + def test_mixed_newstyle_oldstyle_pattern_defaults_to_newstyle(self): + # pattern: '\\/foo\\/(?Pabc)\\/biz\\/(?P[^/]+)\\/bar$' + # note presence of :abc in pattern (oldstyle match) + matcher, generator = self._callFUT('foo/{baz:abc}/biz/{buz}/bar') + self.assertEqual(matcher('/foo/abc/biz/buz/bar'), + {'baz':'abc', 'buz':'buz'}) + self.assertEqual(generator({'baz':1, 'buz':2}), '/foo/1/biz/2/bar') + + def test_custom_regex_with_embedded_squigglies(self): + matcher, generator = self._callFUT('/{buz:\d{4}}') + self.assertEqual(matcher('/2001'), {'buz':'2001'}) + self.assertEqual(matcher('/200'), None) + self.assertEqual(generator({'buz':2001}), '/2001') + + def test_custom_regex_with_embedded_squigglies2(self): + matcher, generator = self._callFUT('/{buz:\d{3,4}}') + self.assertEqual(matcher('/2001'), {'buz':'2001'}) + self.assertEqual(matcher('/200'), {'buz':'200'}) + self.assertEqual(matcher('/20'), None) + self.assertEqual(generator({'buz':2001}), '/2001') + + def test_custom_regex_with_embedded_squigglies3(self): + matcher, generator = self._callFUT( + '/{buz:(\d{2}|\d{4})-[a-zA-Z]{3,4}-\d{2}}') + self.assertEqual(matcher('/2001-Nov-15'), {'buz':'2001-Nov-15'}) + self.assertEqual(matcher('/99-June-10'), {'buz':'99-June-10'}) + self.assertEqual(matcher('/2-Nov-15'), None) + self.assertEqual(matcher('/200-Nov-15'), None) + self.assertEqual(matcher('/2001-No-15'), None) + self.assertEqual(generator({'buz':'2001-Nov-15'}), '/2001-Nov-15') + self.assertEqual(generator({'buz':'99-June-10'}), '/99-June-10') + + def test_pattern_with_high_order_literal(self): + pattern = text_(b'/La Pe\xc3\xb1a/{x}', 'utf-8') + matcher, generator = self._callFUT(pattern) + self.assertEqual(matcher(text_(b'/La Pe\xc3\xb1a/x', 'utf-8')), + {'x':'x'}) + self.assertEqual(generator({'x':'1'}), '/La%20Pe%C3%B1a/1') + + def test_pattern_generate_with_high_order_dynamic(self): + pattern = '/{x}' + _, generator = self._callFUT(pattern) + self.assertEqual( + generator({'x':text_(b'La Pe\xc3\xb1a', 'utf-8')}), + '/La%20Pe%C3%B1a') + + def test_docs_sample_generate(self): + # sample from urldispatch.rst + pattern = text_(b'/La Pe\xc3\xb1a/{city}', 'utf-8') + _, generator = self._callFUT(pattern) + self.assertEqual( + generator({'city':text_(b'Qu\xc3\xa9bec', 'utf-8')}), + '/La%20Pe%C3%B1a/Qu%C3%A9bec') + + def test_generate_with_mixedtype_values(self): + pattern = '/{city}/{state}' + _, generator = self._callFUT(pattern) + result = generator( + {'city': text_(b'Qu\xc3\xa9bec', 'utf-8'), + 'state': b'La Pe\xc3\xb1a'} + ) + self.assertEqual(result, '/Qu%C3%A9bec/La%20Pe%C3%B1a') + # should be a native string + self.assertEqual(type(result), str) + + def test_highorder_pattern_utf8(self): + pattern = b'/La Pe\xc3\xb1a/{city}' + self.assertRaises(ValueError, self._callFUT, pattern) + + def test_generate_with_string_remainder_and_unicode_replacement(self): + pattern = text_(b'/abc*remainder', 'utf-8') + _, generator = self._callFUT(pattern) + result = generator( + {'remainder': text_(b'/Qu\xc3\xa9bec/La Pe\xc3\xb1a', 'utf-8')} + ) + self.assertEqual(result, '/abc/Qu%C3%A9bec/La%20Pe%C3%B1a') + # should be a native string + self.assertEqual(type(result), str) + + def test_generate_with_string_remainder_and_nonstring_replacement(self): + pattern = text_(b'/abc/*remainder', 'utf-8') + _, generator = self._callFUT(pattern) + result = generator( + {'remainder': None} + ) + self.assertEqual(result, '/abc/None') + # should be a native string + self.assertEqual(type(result), str) + +class TestCompileRouteFunctional(unittest.TestCase): + def matches(self, pattern, path, expected): + from pyramid.urldispatch import _compile_route + matcher = _compile_route(pattern)[0] + result = matcher(path) + self.assertEqual(result, expected) + + def generates(self, pattern, dict, result): + from pyramid.urldispatch import _compile_route + self.assertEqual(_compile_route(pattern)[1](dict), result) + + def test_matcher_functional_notdynamic(self): + self.matches('/', '', None) + self.matches('', '', None) + self.matches('/', '/foo', None) + self.matches('/foo/', '/foo', None) + self.matches('', '/', {}) + self.matches('/', '/', {}) + + def test_matcher_functional_newstyle(self): + self.matches('/{x}', '', None) + self.matches('/{x}', '/', None) + self.matches('/abc/{def}', '/abc/', None) + self.matches('/{x}', '/a', {'x':'a'}) + self.matches('zzz/{x}', '/zzz/abc', {'x':'abc'}) + self.matches('zzz/{x}*traverse', '/zzz/abc', {'x':'abc', 'traverse':()}) + self.matches('zzz/{x}*traverse', '/zzz/abc/def/g', + {'x':'abc', 'traverse':('def', 'g')}) + self.matches('*traverse', '/zzz/abc', {'traverse':('zzz', 'abc')}) + self.matches('*traverse', '/zzz/ abc', {'traverse':('zzz', ' abc')}) + #'/La%20Pe%C3%B1a' + self.matches('{x}', text_(b'/La Pe\xc3\xb1a', 'utf-8'), + {'x':text_(b'La Pe\xc3\xb1a', 'utf-8')}) + # '/La%20Pe%C3%B1a/x' + self.matches('*traverse', text_(b'/La Pe\xc3\xb1a/x'), + {'traverse':(text_(b'La Pe\xc3\xb1a'), 'x')}) + self.matches('/foo/{id}.html', '/foo/bar.html', {'id':'bar'}) + self.matches('/{num:[0-9]+}/*traverse', '/555/abc/def', + {'num':'555', 'traverse':('abc', 'def')}) + self.matches('/{num:[0-9]*}/*traverse', '/555/abc/def', + {'num':'555', 'traverse':('abc', 'def')}) + self.matches('zzz/{_}', '/zzz/abc', {'_':'abc'}) + self.matches('zzz/{_abc}', '/zzz/abc', {'_abc':'abc'}) + self.matches('zzz/{abc_def}', '/zzz/abc', {'abc_def':'abc'}) + + def test_matcher_functional_oldstyle(self): + self.matches('/:x', '', None) + self.matches('/:x', '/', None) + self.matches('/abc/:def', '/abc/', None) + self.matches('/:x', '/a', {'x':'a'}) + self.matches('zzz/:x', '/zzz/abc', {'x':'abc'}) + self.matches('zzz/:x*traverse', '/zzz/abc', {'x':'abc', 'traverse':()}) + self.matches('zzz/:x*traverse', '/zzz/abc/def/g', + {'x':'abc', 'traverse':('def', 'g')}) + self.matches('*traverse', '/zzz/abc', {'traverse':('zzz', 'abc')}) + self.matches('*traverse', '/zzz/ abc', {'traverse':('zzz', ' abc')}) + #'/La%20Pe%C3%B1a' + # pattern, path, expected + self.matches(':x', text_(b'/La Pe\xc3\xb1a', 'utf-8'), + {'x':text_(b'La Pe\xc3\xb1a', 'utf-8')}) + # '/La%20Pe%C3%B1a/x' + self.matches('*traverse', text_(b'/La Pe\xc3\xb1a/x', 'utf-8'), + {'traverse':(text_(b'La Pe\xc3\xb1a', 'utf-8'), 'x')}) + self.matches('/foo/:id.html', '/foo/bar.html', {'id':'bar'}) + self.matches('/foo/:id_html', '/foo/bar_html', {'id_html':'bar_html'}) + self.matches('zzz/:_', '/zzz/abc', {'_':'abc'}) + self.matches('zzz/:_abc', '/zzz/abc', {'_abc':'abc'}) + self.matches('zzz/:abc_def', '/zzz/abc', {'abc_def':'abc'}) + + def test_generator_functional_notdynamic(self): + self.generates('', {}, '/') + self.generates('/', {}, '/') + + def test_generator_functional_newstyle(self): + self.generates('/{x}', {'x':''}, '/') + self.generates('/{x}', {'x':'a'}, '/a') + self.generates('/{x}', {'x':'a/b/c'}, '/a/b/c') + self.generates('/{x}', {'x':':@&+$,'}, '/:@&+$,') + self.generates('zzz/{x}', {'x':'abc'}, '/zzz/abc') + self.generates('zzz/{x}*traverse', {'x':'abc', 'traverse':''}, + '/zzz/abc') + self.generates('zzz/{x}*traverse', {'x':'abc', 'traverse':'/def/g'}, + '/zzz/abc/def/g') + self.generates('zzz/{x}*traverse', {'x':':@&+$,', 'traverse':'/:@&+$,'}, + '/zzz/:@&+$,/:@&+$,') + self.generates('/{x}', {'x':text_(b'/La Pe\xc3\xb1a', 'utf-8')}, + '//La%20Pe%C3%B1a') + self.generates('/{x}*y', {'x':text_(b'/La Pe\xc3\xb1a', 'utf-8'), + 'y':'/rest/of/path'}, + '//La%20Pe%C3%B1a/rest/of/path') + self.generates('*traverse', {'traverse':('a', text_(b'La Pe\xf1a'))}, + '/a/La%20Pe%C3%B1a') + self.generates('/foo/{id}.html', {'id':'bar'}, '/foo/bar.html') + self.generates('/foo/{_}', {'_':'20'}, '/foo/20') + self.generates('/foo/{_abc}', {'_abc':'20'}, '/foo/20') + self.generates('/foo/{abc_def}', {'abc_def':'20'}, '/foo/20') + + def test_generator_functional_oldstyle(self): + self.generates('/:x', {'x':''}, '/') + self.generates('/:x', {'x':'a'}, '/a') + self.generates('zzz/:x', {'x':'abc'}, '/zzz/abc') + self.generates('zzz/:x*traverse', {'x':'abc', 'traverse':''}, + '/zzz/abc') + self.generates('zzz/:x*traverse', {'x':'abc', 'traverse':'/def/g'}, + '/zzz/abc/def/g') + self.generates('/:x', {'x':text_(b'/La Pe\xc3\xb1a', 'utf-8')}, + '//La%20Pe%C3%B1a') + self.generates('/:x*y', {'x':text_(b'/La Pe\xc3\xb1a', 'utf-8'), + 'y':'/rest/of/path'}, + '//La%20Pe%C3%B1a/rest/of/path') + self.generates('*traverse', {'traverse':('a', text_(b'La Pe\xf1a'))}, + '/a/La%20Pe%C3%B1a') + self.generates('/foo/:id.html', {'id':'bar'}, '/foo/bar.html') + self.generates('/foo/:_', {'_':'20'}, '/foo/20') + self.generates('/foo/:_abc', {'_abc':'20'}, '/foo/20') + self.generates('/foo/:abc_def', {'abc_def':'20'}, '/foo/20') + +class DummyContext(object): + """ """ + +class DummyRequest(object): + def __init__(self, environ): + self.environ = environ + +class DummyRoute(object): + def __init__(self, generator): + self.generate = generator + diff --git a/src/pyramid/tests/test_util.py b/src/pyramid/tests/test_util.py new file mode 100644 index 000000000..a76cd2017 --- /dev/null +++ b/src/pyramid/tests/test_util.py @@ -0,0 +1,1111 @@ +import unittest +from pyramid.compat import ( + PY2, + text_, + bytes_, + ) + + +class Test_InstancePropertyHelper(unittest.TestCase): + def _makeOne(self): + cls = self._getTargetClass() + return cls() + + def _getTargetClass(self): + from pyramid.util import InstancePropertyHelper + return InstancePropertyHelper + + def test_callable(self): + def worker(obj): + return obj.bar + foo = Dummy() + helper = self._getTargetClass() + helper.set_property(foo, worker) + foo.bar = 1 + self.assertEqual(1, foo.worker) + foo.bar = 2 + self.assertEqual(2, foo.worker) + + def test_callable_with_name(self): + def worker(obj): + return obj.bar + foo = Dummy() + helper = self._getTargetClass() + helper.set_property(foo, worker, name='x') + foo.bar = 1 + self.assertEqual(1, foo.x) + foo.bar = 2 + self.assertEqual(2, foo.x) + + def test_callable_with_reify(self): + def worker(obj): + return obj.bar + foo = Dummy() + helper = self._getTargetClass() + helper.set_property(foo, worker, reify=True) + foo.bar = 1 + self.assertEqual(1, foo.worker) + foo.bar = 2 + self.assertEqual(1, foo.worker) + + def test_callable_with_name_reify(self): + def worker(obj): + return obj.bar + foo = Dummy() + helper = self._getTargetClass() + helper.set_property(foo, worker, name='x') + helper.set_property(foo, worker, name='y', reify=True) + foo.bar = 1 + self.assertEqual(1, foo.y) + self.assertEqual(1, foo.x) + foo.bar = 2 + self.assertEqual(2, foo.x) + self.assertEqual(1, foo.y) + + def test_property_without_name(self): + def worker(obj): pass + foo = Dummy() + helper = self._getTargetClass() + self.assertRaises(ValueError, helper.set_property, foo, property(worker)) + + def test_property_with_name(self): + def worker(obj): + return obj.bar + foo = Dummy() + helper = self._getTargetClass() + helper.set_property(foo, property(worker), name='x') + foo.bar = 1 + self.assertEqual(1, foo.x) + foo.bar = 2 + self.assertEqual(2, foo.x) + + def test_property_with_reify(self): + def worker(obj): pass + foo = Dummy() + helper = self._getTargetClass() + self.assertRaises(ValueError, helper.set_property, + foo, property(worker), name='x', reify=True) + + def test_override_property(self): + def worker(obj): pass + foo = Dummy() + helper = self._getTargetClass() + helper.set_property(foo, worker, name='x') + def doit(): + foo.x = 1 + self.assertRaises(AttributeError, doit) + + def test_override_reify(self): + def worker(obj): pass + foo = Dummy() + helper = self._getTargetClass() + helper.set_property(foo, worker, name='x', reify=True) + foo.x = 1 + self.assertEqual(1, foo.x) + foo.x = 2 + self.assertEqual(2, foo.x) + + def test_reset_property(self): + foo = Dummy() + helper = self._getTargetClass() + helper.set_property(foo, lambda _: 1, name='x') + self.assertEqual(1, foo.x) + helper.set_property(foo, lambda _: 2, name='x') + self.assertEqual(2, foo.x) + + def test_reset_reify(self): + """ This is questionable behavior, but may as well get notified + if it changes.""" + foo = Dummy() + helper = self._getTargetClass() + helper.set_property(foo, lambda _: 1, name='x', reify=True) + self.assertEqual(1, foo.x) + helper.set_property(foo, lambda _: 2, name='x', reify=True) + self.assertEqual(1, foo.x) + + def test_make_property(self): + from pyramid.decorator import reify + helper = self._getTargetClass() + name, fn = helper.make_property(lambda x: 1, name='x', reify=True) + self.assertEqual(name, 'x') + self.assertTrue(isinstance(fn, reify)) + + def test_apply_properties_with_iterable(self): + foo = Dummy() + helper = self._getTargetClass() + x = helper.make_property(lambda _: 1, name='x', reify=True) + y = helper.make_property(lambda _: 2, name='y') + helper.apply_properties(foo, [x, y]) + self.assertEqual(1, foo.x) + self.assertEqual(2, foo.y) + + def test_apply_properties_with_dict(self): + foo = Dummy() + helper = self._getTargetClass() + x_name, x_fn = helper.make_property(lambda _: 1, name='x', reify=True) + y_name, y_fn = helper.make_property(lambda _: 2, name='y') + helper.apply_properties(foo, {x_name: x_fn, y_name: y_fn}) + self.assertEqual(1, foo.x) + self.assertEqual(2, foo.y) + + def test_make_property_unicode(self): + from pyramid.compat import text_ + from pyramid.exceptions import ConfigurationError + + cls = self._getTargetClass() + if PY2: + name = text_(b'La Pe\xc3\xb1a', 'utf-8') + else: + name = b'La Pe\xc3\xb1a' + + def make_bad_name(): + cls.make_property(lambda x: 1, name=name, reify=True) + + self.assertRaises(ConfigurationError, make_bad_name) + + def test_add_property(self): + helper = self._makeOne() + helper.add_property(lambda obj: obj.bar, name='x', reify=True) + helper.add_property(lambda obj: obj.bar, name='y') + self.assertEqual(len(helper.properties), 2) + foo = Dummy() + helper.apply(foo) + foo.bar = 1 + self.assertEqual(foo.x, 1) + self.assertEqual(foo.y, 1) + foo.bar = 2 + self.assertEqual(foo.x, 1) + self.assertEqual(foo.y, 2) + + def test_apply_multiple_times(self): + helper = self._makeOne() + helper.add_property(lambda obj: 1, name='x') + foo, bar = Dummy(), Dummy() + helper.apply(foo) + self.assertEqual(foo.x, 1) + helper.add_property(lambda obj: 2, name='x') + helper.apply(bar) + self.assertEqual(foo.x, 1) + self.assertEqual(bar.x, 2) + +class Test_InstancePropertyMixin(unittest.TestCase): + def _makeOne(self): + cls = self._getTargetClass() + + class Foo(cls): + pass + return Foo() + + def _getTargetClass(self): + from pyramid.util import InstancePropertyMixin + return InstancePropertyMixin + + def test_callable(self): + def worker(obj): + return obj.bar + foo = self._makeOne() + foo.set_property(worker) + foo.bar = 1 + self.assertEqual(1, foo.worker) + foo.bar = 2 + self.assertEqual(2, foo.worker) + + def test_callable_with_name(self): + def worker(obj): + return obj.bar + foo = self._makeOne() + foo.set_property(worker, name='x') + foo.bar = 1 + self.assertEqual(1, foo.x) + foo.bar = 2 + self.assertEqual(2, foo.x) + + def test_callable_with_reify(self): + def worker(obj): + return obj.bar + foo = self._makeOne() + foo.set_property(worker, reify=True) + foo.bar = 1 + self.assertEqual(1, foo.worker) + foo.bar = 2 + self.assertEqual(1, foo.worker) + + def test_callable_with_name_reify(self): + def worker(obj): + return obj.bar + foo = self._makeOne() + foo.set_property(worker, name='x') + foo.set_property(worker, name='y', reify=True) + foo.bar = 1 + self.assertEqual(1, foo.y) + self.assertEqual(1, foo.x) + foo.bar = 2 + self.assertEqual(2, foo.x) + self.assertEqual(1, foo.y) + + def test_property_without_name(self): + def worker(obj): pass + foo = self._makeOne() + self.assertRaises(ValueError, foo.set_property, property(worker)) + + def test_property_with_name(self): + def worker(obj): + return obj.bar + foo = self._makeOne() + foo.set_property(property(worker), name='x') + foo.bar = 1 + self.assertEqual(1, foo.x) + foo.bar = 2 + self.assertEqual(2, foo.x) + + def test_property_with_reify(self): + def worker(obj): pass + foo = self._makeOne() + self.assertRaises(ValueError, foo.set_property, + property(worker), name='x', reify=True) + + def test_override_property(self): + def worker(obj): pass + foo = self._makeOne() + foo.set_property(worker, name='x') + def doit(): + foo.x = 1 + self.assertRaises(AttributeError, doit) + + def test_override_reify(self): + def worker(obj): pass + foo = self._makeOne() + foo.set_property(worker, name='x', reify=True) + foo.x = 1 + self.assertEqual(1, foo.x) + foo.x = 2 + self.assertEqual(2, foo.x) + + def test_reset_property(self): + foo = self._makeOne() + foo.set_property(lambda _: 1, name='x') + self.assertEqual(1, foo.x) + foo.set_property(lambda _: 2, name='x') + self.assertEqual(2, foo.x) + + def test_reset_reify(self): + """ This is questionable behavior, but may as well get notified + if it changes.""" + foo = self._makeOne() + foo.set_property(lambda _: 1, name='x', reify=True) + self.assertEqual(1, foo.x) + foo.set_property(lambda _: 2, name='x', reify=True) + self.assertEqual(1, foo.x) + + def test_new_class_keeps_parent_module_name(self): + foo = self._makeOne() + self.assertEqual(foo.__module__, 'pyramid.tests.test_util') + self.assertEqual(foo.__class__.__module__, 'pyramid.tests.test_util') + foo.set_property(lambda _: 1, name='x', reify=True) + self.assertEqual(foo.__module__, 'pyramid.tests.test_util') + self.assertEqual(foo.__class__.__module__, 'pyramid.tests.test_util') + +class Test_WeakOrderedSet(unittest.TestCase): + def _makeOne(self): + from pyramid.config import WeakOrderedSet + return WeakOrderedSet() + + def test_ctor(self): + wos = self._makeOne() + self.assertEqual(len(wos), 0) + self.assertEqual(wos.last, None) + + def test_add_item(self): + wos = self._makeOne() + reg = Dummy() + wos.add(reg) + self.assertEqual(list(wos), [reg]) + self.assertTrue(reg in wos) + self.assertEqual(wos.last, reg) + + def test_add_multiple_items(self): + wos = self._makeOne() + reg1 = Dummy() + reg2 = Dummy() + wos.add(reg1) + wos.add(reg2) + self.assertEqual(len(wos), 2) + self.assertEqual(list(wos), [reg1, reg2]) + self.assertTrue(reg1 in wos) + self.assertTrue(reg2 in wos) + self.assertEqual(wos.last, reg2) + + def test_add_duplicate_items(self): + wos = self._makeOne() + reg = Dummy() + wos.add(reg) + wos.add(reg) + self.assertEqual(len(wos), 1) + self.assertEqual(list(wos), [reg]) + self.assertTrue(reg in wos) + self.assertEqual(wos.last, reg) + + def test_weakref_removal(self): + wos = self._makeOne() + reg = Dummy() + wos.add(reg) + wos.remove(reg) + self.assertEqual(len(wos), 0) + self.assertEqual(list(wos), []) + self.assertEqual(wos.last, None) + + def test_last_updated(self): + wos = self._makeOne() + reg = Dummy() + reg2 = Dummy() + wos.add(reg) + wos.add(reg2) + wos.remove(reg2) + self.assertEqual(len(wos), 1) + self.assertEqual(list(wos), [reg]) + self.assertEqual(wos.last, reg) + + def test_empty(self): + wos = self._makeOne() + reg = Dummy() + reg2 = Dummy() + wos.add(reg) + wos.add(reg2) + wos.empty() + self.assertEqual(len(wos), 0) + self.assertEqual(list(wos), []) + self.assertEqual(wos.last, None) + +class Test_strings_differ(unittest.TestCase): + def _callFUT(self, *args, **kw): + from pyramid.util import strings_differ + return strings_differ(*args, **kw) + + def test_it_bytes(self): + self.assertFalse(self._callFUT(b'foo', b'foo')) + self.assertTrue(self._callFUT(b'123', b'345')) + self.assertTrue(self._callFUT(b'1234', b'123')) + self.assertTrue(self._callFUT(b'123', b'1234')) + + def test_it_native_str(self): + self.assertFalse(self._callFUT('123', '123')) + self.assertTrue(self._callFUT('123', '1234')) + + def test_it_with_internal_comparator(self): + result = self._callFUT(b'foo', b'foo', compare_digest=None) + self.assertFalse(result) + + result = self._callFUT(b'123', b'abc', compare_digest=None) + self.assertTrue(result) + + def test_it_with_external_comparator(self): + class DummyComparator(object): + called = False + def __init__(self, ret_val): + self.ret_val = ret_val + + def __call__(self, a, b): + self.called = True + return self.ret_val + + dummy_compare = DummyComparator(True) + result = self._callFUT(b'foo', b'foo', compare_digest=dummy_compare) + self.assertTrue(dummy_compare.called) + self.assertFalse(result) + + dummy_compare = DummyComparator(False) + result = self._callFUT(b'123', b'345', compare_digest=dummy_compare) + self.assertTrue(dummy_compare.called) + self.assertTrue(result) + + dummy_compare = DummyComparator(False) + result = self._callFUT(b'abc', b'abc', compare_digest=dummy_compare) + self.assertTrue(dummy_compare.called) + self.assertTrue(result) + +class Test_object_description(unittest.TestCase): + def _callFUT(self, object): + from pyramid.util import object_description + return object_description(object) + + def test_string(self): + self.assertEqual(self._callFUT('abc'), 'abc') + + def test_int(self): + self.assertEqual(self._callFUT(1), '1') + + def test_bool(self): + self.assertEqual(self._callFUT(True), 'True') + + def test_None(self): + self.assertEqual(self._callFUT(None), 'None') + + def test_float(self): + self.assertEqual(self._callFUT(1.2), '1.2') + + def test_tuple(self): + self.assertEqual(self._callFUT(('a', 'b')), "('a', 'b')") + + def test_set(self): + if PY2: + self.assertEqual(self._callFUT(set(['a'])), "set(['a'])") + else: + self.assertEqual(self._callFUT(set(['a'])), "{'a'}") + + def test_list(self): + self.assertEqual(self._callFUT(['a']), "['a']") + + def test_dict(self): + self.assertEqual(self._callFUT({'a':1}), "{'a': 1}") + + def test_nomodule(self): + o = object() + self.assertEqual(self._callFUT(o), 'object %s' % str(o)) + + def test_module(self): + import pyramid + self.assertEqual(self._callFUT(pyramid), 'module pyramid') + + def test_method(self): + self.assertEqual( + self._callFUT(self.test_method), + 'method test_method of class pyramid.tests.test_util.' + 'Test_object_description') + + def test_class(self): + self.assertEqual( + self._callFUT(self.__class__), + 'class pyramid.tests.test_util.Test_object_description') + + def test_function(self): + self.assertEqual( + self._callFUT(dummyfunc), + 'function pyramid.tests.test_util.dummyfunc') + + def test_instance(self): + inst = Dummy() + self.assertEqual( + self._callFUT(inst), + "object %s" % str(inst)) + + def test_shortened_repr(self): + inst = ['1'] * 1000 + self.assertEqual( + self._callFUT(inst), + str(inst)[:100] + ' ... ]') + +class TestTopologicalSorter(unittest.TestCase): + def _makeOne(self, *arg, **kw): + from pyramid.util import TopologicalSorter + return TopologicalSorter(*arg, **kw) + + def test_remove(self): + inst = self._makeOne() + inst.names.append('name') + inst.name2val['name'] = 1 + inst.req_after.add('name') + inst.req_before.add('name') + inst.name2after['name'] = ('bob',) + inst.name2before['name'] = ('fred',) + inst.order.append(('bob', 'name')) + inst.order.append(('name', 'fred')) + inst.remove('name') + self.assertFalse(inst.names) + self.assertFalse(inst.req_before) + self.assertFalse(inst.req_after) + self.assertFalse(inst.name2before) + self.assertFalse(inst.name2after) + self.assertFalse(inst.name2val) + self.assertFalse(inst.order) + + def test_add(self): + from pyramid.util import LAST + sorter = self._makeOne() + sorter.add('name', 'factory') + self.assertEqual(sorter.names, ['name']) + self.assertEqual(sorter.name2val, + {'name':'factory'}) + self.assertEqual(sorter.order, [('name', LAST)]) + sorter.add('name2', 'factory2') + self.assertEqual(sorter.names, ['name', 'name2']) + self.assertEqual(sorter.name2val, + {'name':'factory', 'name2':'factory2'}) + self.assertEqual(sorter.order, + [('name', LAST), ('name2', LAST)]) + sorter.add('name3', 'factory3', before='name2') + self.assertEqual(sorter.names, + ['name', 'name2', 'name3']) + self.assertEqual(sorter.name2val, + {'name':'factory', 'name2':'factory2', + 'name3':'factory3'}) + self.assertEqual(sorter.order, + [('name', LAST), ('name2', LAST), + ('name3', 'name2')]) + + def test_sorted_ordering_1(self): + sorter = self._makeOne() + sorter.add('name1', 'factory1') + sorter.add('name2', 'factory2') + self.assertEqual(sorter.sorted(), + [ + ('name1', 'factory1'), + ('name2', 'factory2'), + ]) + + def test_sorted_ordering_2(self): + from pyramid.util import FIRST + sorter = self._makeOne() + sorter.add('name1', 'factory1') + sorter.add('name2', 'factory2', after=FIRST) + self.assertEqual(sorter.sorted(), + [ + ('name2', 'factory2'), + ('name1', 'factory1'), + ]) + + def test_sorted_ordering_3(self): + from pyramid.util import FIRST + sorter = self._makeOne() + add = sorter.add + add('auth', 'auth_factory', after='browserid') + add('dbt', 'dbt_factory') + add('retry', 'retry_factory', before='txnmgr', after='exceptionview') + add('browserid', 'browserid_factory') + add('txnmgr', 'txnmgr_factory', after='exceptionview') + add('exceptionview', 'excview_factory', after=FIRST) + self.assertEqual(sorter.sorted(), + [ + ('exceptionview', 'excview_factory'), + ('retry', 'retry_factory'), + ('txnmgr', 'txnmgr_factory'), + ('dbt', 'dbt_factory'), + ('browserid', 'browserid_factory'), + ('auth', 'auth_factory'), + ]) + + def test_sorted_ordering_4(self): + from pyramid.util import FIRST + sorter = self._makeOne() + add = sorter.add + add('exceptionview', 'excview_factory', after=FIRST) + add('auth', 'auth_factory', after='browserid') + add('retry', 'retry_factory', before='txnmgr', after='exceptionview') + add('browserid', 'browserid_factory') + add('txnmgr', 'txnmgr_factory', after='exceptionview') + add('dbt', 'dbt_factory') + self.assertEqual(sorter.sorted(), + [ + ('exceptionview', 'excview_factory'), + ('retry', 'retry_factory'), + ('txnmgr', 'txnmgr_factory'), + ('browserid', 'browserid_factory'), + ('auth', 'auth_factory'), + ('dbt', 'dbt_factory'), + ]) + + def test_sorted_ordering_5(self): + from pyramid.util import LAST, FIRST + sorter = self._makeOne() + add = sorter.add + add('exceptionview', 'excview_factory') + add('auth', 'auth_factory', after=FIRST) + add('retry', 'retry_factory', before='txnmgr', after='exceptionview') + add('browserid', 'browserid_factory', after=FIRST) + add('txnmgr', 'txnmgr_factory', after='exceptionview', before=LAST) + add('dbt', 'dbt_factory') + self.assertEqual(sorter.sorted(), + [ + ('browserid', 'browserid_factory'), + ('auth', 'auth_factory'), + ('exceptionview', 'excview_factory'), + ('retry', 'retry_factory'), + ('txnmgr', 'txnmgr_factory'), + ('dbt', 'dbt_factory'), + ]) + + def test_sorted_ordering_missing_before_partial(self): + from pyramid.exceptions import ConfigurationError + sorter = self._makeOne() + add = sorter.add + add('dbt', 'dbt_factory') + add('auth', 'auth_factory', after='browserid') + add('retry', 'retry_factory', before='txnmgr', after='exceptionview') + add('browserid', 'browserid_factory') + self.assertRaises(ConfigurationError, sorter.sorted) + + def test_sorted_ordering_missing_after_partial(self): + from pyramid.exceptions import ConfigurationError + sorter = self._makeOne() + add = sorter.add + add('dbt', 'dbt_factory') + add('auth', 'auth_factory', after='txnmgr') + add('retry', 'retry_factory', before='dbt', after='exceptionview') + add('browserid', 'browserid_factory') + self.assertRaises(ConfigurationError, sorter.sorted) + + def test_sorted_ordering_missing_before_and_after_partials(self): + from pyramid.exceptions import ConfigurationError + sorter = self._makeOne() + add = sorter.add + add('dbt', 'dbt_factory') + add('auth', 'auth_factory', after='browserid') + add('retry', 'retry_factory', before='foo', after='txnmgr') + add('browserid', 'browserid_factory') + self.assertRaises(ConfigurationError, sorter.sorted) + + def test_sorted_ordering_missing_before_partial_with_fallback(self): + from pyramid.util import LAST + sorter = self._makeOne() + add = sorter.add + add('exceptionview', 'excview_factory', before=LAST) + add('auth', 'auth_factory', after='browserid') + add('retry', 'retry_factory', before=('txnmgr', LAST), + after='exceptionview') + add('browserid', 'browserid_factory') + add('dbt', 'dbt_factory') + self.assertEqual(sorter.sorted(), + [ + ('exceptionview', 'excview_factory'), + ('retry', 'retry_factory'), + ('browserid', 'browserid_factory'), + ('auth', 'auth_factory'), + ('dbt', 'dbt_factory'), + ]) + + def test_sorted_ordering_missing_after_partial_with_fallback(self): + from pyramid.util import FIRST + sorter = self._makeOne() + add = sorter.add + add('exceptionview', 'excview_factory', after=FIRST) + add('auth', 'auth_factory', after=('txnmgr','browserid')) + add('retry', 'retry_factory', after='exceptionview') + add('browserid', 'browserid_factory') + add('dbt', 'dbt_factory') + self.assertEqual(sorter.sorted(), + [ + ('exceptionview', 'excview_factory'), + ('retry', 'retry_factory'), + ('browserid', 'browserid_factory'), + ('auth', 'auth_factory'), + ('dbt', 'dbt_factory'), + ]) + + def test_sorted_ordering_with_partial_fallbacks(self): + from pyramid.util import LAST + sorter = self._makeOne() + add = sorter.add + add('exceptionview', 'excview_factory', before=('wontbethere', LAST)) + add('retry', 'retry_factory', after='exceptionview') + add('browserid', 'browserid_factory', before=('wont2', 'exceptionview')) + self.assertEqual(sorter.sorted(), + [ + ('browserid', 'browserid_factory'), + ('exceptionview', 'excview_factory'), + ('retry', 'retry_factory'), + ]) + + def test_sorted_ordering_with_multiple_matching_fallbacks(self): + from pyramid.util import LAST + sorter = self._makeOne() + add = sorter.add + add('exceptionview', 'excview_factory', before=LAST) + add('retry', 'retry_factory', after='exceptionview') + add('browserid', 'browserid_factory', before=('retry', 'exceptionview')) + self.assertEqual(sorter.sorted(), + [ + ('browserid', 'browserid_factory'), + ('exceptionview', 'excview_factory'), + ('retry', 'retry_factory'), + ]) + + def test_sorted_ordering_with_missing_fallbacks(self): + from pyramid.exceptions import ConfigurationError + from pyramid.util import LAST + sorter = self._makeOne() + add = sorter.add + add('exceptionview', 'excview_factory', before=LAST) + add('retry', 'retry_factory', after='exceptionview') + add('browserid', 'browserid_factory', before=('txnmgr', 'auth')) + self.assertRaises(ConfigurationError, sorter.sorted) + + def test_sorted_ordering_conflict_direct(self): + from pyramid.exceptions import CyclicDependencyError + sorter = self._makeOne() + add = sorter.add + add('browserid', 'browserid_factory') + add('auth', 'auth_factory', before='browserid', after='browserid') + self.assertRaises(CyclicDependencyError, sorter.sorted) + + def test_sorted_ordering_conflict_indirect(self): + from pyramid.exceptions import CyclicDependencyError + sorter = self._makeOne() + add = sorter.add + add('browserid', 'browserid_factory') + add('auth', 'auth_factory', before='browserid') + add('dbt', 'dbt_factory', after='browserid', before='auth') + self.assertRaises(CyclicDependencyError, sorter.sorted) + +class TestSentinel(unittest.TestCase): + def test_repr(self): + from pyramid.util import Sentinel + r = repr(Sentinel('ABC')) + self.assertEqual(r, 'ABC') + + +class TestCallableName(unittest.TestCase): + def test_valid_ascii(self): + from pyramid.util import get_callable_name + from pyramid.compat import text_ + + if PY2: + name = text_(b'hello world', 'utf-8') + else: + name = b'hello world' + + self.assertEqual(get_callable_name(name), 'hello world') + + def test_invalid_ascii(self): + from pyramid.util import get_callable_name + from pyramid.compat import text_ + from pyramid.exceptions import ConfigurationError + + def get_bad_name(): + if PY2: + name = text_(b'La Pe\xc3\xb1a', 'utf-8') + else: + name = b'La Pe\xc3\xb1a' + + get_callable_name(name) + + self.assertRaises(ConfigurationError, get_bad_name) + + +class Test_hide_attrs(unittest.TestCase): + def _callFUT(self, obj, *attrs): + from pyramid.util import hide_attrs + return hide_attrs(obj, *attrs) + + def _makeDummy(self): + from pyramid.decorator import reify + class Dummy(object): + x = 1 + + @reify + def foo(self): + return self.x + return Dummy() + + def test_restores_attrs(self): + obj = self._makeDummy() + obj.bar = 'asdf' + orig_foo = obj.foo + with self._callFUT(obj, 'foo', 'bar'): + obj.foo = object() + obj.bar = 'nope' + self.assertEqual(obj.foo, orig_foo) + self.assertEqual(obj.bar, 'asdf') + + def test_restores_attrs_on_exception(self): + obj = self._makeDummy() + orig_foo = obj.foo + try: + with self._callFUT(obj, 'foo'): + obj.foo = object() + raise RuntimeError() + except RuntimeError: + self.assertEqual(obj.foo, orig_foo) + else: # pragma: no cover + self.fail("RuntimeError not raised") + + def test_restores_attrs_to_none(self): + obj = self._makeDummy() + obj.foo = None + with self._callFUT(obj, 'foo'): + obj.foo = object() + self.assertEqual(obj.foo, None) + + def test_deletes_attrs(self): + obj = self._makeDummy() + with self._callFUT(obj, 'foo'): + obj.foo = object() + self.assertTrue('foo' not in obj.__dict__) + + def test_does_not_delete_attr_if_no_attr_to_delete(self): + obj = self._makeDummy() + with self._callFUT(obj, 'foo'): + pass + self.assertTrue('foo' not in obj.__dict__) + + +def dummyfunc(): pass + + +class Dummy(object): + pass + + +class Test_is_same_domain(unittest.TestCase): + def _callFUT(self, *args, **kw): + from pyramid.util import is_same_domain + return is_same_domain(*args, **kw) + + def test_it(self): + self.assertTrue(self._callFUT("example.com", "example.com")) + self.assertFalse(self._callFUT("evil.com", "example.com")) + self.assertFalse(self._callFUT("evil.example.com", "example.com")) + self.assertFalse(self._callFUT("example.com", "")) + + def test_with_wildcard(self): + self.assertTrue(self._callFUT("example.com", ".example.com")) + self.assertTrue(self._callFUT("good.example.com", ".example.com")) + + def test_with_port(self): + self.assertTrue(self._callFUT("example.com:8080", "example.com:8080")) + self.assertFalse(self._callFUT("example.com:8080", "example.com")) + self.assertFalse(self._callFUT("example.com", "example.com:8080")) + + +class Test_make_contextmanager(unittest.TestCase): + def _callFUT(self, *args, **kw): + from pyramid.util import make_contextmanager + return make_contextmanager(*args, **kw) + + def test_with_None(self): + mgr = self._callFUT(None) + with mgr() as ctx: + self.assertIsNone(ctx) + + def test_with_generator(self): + def mygen(ctx): + yield ctx + mgr = self._callFUT(mygen) + with mgr('a') as ctx: + self.assertEqual(ctx, 'a') + + def test_with_multiple_yield_generator(self): + def mygen(): + yield 'a' + yield 'b' + mgr = self._callFUT(mygen) + try: + with mgr() as ctx: + self.assertEqual(ctx, 'a') + except RuntimeError: + pass + else: # pragma: no cover + raise AssertionError('expected raise from multiple yields') + + def test_with_regular_fn(self): + def mygen(): + return 'a' + mgr = self._callFUT(mygen) + with mgr() as ctx: + self.assertEqual(ctx, 'a') + + +class Test_takes_one_arg(unittest.TestCase): + def _callFUT(self, view, attr=None, argname=None): + from pyramid.util import takes_one_arg + return takes_one_arg(view, attr=attr, argname=argname) + + def test_requestonly_newstyle_class_no_init(self): + class foo(object): + """ """ + self.assertFalse(self._callFUT(foo)) + + def test_requestonly_newstyle_class_init_toomanyargs(self): + class foo(object): + def __init__(self, context, request): + """ """ + self.assertFalse(self._callFUT(foo)) + + def test_requestonly_newstyle_class_init_onearg_named_request(self): + class foo(object): + def __init__(self, request): + """ """ + self.assertTrue(self._callFUT(foo)) + + def test_newstyle_class_init_onearg_named_somethingelse(self): + class foo(object): + def __init__(self, req): + """ """ + self.assertTrue(self._callFUT(foo)) + + def test_newstyle_class_init_defaultargs_firstname_not_request(self): + class foo(object): + def __init__(self, context, request=None): + """ """ + self.assertFalse(self._callFUT(foo)) + + def test_newstyle_class_init_defaultargs_firstname_request(self): + class foo(object): + def __init__(self, request, foo=1, bar=2): + """ """ + self.assertTrue(self._callFUT(foo, argname='request')) + + def test_newstyle_class_init_firstname_request_with_secondname(self): + class foo(object): + def __init__(self, request, two): + """ """ + self.assertFalse(self._callFUT(foo)) + + def test_newstyle_class_init_noargs(self): + class foo(object): + def __init__(): + """ """ + self.assertFalse(self._callFUT(foo)) + + def test_oldstyle_class_no_init(self): + class foo: + """ """ + self.assertFalse(self._callFUT(foo)) + + def test_oldstyle_class_init_toomanyargs(self): + class foo: + def __init__(self, context, request): + """ """ + self.assertFalse(self._callFUT(foo)) + + def test_oldstyle_class_init_onearg_named_request(self): + class foo: + def __init__(self, request): + """ """ + self.assertTrue(self._callFUT(foo)) + + def test_oldstyle_class_init_onearg_named_somethingelse(self): + class foo: + def __init__(self, req): + """ """ + self.assertTrue(self._callFUT(foo)) + + def test_oldstyle_class_init_defaultargs_firstname_not_request(self): + class foo: + def __init__(self, context, request=None): + """ """ + self.assertFalse(self._callFUT(foo)) + + def test_oldstyle_class_init_defaultargs_firstname_request(self): + class foo: + def __init__(self, request, foo=1, bar=2): + """ """ + self.assertTrue(self._callFUT(foo, argname='request'), True) + + def test_oldstyle_class_init_noargs(self): + class foo: + def __init__(): + """ """ + self.assertFalse(self._callFUT(foo)) + + def test_function_toomanyargs(self): + def foo(context, request): + """ """ + self.assertFalse(self._callFUT(foo)) + + def test_function_with_attr_false(self): + def bar(context, request): + """ """ + def foo(context, request): + """ """ + foo.bar = bar + self.assertFalse(self._callFUT(foo, 'bar')) + + def test_function_with_attr_true(self): + def bar(context, request): + """ """ + def foo(request): + """ """ + foo.bar = bar + self.assertTrue(self._callFUT(foo, 'bar')) + + def test_function_onearg_named_request(self): + def foo(request): + """ """ + self.assertTrue(self._callFUT(foo)) + + def test_function_onearg_named_somethingelse(self): + def foo(req): + """ """ + self.assertTrue(self._callFUT(foo)) + + def test_function_defaultargs_firstname_not_request(self): + def foo(context, request=None): + """ """ + self.assertFalse(self._callFUT(foo)) + + def test_function_defaultargs_firstname_request(self): + def foo(request, foo=1, bar=2): + """ """ + self.assertTrue(self._callFUT(foo, argname='request')) + + def test_function_noargs(self): + def foo(): + """ """ + self.assertFalse(self._callFUT(foo)) + + def test_instance_toomanyargs(self): + class Foo: + def __call__(self, context, request): + """ """ + foo = Foo() + self.assertFalse(self._callFUT(foo)) + + def test_instance_defaultargs_onearg_named_request(self): + class Foo: + def __call__(self, request): + """ """ + foo = Foo() + self.assertTrue(self._callFUT(foo)) + + def test_instance_defaultargs_onearg_named_somethingelse(self): + class Foo: + def __call__(self, req): + """ """ + foo = Foo() + self.assertTrue(self._callFUT(foo)) + + def test_instance_defaultargs_firstname_not_request(self): + class Foo: + def __call__(self, context, request=None): + """ """ + foo = Foo() + self.assertFalse(self._callFUT(foo)) + + def test_instance_defaultargs_firstname_request(self): + class Foo: + def __call__(self, request, foo=1, bar=2): + """ """ + foo = Foo() + self.assertTrue(self._callFUT(foo, argname='request'), True) + + def test_instance_nocall(self): + class Foo: pass + foo = Foo() + self.assertFalse(self._callFUT(foo)) + + def test_method_onearg_named_request(self): + class Foo: + def method(self, request): + """ """ + foo = Foo() + self.assertTrue(self._callFUT(foo.method)) + + def test_function_annotations(self): + def foo(bar): + """ """ + # avoid SyntaxErrors in python2, this if effectively nop + getattr(foo, '__annotations__', {}).update({'bar': 'baz'}) + self.assertTrue(self._callFUT(foo)) + + +class TestSimpleSerializer(unittest.TestCase): + def _makeOne(self): + from pyramid.util import SimpleSerializer + return SimpleSerializer() + + def test_loads(self): + inst = self._makeOne() + self.assertEqual(inst.loads(b'abc'), text_('abc')) + + def test_dumps(self): + inst = self._makeOne() + self.assertEqual(inst.dumps('abc'), bytes_('abc')) diff --git a/src/pyramid/tests/test_view.py b/src/pyramid/tests/test_view.py new file mode 100644 index 000000000..3344bd739 --- /dev/null +++ b/src/pyramid/tests/test_view.py @@ -0,0 +1,1071 @@ +import unittest +import sys + +from zope.interface import implementer + +from pyramid import testing + +from pyramid.interfaces import IRequest + +class BaseTest(object): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def _registerView(self, reg, app, name): + from pyramid.interfaces import IViewClassifier + for_ = (IViewClassifier, IRequest, IContext) + from pyramid.interfaces import IView + reg.registerAdapter(app, for_, IView, name) + + def _makeEnviron(self, **extras): + environ = { + 'wsgi.url_scheme':'http', + 'wsgi.version':(1,0), + 'SERVER_NAME':'localhost', + 'SERVER_PORT':'8080', + 'REQUEST_METHOD':'GET', + 'PATH_INFO':'/', + } + environ.update(extras) + return environ + + def _makeRequest(self, **environ): + from pyramid.request import Request + from pyramid.registry import Registry + environ = self._makeEnviron(**environ) + request = Request(environ) + request.registry = Registry() + return request + + def _makeContext(self): + from zope.interface import directlyProvides + context = DummyContext() + directlyProvides(context, IContext) + return context + +class Test_notfound_view_config(BaseTest, unittest.TestCase): + def _makeOne(self, **kw): + from pyramid.view import notfound_view_config + return notfound_view_config(**kw) + + def test_ctor(self): + inst = self._makeOne(attr='attr', path_info='path_info', + append_slash=True) + self.assertEqual(inst.__dict__, + {'attr':'attr', 'path_info':'path_info', + 'append_slash':True}) + + def test_it_function(self): + def view(request): pass + decorator = self._makeOne(attr='attr', renderer='renderer', + append_slash=True) + venusian = DummyVenusian() + decorator.venusian = venusian + wrapped = decorator(view) + self.assertTrue(wrapped is view) + config = call_venusian(venusian) + settings = config.settings + self.assertEqual( + settings, + [{'attr': 'attr', 'venusian': venusian, 'append_slash': True, + 'renderer': 'renderer', '_info': 'codeinfo', 'view': None}] + ) + + def test_it_class(self): + decorator = self._makeOne() + venusian = DummyVenusian() + decorator.venusian = venusian + decorator.venusian.info.scope = 'class' + class view(object): pass + wrapped = decorator(view) + self.assertTrue(wrapped is view) + config = call_venusian(venusian) + settings = config.settings + self.assertEqual(len(settings), 1) + self.assertEqual(len(settings[0]), 4) + self.assertEqual(settings[0]['venusian'], venusian) + self.assertEqual(settings[0]['view'], None) # comes from call_venusian + self.assertEqual(settings[0]['attr'], 'view') + self.assertEqual(settings[0]['_info'], 'codeinfo') + + def test_call_with_venusian_args(self): + decorator = self._makeOne(_depth=1, _category='foo') + venusian = DummyVenusian() + decorator.venusian = venusian + def foo(): pass + decorator(foo) + attachments = venusian.attachments + category = attachments[0][2] + depth = attachments[0][3] + self.assertEqual(depth, 2) + self.assertEqual(category, 'foo') + +class Test_forbidden_view_config(BaseTest, unittest.TestCase): + def _makeOne(self, **kw): + from pyramid.view import forbidden_view_config + return forbidden_view_config(**kw) + + def test_ctor(self): + inst = self._makeOne(attr='attr', path_info='path_info') + self.assertEqual(inst.__dict__, + {'attr':'attr', 'path_info':'path_info'}) + + def test_it_function(self): + def view(request): pass + decorator = self._makeOne(attr='attr', renderer='renderer') + venusian = DummyVenusian() + decorator.venusian = venusian + wrapped = decorator(view) + self.assertTrue(wrapped is view) + config = call_venusian(venusian) + settings = config.settings + self.assertEqual( + settings, + [{'attr': 'attr', 'venusian': venusian, + 'renderer': 'renderer', '_info': 'codeinfo', 'view': None}] + ) + + def test_it_class(self): + decorator = self._makeOne() + venusian = DummyVenusian() + decorator.venusian = venusian + decorator.venusian.info.scope = 'class' + class view(object): pass + wrapped = decorator(view) + self.assertTrue(wrapped is view) + config = call_venusian(venusian) + settings = config.settings + self.assertEqual(len(settings), 1) + self.assertEqual(len(settings[0]), 4) + self.assertEqual(settings[0]['venusian'], venusian) + self.assertEqual(settings[0]['view'], None) # comes from call_venusian + self.assertEqual(settings[0]['attr'], 'view') + self.assertEqual(settings[0]['_info'], 'codeinfo') + + def test_call_with_venusian_args(self): + decorator = self._makeOne(_depth=1, _category='foo') + venusian = DummyVenusian() + decorator.venusian = venusian + def foo(): pass + decorator(foo) + attachments = venusian.attachments + category = attachments[0][2] + depth = attachments[0][3] + self.assertEqual(depth, 2) + self.assertEqual(category, 'foo') + +class Test_exception_view_config(BaseTest, unittest.TestCase): + def _makeOne(self, *args, **kw): + from pyramid.view import exception_view_config + return exception_view_config(*args, **kw) + + def test_ctor(self): + inst = self._makeOne(context=Exception, path_info='path_info') + self.assertEqual(inst.__dict__, + {'context':Exception, 'path_info':'path_info'}) + + def test_ctor_positional_exception(self): + inst = self._makeOne(Exception, path_info='path_info') + self.assertEqual(inst.__dict__, + {'context':Exception, 'path_info':'path_info'}) + + def test_ctor_positional_extras(self): + from pyramid.exceptions import ConfigurationError + self.assertRaises(ConfigurationError, lambda: self._makeOne(Exception, True)) + + def test_it_function(self): + def view(request): pass + decorator = self._makeOne(context=Exception, renderer='renderer') + venusian = DummyVenusian() + decorator.venusian = venusian + wrapped = decorator(view) + self.assertTrue(wrapped is view) + config = call_venusian(venusian) + settings = config.settings + self.assertEqual( + settings, + [{'venusian': venusian, 'context': Exception, + 'renderer': 'renderer', '_info': 'codeinfo', 'view': None}] + ) + + def test_it_class(self): + decorator = self._makeOne() + venusian = DummyVenusian() + decorator.venusian = venusian + decorator.venusian.info.scope = 'class' + class view(object): pass + wrapped = decorator(view) + self.assertTrue(wrapped is view) + config = call_venusian(venusian) + settings = config.settings + self.assertEqual(len(settings), 1) + self.assertEqual(len(settings[0]), 4) + self.assertEqual(settings[0]['venusian'], venusian) + self.assertEqual(settings[0]['view'], None) # comes from call_venusian + self.assertEqual(settings[0]['attr'], 'view') + self.assertEqual(settings[0]['_info'], 'codeinfo') + + def test_call_with_venusian_args(self): + decorator = self._makeOne(_depth=1, _category='foo') + venusian = DummyVenusian() + decorator.venusian = venusian + def foo(): pass + decorator(foo) + attachments = venusian.attachments + category = attachments[0][2] + depth = attachments[0][3] + self.assertEqual(depth, 2) + self.assertEqual(category, 'foo') + +class RenderViewToResponseTests(BaseTest, unittest.TestCase): + def _callFUT(self, *arg, **kw): + from pyramid.view import render_view_to_response + return render_view_to_response(*arg, **kw) + + def test_call_no_view_registered(self): + request = self._makeRequest() + context = self._makeContext() + result = self._callFUT(context, request, name='notregistered') + self.assertEqual(result, None) + + def test_call_no_registry_on_request(self): + request = self._makeRequest() + del request.registry + context = self._makeContext() + result = self._callFUT(context, request, name='notregistered') + self.assertEqual(result, None) + + def test_call_view_registered_secure(self): + request = self._makeRequest() + context = self._makeContext() + response = DummyResponse() + view = make_view(response) + self._registerView(request.registry, view, 'registered') + response = self._callFUT(context, request, name='registered', + secure=True) + self.assertEqual(response.status, '200 OK') + + def test_call_view_registered_insecure_no_call_permissive(self): + context = self._makeContext() + request = self._makeRequest() + response = DummyResponse() + view = make_view(response) + self._registerView(request.registry, view, 'registered') + response = self._callFUT(context, request, name='registered', + secure=False) + self.assertEqual(response.status, '200 OK') + + def test_call_view_registered_insecure_with_call_permissive(self): + context = self._makeContext() + request = self._makeRequest() + response = DummyResponse() + view = make_view(response) + def anotherview(context, request): + return DummyResponse('anotherview') + view.__call_permissive__ = anotherview + self._registerView(request.registry, view, 'registered') + response = self._callFUT(context, request, name='registered', + secure=False) + self.assertEqual(response.status, '200 OK') + self.assertEqual(response.app_iter, ['anotherview']) + + def test_call_view_with_request_iface_on_request(self): + # See https://github.com/Pylons/pyramid/issues/1643 + from zope.interface import Interface + class IWontBeFound(Interface): pass + context = self._makeContext() + request = self._makeRequest() + request.request_iface = IWontBeFound + response = DummyResponse('aview') + view = make_view(response) + self._registerView(request.registry, view, 'aview') + response = self._callFUT(context, request, name='aview') + self.assertEqual(response.status, '200 OK') + self.assertEqual(response.app_iter, ['aview']) + +class RenderViewToIterableTests(BaseTest, unittest.TestCase): + def _callFUT(self, *arg, **kw): + from pyramid.view import render_view_to_iterable + return render_view_to_iterable(*arg, **kw) + + def test_call_no_view_registered(self): + request = self._makeRequest() + context = self._makeContext() + result = self._callFUT(context, request, name='notregistered') + self.assertEqual(result, None) + + def test_call_view_registered_secure(self): + request = self._makeRequest() + context = self._makeContext() + response = DummyResponse() + view = make_view(response) + self._registerView(request.registry, view, 'registered') + iterable = self._callFUT(context, request, name='registered', + secure=True) + self.assertEqual(iterable, ()) + + def test_call_view_registered_insecure_no_call_permissive(self): + context = self._makeContext() + request = self._makeRequest() + response = DummyResponse() + view = make_view(response) + self._registerView(request.registry, view, 'registered') + iterable = self._callFUT(context, request, name='registered', + secure=False) + self.assertEqual(iterable, ()) + + def test_call_view_registered_insecure_with_call_permissive(self): + context = self._makeContext() + request = self._makeRequest() + response = DummyResponse() + view = make_view(response) + def anotherview(context, request): + return DummyResponse(b'anotherview') + view.__call_permissive__ = anotherview + self._registerView(request.registry, view, 'registered') + iterable = self._callFUT(context, request, name='registered', + secure=False) + self.assertEqual(iterable, [b'anotherview']) + + def test_verify_output_bytestring(self): + from pyramid.request import Request + from pyramid.config import Configurator + from pyramid.view import render_view + from webob.compat import text_type + config = Configurator(settings={}) + def view(request): + request.response.text = text_type('') + return request.response + + config.add_view(name='test', view=view) + config.commit() + + r = Request({}) + r.registry = config.registry + self.assertEqual(render_view(object(), r, 'test'), b'') + + def test_call_request_has_no_registry(self): + request = self._makeRequest() + del request.registry + registry = self.config.registry + context = self._makeContext() + response = DummyResponse() + view = make_view(response) + self._registerView(registry, view, 'registered') + iterable = self._callFUT(context, request, name='registered', + secure=True) + self.assertEqual(iterable, ()) + +class RenderViewTests(BaseTest, unittest.TestCase): + def _callFUT(self, *arg, **kw): + from pyramid.view import render_view + return render_view(*arg, **kw) + + def test_call_no_view_registered(self): + request = self._makeRequest() + context = self._makeContext() + result = self._callFUT(context, request, name='notregistered') + self.assertEqual(result, None) + + def test_call_view_registered_secure(self): + request = self._makeRequest() + context = self._makeContext() + response = DummyResponse() + view = make_view(response) + self._registerView(request.registry, view, 'registered') + s = self._callFUT(context, request, name='registered', secure=True) + self.assertEqual(s, b'') + + def test_call_view_registered_insecure_no_call_permissive(self): + context = self._makeContext() + request = self._makeRequest() + response = DummyResponse() + view = make_view(response) + self._registerView(request.registry, view, 'registered') + s = self._callFUT(context, request, name='registered', secure=False) + self.assertEqual(s, b'') + + def test_call_view_registered_insecure_with_call_permissive(self): + context = self._makeContext() + request = self._makeRequest() + response = DummyResponse() + view = make_view(response) + def anotherview(context, request): + return DummyResponse(b'anotherview') + view.__call_permissive__ = anotherview + self._registerView(request.registry, view, 'registered') + s = self._callFUT(context, request, name='registered', secure=False) + self.assertEqual(s, b'anotherview') + +class TestViewConfigDecorator(unittest.TestCase): + def setUp(self): + testing.setUp() + + def tearDown(self): + testing.tearDown() + + def _getTargetClass(self): + from pyramid.view import view_config + return view_config + + def _makeOne(self, *arg, **kw): + return self._getTargetClass()(*arg, **kw) + + def test_create_defaults(self): + decorator = self._makeOne() + self.assertEqual(decorator.__dict__, {}) + + def test_create_context_trumps_for(self): + decorator = self._makeOne(context='123', for_='456') + self.assertEqual(decorator.context, '123') + + def test_create_for_trumps_context_None(self): + decorator = self._makeOne(context=None, for_='456') + self.assertEqual(decorator.context, '456') + + def test_create_nondefaults(self): + decorator = self._makeOne( + name=None, request_type=None, for_=None, + permission='foo', mapper='mapper', + decorator='decorator', match_param='match_param' + ) + self.assertEqual(decorator.name, None) + self.assertEqual(decorator.request_type, None) + self.assertEqual(decorator.context, None) + self.assertEqual(decorator.permission, 'foo') + self.assertEqual(decorator.mapper, 'mapper') + self.assertEqual(decorator.decorator, 'decorator') + self.assertEqual(decorator.match_param, 'match_param') + + def test_create_with_other_predicates(self): + decorator = self._makeOne(foo=1) + self.assertEqual(decorator.foo, 1) + + def test_create_decorator_tuple(self): + decorator = self._makeOne(decorator=('decorator1', 'decorator2')) + self.assertEqual(decorator.decorator, ('decorator1', 'decorator2')) + + def test_call_function(self): + decorator = self._makeOne() + venusian = DummyVenusian() + decorator.venusian = venusian + def foo(): pass + wrapped = decorator(foo) + self.assertTrue(wrapped is foo) + config = call_venusian(venusian) + settings = config.settings + self.assertEqual(len(settings), 1) + self.assertEqual(len(settings), 1) + self.assertEqual(len(settings[0]), 3) + self.assertEqual(settings[0]['venusian'], venusian) + self.assertEqual(settings[0]['view'], None) # comes from call_venusian + self.assertEqual(settings[0]['_info'], 'codeinfo') + + def test_call_class(self): + decorator = self._makeOne() + venusian = DummyVenusian() + decorator.venusian = venusian + decorator.venusian.info.scope = 'class' + class foo(object): pass + wrapped = decorator(foo) + self.assertTrue(wrapped is foo) + config = call_venusian(venusian) + settings = config.settings + self.assertEqual(len(settings), 1) + self.assertEqual(len(settings[0]), 4) + self.assertEqual(settings[0]['venusian'], venusian) + self.assertEqual(settings[0]['view'], None) # comes from call_venusian + self.assertEqual(settings[0]['attr'], 'foo') + self.assertEqual(settings[0]['_info'], 'codeinfo') + + def test_call_class_attr_already_set(self): + decorator = self._makeOne(attr='abc') + venusian = DummyVenusian() + decorator.venusian = venusian + decorator.venusian.info.scope = 'class' + class foo(object): pass + wrapped = decorator(foo) + self.assertTrue(wrapped is foo) + config = call_venusian(venusian) + settings = config.settings + self.assertEqual(len(settings), 1) + self.assertEqual(len(settings[0]), 4) + self.assertEqual(settings[0]['venusian'], venusian) + self.assertEqual(settings[0]['view'], None) # comes from call_venusian + self.assertEqual(settings[0]['attr'], 'abc') + self.assertEqual(settings[0]['_info'], 'codeinfo') + + def test_stacking(self): + decorator1 = self._makeOne(name='1') + venusian1 = DummyVenusian() + decorator1.venusian = venusian1 + venusian2 = DummyVenusian() + decorator2 = self._makeOne(name='2') + decorator2.venusian = venusian2 + def foo(): pass + wrapped1 = decorator1(foo) + wrapped2 = decorator2(wrapped1) + self.assertTrue(wrapped1 is foo) + self.assertTrue(wrapped2 is foo) + config1 = call_venusian(venusian1) + self.assertEqual(len(config1.settings), 1) + self.assertEqual(config1.settings[0]['name'], '1') + config2 = call_venusian(venusian2) + self.assertEqual(len(config2.settings), 1) + self.assertEqual(config2.settings[0]['name'], '2') + + def test_call_as_method(self): + decorator = self._makeOne() + venusian = DummyVenusian() + decorator.venusian = venusian + decorator.venusian.info.scope = 'class' + def foo(self): pass + def bar(self): pass + class foo(object): + foomethod = decorator(foo) + barmethod = decorator(bar) + config = call_venusian(venusian) + settings = config.settings + self.assertEqual(len(settings), 2) + self.assertEqual(settings[0]['attr'], 'foo') + self.assertEqual(settings[1]['attr'], 'bar') + + def test_with_custom_predicates(self): + decorator = self._makeOne(custom_predicates=(1,)) + venusian = DummyVenusian() + decorator.venusian = venusian + def foo(context, request): pass + decorated = decorator(foo) + self.assertTrue(decorated is foo) + config = call_venusian(venusian) + settings = config.settings + self.assertEqual(settings[0]['custom_predicates'], (1,)) + + def test_call_with_renderer_string(self): + import pyramid.tests + decorator = self._makeOne(renderer='fixtures/minimal.pt') + venusian = DummyVenusian() + decorator.venusian = venusian + def foo(): pass + wrapped = decorator(foo) + self.assertTrue(wrapped is foo) + config = call_venusian(venusian) + settings = config.settings + self.assertEqual(len(settings), 1) + renderer = settings[0]['renderer'] + self.assertEqual(renderer, 'fixtures/minimal.pt') + self.assertEqual(config.pkg, pyramid.tests) + + def test_call_with_renderer_dict(self): + import pyramid.tests + decorator = self._makeOne(renderer={'a':1}) + venusian = DummyVenusian() + decorator.venusian = venusian + def foo(): pass + wrapped = decorator(foo) + self.assertTrue(wrapped is foo) + config = call_venusian(venusian) + settings = config.settings + self.assertEqual(len(settings), 1) + self.assertEqual(settings[0]['renderer'], {'a':1}) + self.assertEqual(config.pkg, pyramid.tests) + + def test_call_with_renderer_IRendererInfo(self): + import pyramid.tests + from pyramid.interfaces import IRendererInfo + @implementer(IRendererInfo) + class DummyRendererHelper(object): + pass + renderer_helper = DummyRendererHelper() + decorator = self._makeOne(renderer=renderer_helper) + venusian = DummyVenusian() + decorator.venusian = venusian + def foo(): pass + wrapped = decorator(foo) + self.assertTrue(wrapped is foo) + context = DummyVenusianContext() + config = call_venusian(venusian, context) + settings = config.settings + self.assertEqual(len(settings), 1) + renderer = settings[0]['renderer'] + self.assertTrue(renderer is renderer_helper) + self.assertEqual(config.pkg, pyramid.tests) + + def test_call_withdepth(self): + decorator = self._makeOne(_depth=1) + venusian = DummyVenusian() + decorator.venusian = venusian + def foo(): pass + decorator(foo) + attachments = venusian.attachments + depth = attachments[0][3] + self.assertEqual(depth, 2) + + def test_call_withoutcategory(self): + decorator = self._makeOne() + venusian = DummyVenusian() + decorator.venusian = venusian + def foo(): pass + decorator(foo) + attachments = venusian.attachments + category = attachments[0][2] + self.assertEqual(category, 'pyramid') + + def test_call_withcategory(self): + decorator = self._makeOne(_category='not_pyramid') + venusian = DummyVenusian() + decorator.venusian = venusian + def foo(): pass + decorator(foo) + attachments = venusian.attachments + category = attachments[0][2] + self.assertEqual(category, 'not_pyramid') + +class Test_append_slash_notfound_view(BaseTest, unittest.TestCase): + def _callFUT(self, context, request): + from pyramid.view import append_slash_notfound_view + return append_slash_notfound_view(context, request) + + def _registerMapper(self, reg, match=True): + from pyramid.interfaces import IRoutesMapper + class DummyRoute(object): + def __init__(self, val): + self.val = val + def match(self, path): + return self.val + class DummyMapper(object): + def __init__(self): + self.routelist = [ DummyRoute(match) ] + def get_routes(self): + return self.routelist + mapper = DummyMapper() + reg.registerUtility(mapper, IRoutesMapper) + return mapper + + def test_context_is_not_exception(self): + request = self._makeRequest(PATH_INFO='/abc') + request.exception = ExceptionResponse() + context = DummyContext() + response = self._callFUT(context, request) + self.assertEqual(response.status, '404 Not Found') + self.assertEqual(response.app_iter, ['Not Found']) + + def test_no_mapper(self): + request = self._makeRequest(PATH_INFO='/abc') + context = ExceptionResponse() + response = self._callFUT(context, request) + self.assertEqual(response.status, '404 Not Found') + + def test_no_path(self): + request = self._makeRequest() + context = ExceptionResponse() + self._registerMapper(request.registry, True) + response = self._callFUT(context, request) + self.assertEqual(response.status, '404 Not Found') + + def test_mapper_path_already_slash_ending(self): + request = self._makeRequest(PATH_INFO='/abc/') + context = ExceptionResponse() + self._registerMapper(request.registry, True) + response = self._callFUT(context, request) + self.assertEqual(response.status, '404 Not Found') + + def test_no_route_matches(self): + request = self._makeRequest(PATH_INFO='/abc') + context = ExceptionResponse() + mapper = self._registerMapper(request.registry, True) + mapper.routelist[0].val = None + response = self._callFUT(context, request) + self.assertEqual(response.status, '404 Not Found') + + def test_matches(self): + request = self._makeRequest(PATH_INFO='/abc') + context = ExceptionResponse() + self._registerMapper(request.registry, True) + response = self._callFUT(context, request) + self.assertEqual(response.status, '307 Temporary Redirect') + self.assertEqual(response.location, '/abc/') + + def test_matches_with_script_name(self): + request = self._makeRequest(PATH_INFO='/abc', SCRIPT_NAME='/foo') + context = ExceptionResponse() + self._registerMapper(request.registry, True) + response = self._callFUT(context, request) + self.assertEqual(response.status, '307 Temporary Redirect') + self.assertEqual(response.location, '/foo/abc/') + + def test_with_query_string(self): + request = self._makeRequest(PATH_INFO='/abc', QUERY_STRING='a=1&b=2') + context = ExceptionResponse() + self._registerMapper(request.registry, True) + response = self._callFUT(context, request) + self.assertEqual(response.status, '307 Temporary Redirect') + self.assertEqual(response.location, '/abc/?a=1&b=2') + +class TestAppendSlashNotFoundViewFactory(BaseTest, unittest.TestCase): + def _makeOne(self, notfound_view): + from pyramid.view import AppendSlashNotFoundViewFactory + return AppendSlashNotFoundViewFactory(notfound_view) + + def test_custom_notfound_view(self): + request = self._makeRequest(PATH_INFO='/abc') + context = ExceptionResponse() + def custom_notfound(context, request): + return 'OK' + view = self._makeOne(custom_notfound) + response = view(context, request) + self.assertEqual(response, 'OK') + +class Test_default_exceptionresponse_view(unittest.TestCase): + def _callFUT(self, context, request): + from pyramid.view import default_exceptionresponse_view + return default_exceptionresponse_view(context, request) + + def test_is_exception(self): + context = Exception() + result = self._callFUT(context, None) + self.assertTrue(result is context) + + def test_is_not_exception_context_is_false_still_chose(self): + request = DummyRequest() + request.exception = 0 + result = self._callFUT(None, request) + self.assertTrue(result is None) + + def test_is_not_exception_no_request_exception(self): + context = object() + request = DummyRequest() + request.exception = None + result = self._callFUT(context, request) + self.assertTrue(result is context) + + def test_is_not_exception_request_exception(self): + context = object() + request = DummyRequest() + request.exception = 'abc' + result = self._callFUT(context, request) + self.assertEqual(result, 'abc') + +class Test_view_defaults(unittest.TestCase): + def test_it(self): + from pyramid.view import view_defaults + @view_defaults(route_name='abc', renderer='def') + class Foo(object): pass + self.assertEqual(Foo.__view_defaults__['route_name'],'abc') + self.assertEqual(Foo.__view_defaults__['renderer'],'def') + + def test_it_inheritance_not_overridden(self): + from pyramid.view import view_defaults + @view_defaults(route_name='abc', renderer='def') + class Foo(object): pass + class Bar(Foo): pass + self.assertEqual(Bar.__view_defaults__['route_name'],'abc') + self.assertEqual(Bar.__view_defaults__['renderer'],'def') + + def test_it_inheritance_overriden(self): + from pyramid.view import view_defaults + @view_defaults(route_name='abc', renderer='def') + class Foo(object): pass + @view_defaults(route_name='ghi') + class Bar(Foo): pass + self.assertEqual(Bar.__view_defaults__['route_name'],'ghi') + self.assertFalse('renderer' in Bar.__view_defaults__) + + def test_it_inheritance_overriden_empty(self): + from pyramid.view import view_defaults + @view_defaults(route_name='abc', renderer='def') + class Foo(object): pass + @view_defaults() + class Bar(Foo): pass + self.assertEqual(Bar.__view_defaults__, {}) + +class TestViewMethodsMixin(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + testing.tearDown() + + def _makeOne(self, environ=None): + from pyramid.decorator import reify + from pyramid.view import ViewMethodsMixin + if environ is None: + environ = {} + class Request(ViewMethodsMixin): + def __init__(self, environ): + self.environ = environ + + @reify + def response(self): + return DummyResponse() + request = Request(environ) + request.registry = self.config.registry + return request + + def test_it(self): + def exc_view(exc, request): + self.assertTrue(exc is dummy_exc) + self.assertTrue(request.exception is dummy_exc) + return DummyResponse(b'foo') + self.config.add_view(exc_view, context=RuntimeError) + request = self._makeOne() + dummy_exc = RuntimeError() + try: + raise dummy_exc + except RuntimeError: + response = request.invoke_exception_view() + self.assertEqual(response.app_iter, [b'foo']) + else: # pragma: no cover + self.fail() + + def test_it_hides_attrs(self): + def exc_view(exc, request): + self.assertTrue(exc is not orig_exc) + self.assertTrue(request.exception is not orig_exc) + self.assertTrue(request.exc_info is not orig_exc_info) + self.assertTrue(request.response is not orig_response) + request.response.app_iter = [b'bar'] + return request.response + self.config.add_view(exc_view, context=RuntimeError) + request = self._makeOne() + orig_exc = request.exception = DummyContext() + orig_exc_info = request.exc_info = DummyContext() + orig_response = request.response = DummyResponse(b'foo') + try: + raise RuntimeError + except RuntimeError as ex: + response = request.invoke_exception_view() + self.assertEqual(response.app_iter, [b'bar']) + self.assertTrue(request.exception is ex) + self.assertTrue(request.exc_info[1] is ex) + self.assertTrue(request.response is orig_response) + else: # pragma: no cover + self.fail() + + def test_it_supports_alternate_requests(self): + def exc_view(exc, request): + self.assertTrue(request is other_req) + from pyramid.threadlocal import get_current_request + self.assertTrue(get_current_request() is other_req) + return DummyResponse(b'foo') + self.config.add_view(exc_view, context=RuntimeError) + request = self._makeOne() + other_req = self._makeOne() + try: + raise RuntimeError + except RuntimeError: + response = request.invoke_exception_view(request=other_req) + self.assertEqual(response.app_iter, [b'foo']) + else: # pragma: no cover + self.fail() + + def test_it_supports_threadlocal_registry(self): + def exc_view(exc, request): + return DummyResponse(b'foo') + self.config.add_view(exc_view, context=RuntimeError) + request = self._makeOne() + del request.registry + try: + raise RuntimeError + except RuntimeError: + response = request.invoke_exception_view() + self.assertEqual(response.app_iter, [b'foo']) + else: # pragma: no cover + self.fail() + + def test_it_raises_if_no_registry(self): + request = self._makeOne() + del request.registry + from pyramid.threadlocal import manager + manager.push({'registry': None, 'request': request}) + try: + raise RuntimeError + except RuntimeError: + try: + request.invoke_exception_view() + except RuntimeError as e: + self.assertEqual(e.args[0], "Unable to retrieve registry") + else: # pragma: no cover + self.fail() + finally: + manager.pop() + + def test_it_supports_alternate_exc_info(self): + def exc_view(exc, request): + self.assertTrue(request.exc_info is exc_info) + return DummyResponse(b'foo') + self.config.add_view(exc_view, context=RuntimeError) + request = self._makeOne() + try: + raise RuntimeError + except RuntimeError: + exc_info = sys.exc_info() + response = request.invoke_exception_view(exc_info=exc_info) + self.assertEqual(response.app_iter, [b'foo']) + + def test_it_rejects_secured_view(self): + from pyramid.exceptions import Forbidden + def exc_view(exc, request): pass + self.config.testing_securitypolicy(permissive=False) + self.config.add_view(exc_view, context=RuntimeError, permission='view') + request = self._makeOne() + try: + raise RuntimeError + except RuntimeError: + self.assertRaises(Forbidden, request.invoke_exception_view) + else: # pragma: no cover + self.fail() + + def test_it_allows_secured_view(self): + def exc_view(exc, request): + return DummyResponse(b'foo') + self.config.testing_securitypolicy(permissive=False) + self.config.add_view(exc_view, context=RuntimeError, permission='view') + request = self._makeOne() + try: + raise RuntimeError + except RuntimeError: + response = request.invoke_exception_view(secure=False) + self.assertEqual(response.app_iter, [b'foo']) + else: # pragma: no cover + self.fail() + + def test_it_raises_if_not_found(self): + from pyramid.httpexceptions import HTTPNotFound + request = self._makeOne() + dummy_exc = RuntimeError() + try: + raise dummy_exc + except RuntimeError: + self.assertRaises(HTTPNotFound, request.invoke_exception_view) + else: # pragma: no cover + self.fail() + + def test_it_reraises_if_not_found(self): + request = self._makeOne() + dummy_exc = RuntimeError() + try: + raise dummy_exc + except RuntimeError: + self.assertRaises( + RuntimeError, + lambda: request.invoke_exception_view(reraise=True)) + else: # pragma: no cover + self.fail() + + def test_it_raises_predicate_mismatch(self): + from pyramid.exceptions import PredicateMismatch + def exc_view(exc, request): pass + self.config.add_view(exc_view, context=Exception, request_method='POST') + request = self._makeOne() + request.method = 'GET' + dummy_exc = RuntimeError() + try: + raise dummy_exc + except RuntimeError: + self.assertRaises(PredicateMismatch, request.invoke_exception_view) + else: # pragma: no cover + self.fail() + + def test_it_reraises_after_predicate_mismatch(self): + def exc_view(exc, request): pass + self.config.add_view(exc_view, context=Exception, request_method='POST') + request = self._makeOne() + request.method = 'GET' + dummy_exc = RuntimeError() + try: + raise dummy_exc + except RuntimeError: + self.assertRaises( + RuntimeError, + lambda: request.invoke_exception_view(reraise=True)) + else: # pragma: no cover + self.fail() + +class ExceptionResponse(Exception): + status = '404 Not Found' + app_iter = ['Not Found'] + headerlist = [] + +class DummyContext: + pass + +def make_view(response): + def view(context, request): + return response + return view + +class DummyRequest: + exception = None + request_iface = IRequest + + def __init__(self, environ=None): + if environ is None: + environ = {} + self.environ = environ + +from pyramid.interfaces import IResponse + +@implementer(IResponse) +class DummyResponse(object): + headerlist = () + app_iter = () + status = '200 OK' + environ = None + def __init__(self, body=None): + if body is None: + self.app_iter = () + else: + self.app_iter = [body] + +from zope.interface import Interface +class IContext(Interface): + pass + +class DummyVenusianInfo(object): + scope = 'notaclass' + module = sys.modules['pyramid.tests'] + codeinfo = 'codeinfo' + +class DummyVenusian(object): + def __init__(self, info=None): + if info is None: + info = DummyVenusianInfo() + self.info = info + self.attachments = [] + + def attach(self, wrapped, callback, category=None, depth=1): + self.attachments.append((wrapped, callback, category, depth)) + return self.info + +class DummyRegistry(object): + pass + +class DummyConfig(object): + def __init__(self): + self.settings = [] + self.registry = DummyRegistry() + + def add_view(self, **kw): + self.settings.append(kw) + + add_notfound_view = add_forbidden_view = add_exception_view = add_view + + def with_package(self, pkg): + self.pkg = pkg + return self + +class DummyVenusianContext(object): + def __init__(self): + self.config = DummyConfig() + +def call_venusian(venusian, context=None): + if context is None: + context = DummyVenusianContext() + for wrapped, callback, category, depth in venusian.attachments: + callback(context, None, None) + return context.config + diff --git a/src/pyramid/tests/test_viewderivers.py b/src/pyramid/tests/test_viewderivers.py new file mode 100644 index 000000000..6b81cc1e5 --- /dev/null +++ b/src/pyramid/tests/test_viewderivers.py @@ -0,0 +1,1795 @@ +import unittest +from zope.interface import implementer + +from pyramid import testing +from pyramid.exceptions import ConfigurationError +from pyramid.interfaces import ( + IResponse, + IRequest, + ) + +class TestDeriveView(unittest.TestCase): + + def setUp(self): + self.config = testing.setUp() + self.config.set_default_csrf_options(require_csrf=False) + + def tearDown(self): + self.config = None + testing.tearDown() + + def _makeRequest(self): + request = DummyRequest() + request.registry = self.config.registry + return request + + def _registerLogger(self): + from pyramid.interfaces import IDebugLogger + logger = DummyLogger() + self.config.registry.registerUtility(logger, IDebugLogger) + return logger + + def _registerSecurityPolicy(self, permissive): + from pyramid.interfaces import IAuthenticationPolicy + from pyramid.interfaces import IAuthorizationPolicy + policy = DummySecurityPolicy(permissive) + self.config.registry.registerUtility(policy, IAuthenticationPolicy) + self.config.registry.registerUtility(policy, IAuthorizationPolicy) + + def test_function_returns_non_adaptable(self): + def view(request): + return None + result = self.config.derive_view(view) + self.assertFalse(result is view) + try: + result(None, None) + except ValueError as e: + self.assertEqual( + e.args[0], + 'Could not convert return value of the view callable function ' + 'pyramid.tests.test_viewderivers.view into a response ' + 'object. The value returned was None. You may have forgotten ' + 'to return a value from the view callable.' + ) + else: # pragma: no cover + raise AssertionError + + def test_function_returns_non_adaptable_dict(self): + def view(request): + return {'a':1} + result = self.config.derive_view(view) + self.assertFalse(result is view) + try: + result(None, None) + except ValueError as e: + self.assertEqual( + e.args[0], + "Could not convert return value of the view callable function " + "pyramid.tests.test_viewderivers.view into a response " + "object. The value returned was {'a': 1}. You may have " + "forgotten to define a renderer in the view configuration." + ) + else: # pragma: no cover + raise AssertionError + + def test_instance_returns_non_adaptable(self): + class AView(object): + def __call__(self, request): + return None + view = AView() + result = self.config.derive_view(view) + self.assertFalse(result is view) + try: + result(None, None) + except ValueError as e: + msg = e.args[0] + self.assertTrue(msg.startswith( + 'Could not convert return value of the view callable object ' + ' into a response object. The value returned was None. You ' + 'may have forgotten to return a value from the view callable.')) + else: # pragma: no cover + raise AssertionError + + def test_function_returns_true_Response_no_renderer(self): + from pyramid.response import Response + r = Response('Hello') + def view(request): + return r + result = self.config.derive_view(view) + self.assertFalse(result is view) + response = result(None, None) + self.assertEqual(response, r) + + def test_function_returns_true_Response_with_renderer(self): + from pyramid.response import Response + r = Response('Hello') + def view(request): + return r + renderer = object() + result = self.config.derive_view(view) + self.assertFalse(result is view) + response = result(None, None) + self.assertEqual(response, r) + + def test_requestonly_default_method_returns_non_adaptable(self): + request = DummyRequest() + class AView(object): + def __init__(self, request): + pass + def __call__(self): + return None + result = self.config.derive_view(AView) + self.assertFalse(result is AView) + try: + result(None, request) + except ValueError as e: + self.assertEqual( + e.args[0], + 'Could not convert return value of the view callable ' + 'method __call__ of ' + 'class pyramid.tests.test_viewderivers.AView into a ' + 'response object. The value returned was None. You may have ' + 'forgotten to return a value from the view callable.' + ) + else: # pragma: no cover + raise AssertionError + + def test_requestonly_nondefault_method_returns_non_adaptable(self): + request = DummyRequest() + class AView(object): + def __init__(self, request): + pass + def theviewmethod(self): + return None + result = self.config.derive_view(AView, attr='theviewmethod') + self.assertFalse(result is AView) + try: + result(None, request) + except ValueError as e: + self.assertEqual( + e.args[0], + 'Could not convert return value of the view callable ' + 'method theviewmethod of ' + 'class pyramid.tests.test_viewderivers.AView into a ' + 'response object. The value returned was None. You may have ' + 'forgotten to return a value from the view callable.' + ) + else: # pragma: no cover + raise AssertionError + + def test_requestonly_function(self): + response = DummyResponse() + def view(request): + return response + result = self.config.derive_view(view) + self.assertFalse(result is view) + self.assertEqual(result(None, None), response) + + def test_requestonly_function_with_renderer(self): + response = DummyResponse() + class moo(object): + def render_view(inself, req, resp, view_inst, ctx): + self.assertEqual(req, request) + self.assertEqual(resp, 'OK') + self.assertEqual(view_inst, view) + self.assertEqual(ctx, context) + return response + def clone(self): + return self + def view(request): + return 'OK' + result = self.config.derive_view(view, renderer=moo()) + self.assertFalse(result.__wraps__ is view) + request = self._makeRequest() + context = testing.DummyResource() + self.assertEqual(result(context, request), response) + + def test_requestonly_function_with_renderer_request_override(self): + def moo(info): + def inner(value, system): + self.assertEqual(value, 'OK') + self.assertEqual(system['request'], request) + self.assertEqual(system['context'], context) + return b'moo' + return inner + def view(request): + return 'OK' + self.config.add_renderer('moo', moo) + result = self.config.derive_view(view, renderer='string') + self.assertFalse(result is view) + request = self._makeRequest() + request.override_renderer = 'moo' + context = testing.DummyResource() + self.assertEqual(result(context, request).body, b'moo') + + def test_requestonly_function_with_renderer_request_has_view(self): + response = DummyResponse() + class moo(object): + def render_view(inself, req, resp, view_inst, ctx): + self.assertEqual(req, request) + self.assertEqual(resp, 'OK') + self.assertEqual(view_inst, 'view') + self.assertEqual(ctx, context) + return response + def clone(self): + return self + def view(request): + return 'OK' + result = self.config.derive_view(view, renderer=moo()) + self.assertFalse(result.__wraps__ is view) + request = self._makeRequest() + request.__view__ = 'view' + context = testing.DummyResource() + r = result(context, request) + self.assertEqual(r, response) + self.assertFalse(hasattr(request, '__view__')) + + def test_class_without_attr(self): + response = DummyResponse() + class View(object): + def __init__(self, request): + pass + def __call__(self): + return response + result = self.config.derive_view(View) + request = self._makeRequest() + self.assertEqual(result(None, request), response) + self.assertEqual(request.__view__.__class__, View) + + def test_class_with_attr(self): + response = DummyResponse() + class View(object): + def __init__(self, request): + pass + def another(self): + return response + result = self.config.derive_view(View, attr='another') + request = self._makeRequest() + self.assertEqual(result(None, request), response) + self.assertEqual(request.__view__.__class__, View) + + def test_as_function_context_and_request(self): + def view(context, request): + return 'OK' + result = self.config.derive_view(view) + self.assertTrue(result.__wraps__ is view) + self.assertFalse(hasattr(result, '__call_permissive__')) + self.assertEqual(view(None, None), 'OK') + + def test_as_function_requestonly(self): + response = DummyResponse() + def view(request): + return response + result = self.config.derive_view(view) + self.assertFalse(result is view) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertFalse(hasattr(result, '__call_permissive__')) + self.assertEqual(result(None, None), response) + + def test_as_newstyle_class_context_and_request(self): + response = DummyResponse() + class view(object): + def __init__(self, context, request): + pass + def __call__(self): + return response + result = self.config.derive_view(view) + self.assertFalse(result is view) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertFalse(hasattr(result, '__call_permissive__')) + request = self._makeRequest() + self.assertEqual(result(None, request), response) + self.assertEqual(request.__view__.__class__, view) + + def test_as_newstyle_class_requestonly(self): + response = DummyResponse() + class view(object): + def __init__(self, context, request): + pass + def __call__(self): + return response + result = self.config.derive_view(view) + self.assertFalse(result is view) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertFalse(hasattr(result, '__call_permissive__')) + request = self._makeRequest() + self.assertEqual(result(None, request), response) + self.assertEqual(request.__view__.__class__, view) + + def test_as_oldstyle_class_context_and_request(self): + response = DummyResponse() + class view: + def __init__(self, context, request): + pass + def __call__(self): + return response + result = self.config.derive_view(view) + self.assertFalse(result is view) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertFalse(hasattr(result, '__call_permissive__')) + request = self._makeRequest() + self.assertEqual(result(None, request), response) + self.assertEqual(request.__view__.__class__, view) + + def test_as_oldstyle_class_requestonly(self): + response = DummyResponse() + class view: + def __init__(self, context, request): + pass + def __call__(self): + return response + result = self.config.derive_view(view) + self.assertFalse(result is view) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertFalse(hasattr(result, '__call_permissive__')) + request = self._makeRequest() + self.assertEqual(result(None, request), response) + self.assertEqual(request.__view__.__class__, view) + + def test_as_instance_context_and_request(self): + response = DummyResponse() + class View: + def __call__(self, context, request): + return response + view = View() + result = self.config.derive_view(view) + self.assertTrue(result.__wraps__ is view) + self.assertFalse(hasattr(result, '__call_permissive__')) + self.assertEqual(result(None, None), response) + + def test_as_instance_requestonly(self): + response = DummyResponse() + class View: + def __call__(self, request): + return response + view = View() + result = self.config.derive_view(view) + self.assertFalse(result is view) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertTrue('test_viewderivers' in result.__name__) + self.assertFalse(hasattr(result, '__call_permissive__')) + self.assertEqual(result(None, None), response) + + def test_with_debug_authorization_no_authpol(self): + response = DummyResponse() + view = lambda *arg: response + self.config.registry.settings = dict( + debug_authorization=True, reload_templates=True) + logger = self._registerLogger() + result = self.config._derive_view(view, permission='view') + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertFalse(hasattr(result, '__call_permissive__')) + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + self.assertEqual(result(None, request), response) + self.assertEqual(len(logger.messages), 1) + self.assertEqual(logger.messages[0], + "debug_authorization of url url (view name " + "'view_name' against context None): Allowed " + "(no authorization policy in use)") + + def test_with_debug_authorization_authn_policy_no_authz_policy(self): + response = DummyResponse() + view = lambda *arg: response + self.config.registry.settings = dict(debug_authorization=True) + from pyramid.interfaces import IAuthenticationPolicy + policy = DummySecurityPolicy(False) + self.config.registry.registerUtility(policy, IAuthenticationPolicy) + logger = self._registerLogger() + result = self.config._derive_view(view, permission='view') + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertFalse(hasattr(result, '__call_permissive__')) + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + self.assertEqual(result(None, request), response) + self.assertEqual(len(logger.messages), 1) + self.assertEqual(logger.messages[0], + "debug_authorization of url url (view name " + "'view_name' against context None): Allowed " + "(no authorization policy in use)") + + def test_with_debug_authorization_authz_policy_no_authn_policy(self): + response = DummyResponse() + view = lambda *arg: response + self.config.registry.settings = dict(debug_authorization=True) + from pyramid.interfaces import IAuthorizationPolicy + policy = DummySecurityPolicy(False) + self.config.registry.registerUtility(policy, IAuthorizationPolicy) + logger = self._registerLogger() + result = self.config._derive_view(view, permission='view') + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertFalse(hasattr(result, '__call_permissive__')) + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + self.assertEqual(result(None, request), response) + self.assertEqual(len(logger.messages), 1) + self.assertEqual(logger.messages[0], + "debug_authorization of url url (view name " + "'view_name' against context None): Allowed " + "(no authorization policy in use)") + + def test_with_debug_authorization_no_permission(self): + response = DummyResponse() + view = lambda *arg: response + self.config.registry.settings = dict( + debug_authorization=True, reload_templates=True) + self._registerSecurityPolicy(True) + logger = self._registerLogger() + result = self.config._derive_view(view) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertFalse(hasattr(result, '__call_permissive__')) + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + self.assertEqual(result(None, request), response) + self.assertEqual(len(logger.messages), 1) + self.assertEqual(logger.messages[0], + "debug_authorization of url url (view name " + "'view_name' against context None): Allowed (" + "no permission registered)") + + def test_debug_auth_permission_authpol_permitted(self): + response = DummyResponse() + view = lambda *arg: response + self.config.registry.settings = dict( + debug_authorization=True, reload_templates=True) + logger = self._registerLogger() + self._registerSecurityPolicy(True) + result = self.config._derive_view(view, permission='view') + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertEqual(result.__call_permissive__.__wraps__, view) + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + self.assertEqual(result(None, request), response) + self.assertEqual(len(logger.messages), 1) + self.assertEqual(logger.messages[0], + "debug_authorization of url url (view name " + "'view_name' against context None): True") + + def test_debug_auth_permission_authpol_permitted_no_request(self): + response = DummyResponse() + view = lambda *arg: response + self.config.registry.settings = dict( + debug_authorization=True, reload_templates=True) + logger = self._registerLogger() + self._registerSecurityPolicy(True) + result = self.config._derive_view(view, permission='view') + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertEqual(result.__call_permissive__.__wraps__, view) + self.assertEqual(result(None, None), response) + self.assertEqual(len(logger.messages), 1) + self.assertEqual(logger.messages[0], + "debug_authorization of url None (view name " + "None against context None): True") + + def test_debug_auth_permission_authpol_denied(self): + from pyramid.httpexceptions import HTTPForbidden + response = DummyResponse() + view = lambda *arg: response + self.config.registry.settings = dict( + debug_authorization=True, reload_templates=True) + logger = self._registerLogger() + self._registerSecurityPolicy(False) + result = self.config._derive_view(view, permission='view') + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertEqual(result.__call_permissive__.__wraps__, view) + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + self.assertRaises(HTTPForbidden, result, None, request) + self.assertEqual(len(logger.messages), 1) + self.assertEqual(logger.messages[0], + "debug_authorization of url url (view name " + "'view_name' against context None): False") + + def test_debug_auth_permission_authpol_denied2(self): + view = lambda *arg: 'OK' + self.config.registry.settings = dict( + debug_authorization=True, reload_templates=True) + self._registerLogger() + self._registerSecurityPolicy(False) + result = self.config._derive_view(view, permission='view') + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + permitted = result.__permitted__(None, None) + self.assertEqual(permitted, False) + + def test_debug_auth_permission_authpol_overridden(self): + from pyramid.security import NO_PERMISSION_REQUIRED + response = DummyResponse() + view = lambda *arg: response + self.config.registry.settings = dict( + debug_authorization=True, reload_templates=True) + logger = self._registerLogger() + self._registerSecurityPolicy(False) + result = self.config._derive_view(view, permission=NO_PERMISSION_REQUIRED) + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertFalse(hasattr(result, '__call_permissive__')) + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + self.assertEqual(result(None, request), response) + self.assertEqual(len(logger.messages), 1) + self.assertEqual(logger.messages[0], + "debug_authorization of url url (view name " + "'view_name' against context None): " + "Allowed (NO_PERMISSION_REQUIRED)") + + def test_debug_auth_permission_authpol_permitted_excview(self): + response = DummyResponse() + view = lambda *arg: response + self.config.registry.settings = dict( + debug_authorization=True, reload_templates=True) + logger = self._registerLogger() + self._registerSecurityPolicy(True) + result = self.config._derive_view( + view, context=Exception, permission='view') + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertEqual(result.__call_permissive__.__wraps__, view) + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + self.assertEqual(result(Exception(), request), response) + self.assertEqual(len(logger.messages), 1) + self.assertEqual(logger.messages[0], + "debug_authorization of url url (view name " + "'view_name' against context Exception()): True") + + def test_secured_view_authn_policy_no_authz_policy(self): + response = DummyResponse() + view = lambda *arg: response + self.config.registry.settings = {} + from pyramid.interfaces import IAuthenticationPolicy + policy = DummySecurityPolicy(False) + self.config.registry.registerUtility(policy, IAuthenticationPolicy) + result = self.config._derive_view(view, permission='view') + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertFalse(hasattr(result, '__call_permissive__')) + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + self.assertEqual(result(None, request), response) + + def test_secured_view_authz_policy_no_authn_policy(self): + response = DummyResponse() + view = lambda *arg: response + self.config.registry.settings = {} + from pyramid.interfaces import IAuthorizationPolicy + policy = DummySecurityPolicy(False) + self.config.registry.registerUtility(policy, IAuthorizationPolicy) + result = self.config._derive_view(view, permission='view') + self.assertEqual(view.__module__, result.__module__) + self.assertEqual(view.__doc__, result.__doc__) + self.assertEqual(view.__name__, result.__name__) + self.assertFalse(hasattr(result, '__call_permissive__')) + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + self.assertEqual(result(None, request), response) + + def test_secured_view_raises_forbidden_no_name(self): + from pyramid.interfaces import IAuthenticationPolicy + from pyramid.interfaces import IAuthorizationPolicy + from pyramid.httpexceptions import HTTPForbidden + response = DummyResponse() + view = lambda *arg: response + self.config.registry.settings = {} + policy = DummySecurityPolicy(False) + self.config.registry.registerUtility(policy, IAuthenticationPolicy) + self.config.registry.registerUtility(policy, IAuthorizationPolicy) + result = self.config._derive_view(view, permission='view') + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + try: + result(None, request) + except HTTPForbidden as e: + self.assertEqual(e.message, + 'Unauthorized: failed permission check') + else: # pragma: no cover + raise AssertionError + + def test_secured_view_raises_forbidden_with_name(self): + from pyramid.interfaces import IAuthenticationPolicy + from pyramid.interfaces import IAuthorizationPolicy + from pyramid.httpexceptions import HTTPForbidden + def myview(request): pass + self.config.registry.settings = {} + policy = DummySecurityPolicy(False) + self.config.registry.registerUtility(policy, IAuthenticationPolicy) + self.config.registry.registerUtility(policy, IAuthorizationPolicy) + result = self.config._derive_view(myview, permission='view') + request = self._makeRequest() + request.view_name = 'view_name' + request.url = 'url' + try: + result(None, request) + except HTTPForbidden as e: + self.assertEqual(e.message, + 'Unauthorized: myview failed permission check') + else: # pragma: no cover + raise AssertionError + + def test_secured_view_skipped_by_default_on_exception_view(self): + from pyramid.request import Request + from pyramid.security import NO_PERMISSION_REQUIRED + def view(request): + raise ValueError + def excview(request): + return 'hello' + self._registerSecurityPolicy(False) + self.config.add_settings({'debug_authorization': True}) + self.config.set_default_permission('view') + self.config.add_view(view, name='foo', permission=NO_PERMISSION_REQUIRED) + self.config.add_view(excview, context=ValueError, renderer='string') + app = self.config.make_wsgi_app() + request = Request.blank('/foo', base_url='http://example.com') + request.method = 'POST' + response = request.get_response(app) + self.assertTrue(b'hello' in response.body) + + def test_secured_view_failed_on_explicit_exception_view(self): + from pyramid.httpexceptions import HTTPForbidden + from pyramid.request import Request + from pyramid.security import NO_PERMISSION_REQUIRED + def view(request): + raise ValueError + def excview(request): pass + self._registerSecurityPolicy(False) + self.config.add_view(view, name='foo', permission=NO_PERMISSION_REQUIRED) + self.config.add_view(excview, context=ValueError, renderer='string', + permission='view') + app = self.config.make_wsgi_app() + request = Request.blank('/foo', base_url='http://example.com') + request.method = 'POST' + try: + request.get_response(app) + except HTTPForbidden: + pass + else: # pragma: no cover + raise AssertionError + + def test_secured_view_passed_on_explicit_exception_view(self): + from pyramid.request import Request + from pyramid.security import NO_PERMISSION_REQUIRED + def view(request): + raise ValueError + def excview(request): + return 'hello' + self._registerSecurityPolicy(True) + self.config.add_view(view, name='foo', permission=NO_PERMISSION_REQUIRED) + self.config.add_view(excview, context=ValueError, renderer='string', + permission='view') + app = self.config.make_wsgi_app() + request = Request.blank('/foo', base_url='http://example.com') + request.method = 'POST' + request.headers['X-CSRF-Token'] = 'foo' + response = request.get_response(app) + self.assertTrue(b'hello' in response.body) + + def test_predicate_mismatch_view_has_no_name(self): + from pyramid.exceptions import PredicateMismatch + response = DummyResponse() + view = lambda *arg: response + def predicate1(context, request): + return False + predicate1.text = lambda *arg: 'text' + result = self.config._derive_view(view, predicates=[predicate1]) + request = self._makeRequest() + request.method = 'POST' + try: + result(None, None) + except PredicateMismatch as e: + self.assertEqual(e.detail, + 'predicate mismatch for view (text)') + else: # pragma: no cover + raise AssertionError + + def test_predicate_mismatch_view_has_name(self): + from pyramid.exceptions import PredicateMismatch + def myview(request): pass + def predicate1(context, request): + return False + predicate1.text = lambda *arg: 'text' + result = self.config._derive_view(myview, predicates=[predicate1]) + request = self._makeRequest() + request.method = 'POST' + try: + result(None, None) + except PredicateMismatch as e: + self.assertEqual(e.detail, + 'predicate mismatch for view myview (text)') + else: # pragma: no cover + raise AssertionError + + def test_predicate_mismatch_exception_has_text_in_detail(self): + from pyramid.exceptions import PredicateMismatch + def myview(request): pass + def predicate1(context, request): + return True + predicate1.text = lambda *arg: 'pred1' + def predicate2(context, request): + return False + predicate2.text = lambda *arg: 'pred2' + result = self.config._derive_view(myview, + predicates=[predicate1, predicate2]) + request = self._makeRequest() + request.method = 'POST' + try: + result(None, None) + except PredicateMismatch as e: + self.assertEqual(e.detail, + 'predicate mismatch for view myview (pred2)') + else: # pragma: no cover + raise AssertionError + + def test_with_predicates_all(self): + response = DummyResponse() + view = lambda *arg: response + predicates = [] + def predicate1(context, request): + predicates.append(True) + return True + def predicate2(context, request): + predicates.append(True) + return True + result = self.config._derive_view(view, + predicates=[predicate1, predicate2]) + request = self._makeRequest() + request.method = 'POST' + next = result(None, None) + self.assertEqual(next, response) + self.assertEqual(predicates, [True, True]) + + def test_with_predicates_checker(self): + view = lambda *arg: 'OK' + predicates = [] + def predicate1(context, request): + predicates.append(True) + return True + def predicate2(context, request): + predicates.append(True) + return True + result = self.config._derive_view(view, + predicates=[predicate1, predicate2]) + request = self._makeRequest() + request.method = 'POST' + next = result.__predicated__(None, None) + self.assertEqual(next, True) + self.assertEqual(predicates, [True, True]) + + def test_with_predicates_notall(self): + from pyramid.httpexceptions import HTTPNotFound + view = lambda *arg: 'OK' + predicates = [] + def predicate1(context, request): + predicates.append(True) + return True + predicate1.text = lambda *arg: 'text' + def predicate2(context, request): + predicates.append(True) + return False + predicate2.text = lambda *arg: 'text' + result = self.config._derive_view(view, + predicates=[predicate1, predicate2]) + request = self._makeRequest() + request.method = 'POST' + self.assertRaises(HTTPNotFound, result, None, None) + self.assertEqual(predicates, [True, True]) + + def test_with_wrapper_viewname(self): + from pyramid.response import Response + from pyramid.interfaces import IView + from pyramid.interfaces import IViewClassifier + inner_response = Response('OK') + def inner_view(context, request): + return inner_response + def outer_view(context, request): + self.assertEqual(request.wrapped_response, inner_response) + self.assertEqual(request.wrapped_body, inner_response.body) + self.assertEqual(request.wrapped_view.__original_view__, + inner_view) + return Response(b'outer ' + request.wrapped_body) + self.config.registry.registerAdapter( + outer_view, (IViewClassifier, None, None), IView, 'owrap') + result = self.config._derive_view(inner_view, viewname='inner', + wrapper_viewname='owrap') + self.assertFalse(result is inner_view) + self.assertEqual(inner_view.__module__, result.__module__) + self.assertEqual(inner_view.__doc__, result.__doc__) + request = self._makeRequest() + response = result(None, request) + self.assertEqual(response.body, b'outer OK') + + def test_with_wrapper_viewname_notfound(self): + from pyramid.response import Response + inner_response = Response('OK') + def inner_view(context, request): + return inner_response + wrapped = self.config._derive_view(inner_view, viewname='inner', + wrapper_viewname='owrap') + request = self._makeRequest() + self.assertRaises(ValueError, wrapped, None, request) + + def test_as_newstyle_class_context_and_request_attr_and_renderer(self): + response = DummyResponse() + class renderer(object): + def render_view(inself, req, resp, view_inst, ctx): + self.assertEqual(req, request) + self.assertEqual(resp, {'a':'1'}) + self.assertEqual(view_inst.__class__, View) + self.assertEqual(ctx, context) + return response + def clone(self): + return self + class View(object): + def __init__(self, context, request): + pass + def index(self): + return {'a':'1'} + result = self.config._derive_view(View, + renderer=renderer(), attr='index') + self.assertFalse(result is View) + self.assertEqual(result.__module__, View.__module__) + self.assertEqual(result.__doc__, View.__doc__) + self.assertEqual(result.__name__, View.__name__) + request = self._makeRequest() + context = testing.DummyResource() + self.assertEqual(result(context, request), response) + + def test_as_newstyle_class_requestonly_attr_and_renderer(self): + response = DummyResponse() + class renderer(object): + def render_view(inself, req, resp, view_inst, ctx): + self.assertEqual(req, request) + self.assertEqual(resp, {'a':'1'}) + self.assertEqual(view_inst.__class__, View) + self.assertEqual(ctx, context) + return response + def clone(self): + return self + class View(object): + def __init__(self, request): + pass + def index(self): + return {'a':'1'} + result = self.config.derive_view(View, + renderer=renderer(), attr='index') + self.assertFalse(result is View) + self.assertEqual(result.__module__, View.__module__) + self.assertEqual(result.__doc__, View.__doc__) + self.assertEqual(result.__name__, View.__name__) + request = self._makeRequest() + context = testing.DummyResource() + self.assertEqual(result(context, request), response) + + def test_as_oldstyle_cls_context_request_attr_and_renderer(self): + response = DummyResponse() + class renderer(object): + def render_view(inself, req, resp, view_inst, ctx): + self.assertEqual(req, request) + self.assertEqual(resp, {'a':'1'}) + self.assertEqual(view_inst.__class__, View) + self.assertEqual(ctx, context) + return response + def clone(self): + return self + class View: + def __init__(self, context, request): + pass + def index(self): + return {'a':'1'} + result = self.config.derive_view(View, + renderer=renderer(), attr='index') + self.assertFalse(result is View) + self.assertEqual(result.__module__, View.__module__) + self.assertEqual(result.__doc__, View.__doc__) + self.assertEqual(result.__name__, View.__name__) + request = self._makeRequest() + context = testing.DummyResource() + self.assertEqual(result(context, request), response) + + def test_as_oldstyle_cls_requestonly_attr_and_renderer(self): + response = DummyResponse() + class renderer(object): + def render_view(inself, req, resp, view_inst, ctx): + self.assertEqual(req, request) + self.assertEqual(resp, {'a':'1'}) + self.assertEqual(view_inst.__class__, View) + self.assertEqual(ctx, context) + return response + def clone(self): + return self + class View: + def __init__(self, request): + pass + def index(self): + return {'a':'1'} + result = self.config.derive_view(View, + renderer=renderer(), attr='index') + self.assertFalse(result is View) + self.assertEqual(result.__module__, View.__module__) + self.assertEqual(result.__doc__, View.__doc__) + self.assertEqual(result.__name__, View.__name__) + request = self._makeRequest() + context = testing.DummyResource() + self.assertEqual(result(context, request), response) + + def test_as_instance_context_and_request_attr_and_renderer(self): + response = DummyResponse() + class renderer(object): + def render_view(inself, req, resp, view_inst, ctx): + self.assertEqual(req, request) + self.assertEqual(resp, {'a':'1'}) + self.assertEqual(view_inst, view) + self.assertEqual(ctx, context) + return response + def clone(self): + return self + class View: + def index(self, context, request): + return {'a':'1'} + view = View() + result = self.config.derive_view(view, + renderer=renderer(), attr='index') + self.assertFalse(result is view) + self.assertEqual(result.__module__, view.__module__) + self.assertEqual(result.__doc__, view.__doc__) + request = self._makeRequest() + context = testing.DummyResource() + self.assertEqual(result(context, request), response) + + def test_as_instance_requestonly_attr_and_renderer(self): + response = DummyResponse() + class renderer(object): + def render_view(inself, req, resp, view_inst, ctx): + self.assertEqual(req, request) + self.assertEqual(resp, {'a':'1'}) + self.assertEqual(view_inst, view) + self.assertEqual(ctx, context) + return response + def clone(self): + return self + class View: + def index(self, request): + return {'a':'1'} + view = View() + result = self.config.derive_view(view, + renderer=renderer(), attr='index') + self.assertFalse(result is view) + self.assertEqual(result.__module__, view.__module__) + self.assertEqual(result.__doc__, view.__doc__) + request = self._makeRequest() + context = testing.DummyResource() + self.assertEqual(result(context, request), response) + + def test_with_view_mapper_config_specified(self): + response = DummyResponse() + class mapper(object): + def __init__(self, **kw): + self.kw = kw + def __call__(self, view): + def wrapped(context, request): + return response + return wrapped + def view(context, request): return 'NOTOK' + result = self.config._derive_view(view, mapper=mapper) + self.assertFalse(result.__wraps__ is view) + self.assertEqual(result(None, None), response) + + def test_with_view_mapper_view_specified(self): + from pyramid.response import Response + response = Response() + def mapper(**kw): + def inner(view): + def superinner(context, request): + self.assertEqual(request, None) + return response + return superinner + return inner + def view(context, request): return 'NOTOK' + view.__view_mapper__ = mapper + result = self.config.derive_view(view) + self.assertFalse(result.__wraps__ is view) + self.assertEqual(result(None, None), response) + + def test_with_view_mapper_default_mapper_specified(self): + from pyramid.response import Response + response = Response() + def mapper(**kw): + def inner(view): + def superinner(context, request): + self.assertEqual(request, None) + return response + return superinner + return inner + self.config.set_view_mapper(mapper) + def view(context, request): return 'NOTOK' + result = self.config.derive_view(view) + self.assertFalse(result.__wraps__ is view) + self.assertEqual(result(None, None), response) + + def test_attr_wrapped_view_branching_default_phash(self): + from pyramid.config.util import DEFAULT_PHASH + def view(context, request): pass + result = self.config._derive_view(view, phash=DEFAULT_PHASH) + self.assertEqual(result.__wraps__, view) + + def test_attr_wrapped_view_branching_nondefault_phash(self): + def view(context, request): pass + result = self.config._derive_view(view, phash='nondefault') + self.assertNotEqual(result, view) + + def test_http_cached_view_integer(self): + import datetime + from pyramid.response import Response + response = Response('OK') + def inner_view(context, request): + return response + result = self.config._derive_view(inner_view, http_cache=3600) + self.assertFalse(result is inner_view) + self.assertEqual(inner_view.__module__, result.__module__) + self.assertEqual(inner_view.__doc__, result.__doc__) + request = self._makeRequest() + when = datetime.datetime.utcnow() + datetime.timedelta(hours=1) + result = result(None, request) + self.assertEqual(result, response) + headers = dict(result.headerlist) + expires = parse_httpdate(headers['Expires']) + assert_similar_datetime(expires, when) + self.assertEqual(headers['Cache-Control'], 'max-age=3600') + + def test_http_cached_view_timedelta(self): + import datetime + from pyramid.response import Response + response = Response('OK') + def inner_view(context, request): + return response + result = self.config._derive_view(inner_view, + http_cache=datetime.timedelta(hours=1)) + self.assertFalse(result is inner_view) + self.assertEqual(inner_view.__module__, result.__module__) + self.assertEqual(inner_view.__doc__, result.__doc__) + request = self._makeRequest() + when = datetime.datetime.utcnow() + datetime.timedelta(hours=1) + result = result(None, request) + self.assertEqual(result, response) + headers = dict(result.headerlist) + expires = parse_httpdate(headers['Expires']) + assert_similar_datetime(expires, when) + self.assertEqual(headers['Cache-Control'], 'max-age=3600') + + def test_http_cached_view_tuple(self): + import datetime + from pyramid.response import Response + response = Response('OK') + def inner_view(context, request): + return response + result = self.config._derive_view(inner_view, + http_cache=(3600, {'public':True})) + self.assertFalse(result is inner_view) + self.assertEqual(inner_view.__module__, result.__module__) + self.assertEqual(inner_view.__doc__, result.__doc__) + request = self._makeRequest() + when = datetime.datetime.utcnow() + datetime.timedelta(hours=1) + result = result(None, request) + self.assertEqual(result, response) + headers = dict(result.headerlist) + expires = parse_httpdate(headers['Expires']) + assert_similar_datetime(expires, when) + self.assertEqual(headers['Cache-Control'], 'max-age=3600, public') + + def test_http_cached_view_tuple_seconds_None(self): + from pyramid.response import Response + response = Response('OK') + def inner_view(context, request): + return response + result = self.config._derive_view(inner_view, + http_cache=(None, {'public':True})) + self.assertFalse(result is inner_view) + self.assertEqual(inner_view.__module__, result.__module__) + self.assertEqual(inner_view.__doc__, result.__doc__) + request = self._makeRequest() + result = result(None, request) + self.assertEqual(result, response) + headers = dict(result.headerlist) + self.assertFalse('Expires' in headers) + self.assertEqual(headers['Cache-Control'], 'public') + + def test_http_cached_view_prevent_auto_set(self): + from pyramid.response import Response + response = Response() + response.cache_control.prevent_auto = True + def inner_view(context, request): + return response + result = self.config._derive_view(inner_view, http_cache=3600) + request = self._makeRequest() + result = result(None, request) + self.assertEqual(result, response) # doesn't blow up + headers = dict(result.headerlist) + self.assertFalse('Expires' in headers) + self.assertFalse('Cache-Control' in headers) + + def test_http_cached_prevent_http_cache_in_settings(self): + self.config.registry.settings['prevent_http_cache'] = True + from pyramid.response import Response + response = Response() + def inner_view(context, request): + return response + result = self.config._derive_view(inner_view, http_cache=3600) + request = self._makeRequest() + result = result(None, request) + self.assertEqual(result, response) + headers = dict(result.headerlist) + self.assertFalse('Expires' in headers) + self.assertFalse('Cache-Control' in headers) + + def test_http_cached_view_bad_tuple(self): + def view(request): pass + self.assertRaises(ConfigurationError, self.config._derive_view, + view, http_cache=(None,)) + + def test_csrf_view_ignores_GET(self): + response = DummyResponse() + def inner_view(request): + return response + request = self._makeRequest() + request.method = 'GET' + view = self.config._derive_view(inner_view, require_csrf=True) + result = view(None, request) + self.assertTrue(result is response) + + def test_csrf_view_fails_with_bad_POST_header(self): + from pyramid.exceptions import BadCSRFToken + def inner_view(request): pass + request = self._makeRequest() + request.scheme = "http" + request.method = 'POST' + request.session = DummySession({'csrf_token': 'foo'}) + request.headers = {'X-CSRF-Token': 'bar'} + view = self.config._derive_view(inner_view, require_csrf=True) + self.assertRaises(BadCSRFToken, lambda: view(None, request)) + + def test_csrf_view_passes_with_good_POST_header(self): + response = DummyResponse() + def inner_view(request): + return response + request = self._makeRequest() + request.scheme = "http" + request.method = 'POST' + request.session = DummySession({'csrf_token': 'foo'}) + request.headers = {'X-CSRF-Token': 'foo'} + view = self.config._derive_view(inner_view, require_csrf=True) + result = view(None, request) + self.assertTrue(result is response) + + def test_csrf_view_fails_with_bad_POST_token(self): + from pyramid.exceptions import BadCSRFToken + def inner_view(request): pass + request = self._makeRequest() + request.scheme = "http" + request.method = 'POST' + request.session = DummySession({'csrf_token': 'foo'}) + request.POST = {'csrf_token': 'bar'} + view = self.config._derive_view(inner_view, require_csrf=True) + self.assertRaises(BadCSRFToken, lambda: view(None, request)) + + def test_csrf_view_passes_with_good_POST_token(self): + response = DummyResponse() + def inner_view(request): + return response + request = self._makeRequest() + request.scheme = "http" + request.method = 'POST' + request.session = DummySession({'csrf_token': 'foo'}) + request.POST = {'csrf_token': 'foo'} + view = self.config._derive_view(inner_view, require_csrf=True) + result = view(None, request) + self.assertTrue(result is response) + + def test_csrf_view_https_domain(self): + response = DummyResponse() + def inner_view(request): + return response + request = self._makeRequest() + request.scheme = "https" + request.domain = "example.com" + request.host_port = "443" + request.referrer = "https://example.com/login/" + request.method = 'POST' + request.session = DummySession({'csrf_token': 'foo'}) + request.POST = {'csrf_token': 'foo'} + view = self.config._derive_view(inner_view, require_csrf=True) + result = view(None, request) + self.assertTrue(result is response) + + def test_csrf_view_fails_on_bad_PUT_header(self): + from pyramid.exceptions import BadCSRFToken + def inner_view(request): pass + request = self._makeRequest() + request.scheme = "http" + request.method = 'PUT' + request.session = DummySession({'csrf_token': 'foo'}) + request.headers = {'X-CSRF-Token': 'bar'} + view = self.config._derive_view(inner_view, require_csrf=True) + self.assertRaises(BadCSRFToken, lambda: view(None, request)) + + def test_csrf_view_fails_on_bad_referrer(self): + from pyramid.exceptions import BadCSRFOrigin + def inner_view(request): pass + request = self._makeRequest() + request.method = "POST" + request.scheme = "https" + request.host_port = "443" + request.domain = "example.com" + request.referrer = "https://not-example.com/evil/" + request.registry.settings = {} + view = self.config._derive_view(inner_view, require_csrf=True) + self.assertRaises(BadCSRFOrigin, lambda: view(None, request)) + + def test_csrf_view_fails_on_bad_origin(self): + from pyramid.exceptions import BadCSRFOrigin + def inner_view(request): pass + request = self._makeRequest() + request.method = "POST" + request.scheme = "https" + request.host_port = "443" + request.domain = "example.com" + request.headers = {"Origin": "https://not-example.com/evil/"} + request.registry.settings = {} + view = self.config._derive_view(inner_view, require_csrf=True) + self.assertRaises(BadCSRFOrigin, lambda: view(None, request)) + + def test_csrf_view_enabled_by_default(self): + from pyramid.exceptions import BadCSRFToken + def inner_view(request): pass + request = self._makeRequest() + request.scheme = "http" + request.method = 'POST' + request.session = DummySession({'csrf_token': 'foo'}) + self.config.set_default_csrf_options(require_csrf=True) + view = self.config._derive_view(inner_view) + self.assertRaises(BadCSRFToken, lambda: view(None, request)) + + def test_csrf_view_enabled_via_callback(self): + def callback(request): + return True + from pyramid.exceptions import BadCSRFToken + def inner_view(request): pass + request = self._makeRequest() + request.scheme = "http" + request.method = 'POST' + request.session = DummySession({'csrf_token': 'foo'}) + self.config.set_default_csrf_options(require_csrf=True, callback=callback) + view = self.config._derive_view(inner_view) + self.assertRaises(BadCSRFToken, lambda: view(None, request)) + + def test_csrf_view_disabled_via_callback(self): + def callback(request): + return False + response = DummyResponse() + def inner_view(request): + return response + request = self._makeRequest() + request.scheme = "http" + request.method = 'POST' + request.session = DummySession({'csrf_token': 'foo'}) + self.config.set_default_csrf_options(require_csrf=True, callback=callback) + view = self.config._derive_view(inner_view) + result = view(None, request) + self.assertTrue(result is response) + + def test_csrf_view_uses_custom_csrf_token(self): + response = DummyResponse() + def inner_view(request): + return response + request = self._makeRequest() + request.scheme = "http" + request.method = 'POST' + request.session = DummySession({'csrf_token': 'foo'}) + request.POST = {'DUMMY': 'foo'} + self.config.set_default_csrf_options(require_csrf=True, token='DUMMY') + view = self.config._derive_view(inner_view) + result = view(None, request) + self.assertTrue(result is response) + + def test_csrf_view_uses_custom_csrf_header(self): + response = DummyResponse() + def inner_view(request): + return response + request = self._makeRequest() + request.scheme = "http" + request.method = 'POST' + request.session = DummySession({'csrf_token': 'foo'}) + request.headers = {'DUMMY': 'foo'} + self.config.set_default_csrf_options(require_csrf=True, header='DUMMY') + view = self.config._derive_view(inner_view) + result = view(None, request) + self.assertTrue(result is response) + + def test_csrf_view_uses_custom_methods(self): + response = DummyResponse() + def inner_view(request): + return response + request = self._makeRequest() + request.scheme = "http" + request.method = 'PUT' + request.session = DummySession({'csrf_token': 'foo'}) + self.config.set_default_csrf_options( + require_csrf=True, safe_methods=['PUT']) + view = self.config._derive_view(inner_view) + result = view(None, request) + self.assertTrue(result is response) + + def test_csrf_view_uses_view_option_override(self): + response = DummyResponse() + def inner_view(request): + return response + request = self._makeRequest() + request.scheme = "http" + request.method = 'POST' + request.session = DummySession({'csrf_token': 'foo'}) + request.POST = {'csrf_token': 'bar'} + self.config.set_default_csrf_options(require_csrf=True) + view = self.config._derive_view(inner_view, require_csrf=False) + result = view(None, request) + self.assertTrue(result is response) + + def test_csrf_view_skipped_by_default_on_exception_view(self): + from pyramid.request import Request + def view(request): + raise ValueError + def excview(request): + return 'hello' + self.config.set_default_csrf_options(require_csrf=True) + self.config.set_session_factory( + lambda request: DummySession({'csrf_token': 'foo'})) + self.config.add_view(view, name='foo', require_csrf=False) + self.config.add_view(excview, context=ValueError, renderer='string') + app = self.config.make_wsgi_app() + request = Request.blank('/foo', base_url='http://example.com') + request.method = 'POST' + response = request.get_response(app) + self.assertTrue(b'hello' in response.body) + + def test_csrf_view_failed_on_explicit_exception_view(self): + from pyramid.exceptions import BadCSRFToken + from pyramid.request import Request + def view(request): + raise ValueError + def excview(request): pass + self.config.set_default_csrf_options(require_csrf=True) + self.config.set_session_factory( + lambda request: DummySession({'csrf_token': 'foo'})) + self.config.add_view(view, name='foo', require_csrf=False) + self.config.add_view(excview, context=ValueError, renderer='string', + require_csrf=True) + app = self.config.make_wsgi_app() + request = Request.blank('/foo', base_url='http://example.com') + request.method = 'POST' + try: + request.get_response(app) + except BadCSRFToken: + pass + else: # pragma: no cover + raise AssertionError + + def test_csrf_view_passed_on_explicit_exception_view(self): + from pyramid.request import Request + def view(request): + raise ValueError + def excview(request): + return 'hello' + self.config.set_default_csrf_options(require_csrf=True) + self.config.set_session_factory( + lambda request: DummySession({'csrf_token': 'foo'})) + self.config.add_view(view, name='foo', require_csrf=False) + self.config.add_view(excview, context=ValueError, renderer='string', + require_csrf=True) + app = self.config.make_wsgi_app() + request = Request.blank('/foo', base_url='http://example.com') + request.method = 'POST' + request.headers['X-CSRF-Token'] = 'foo' + response = request.get_response(app) + self.assertTrue(b'hello' in response.body) + + +class TestDerivationOrder(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + self.config = None + testing.tearDown() + + def test_right_order_user_sorted(self): + from pyramid.interfaces import IViewDerivers + + self.config.add_view_deriver(None, 'deriv1') + self.config.add_view_deriver(None, 'deriv2', 'decorated_view', 'deriv1') + self.config.add_view_deriver(None, 'deriv3', 'deriv2', 'deriv1') + + derivers = self.config.registry.getUtility(IViewDerivers) + derivers_sorted = derivers.sorted() + dlist = [d for (d, _) in derivers_sorted] + self.assertEqual([ + 'secured_view', + 'csrf_view', + 'owrapped_view', + 'http_cached_view', + 'decorated_view', + 'deriv2', + 'deriv3', + 'deriv1', + 'rendered_view', + 'mapped_view', + ], dlist) + + def test_right_order_implicit(self): + from pyramid.interfaces import IViewDerivers + + self.config.add_view_deriver(None, 'deriv1') + self.config.add_view_deriver(None, 'deriv2') + self.config.add_view_deriver(None, 'deriv3') + + derivers = self.config.registry.getUtility(IViewDerivers) + derivers_sorted = derivers.sorted() + dlist = [d for (d, _) in derivers_sorted] + self.assertEqual([ + 'secured_view', + 'csrf_view', + 'owrapped_view', + 'http_cached_view', + 'decorated_view', + 'deriv3', + 'deriv2', + 'deriv1', + 'rendered_view', + 'mapped_view', + ], dlist) + + def test_right_order_under_rendered_view(self): + from pyramid.interfaces import IViewDerivers + + self.config.add_view_deriver(None, 'deriv1', 'rendered_view', 'mapped_view') + + derivers = self.config.registry.getUtility(IViewDerivers) + derivers_sorted = derivers.sorted() + dlist = [d for (d, _) in derivers_sorted] + self.assertEqual([ + 'secured_view', + 'csrf_view', + 'owrapped_view', + 'http_cached_view', + 'decorated_view', + 'rendered_view', + 'deriv1', + 'mapped_view', + ], dlist) + + + def test_right_order_under_rendered_view_others(self): + from pyramid.interfaces import IViewDerivers + + self.config.add_view_deriver(None, 'deriv1', 'rendered_view', 'mapped_view') + self.config.add_view_deriver(None, 'deriv2') + self.config.add_view_deriver(None, 'deriv3') + + derivers = self.config.registry.getUtility(IViewDerivers) + derivers_sorted = derivers.sorted() + dlist = [d for (d, _) in derivers_sorted] + self.assertEqual([ + 'secured_view', + 'csrf_view', + 'owrapped_view', + 'http_cached_view', + 'decorated_view', + 'deriv3', + 'deriv2', + 'rendered_view', + 'deriv1', + 'mapped_view', + ], dlist) + + +class TestAddDeriver(unittest.TestCase): + + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + self.config = None + testing.tearDown() + + def test_add_single_deriver(self): + response = DummyResponse() + response.deriv = False + view = lambda *arg: response + + def deriv(view, info): + self.assertFalse(response.deriv) + response.deriv = True + return view + + result = self.config._derive_view(view) + self.assertFalse(response.deriv) + self.config.add_view_deriver(deriv, 'test_deriv') + + result = self.config._derive_view(view) + self.assertTrue(response.deriv) + + def test_override_deriver(self): + flags = {} + + class AView: + def __init__(self): + self.response = DummyResponse() + + def deriv1(view, info): + flags['deriv1'] = True + return view + + def deriv2(view, info): + flags['deriv2'] = True + return view + + view1 = AView() + self.config.add_view_deriver(deriv1, 'test_deriv') + result = self.config._derive_view(view1) + self.assertTrue(flags.get('deriv1')) + self.assertFalse(flags.get('deriv2')) + + flags.clear() + view2 = AView() + self.config.add_view_deriver(deriv2, 'test_deriv') + result = self.config._derive_view(view2) + self.assertFalse(flags.get('deriv1')) + self.assertTrue(flags.get('deriv2')) + + def test_override_mapped_view(self): + from pyramid.viewderivers import VIEW + response = DummyResponse() + view = lambda *arg: response + flags = {} + + def deriv1(view, info): + flags['deriv1'] = True + return view + + result = self.config._derive_view(view) + self.assertFalse(flags.get('deriv1')) + + flags.clear() + self.config.add_view_deriver( + deriv1, name='mapped_view', under='rendered_view', over=VIEW) + result = self.config._derive_view(view) + self.assertTrue(flags.get('deriv1')) + + def test_add_multi_derivers_ordered(self): + from pyramid.viewderivers import INGRESS + response = DummyResponse() + view = lambda *arg: response + response.deriv = [] + + def deriv1(view, info): + response.deriv.append('deriv1') + return view + + def deriv2(view, info): + response.deriv.append('deriv2') + return view + + def deriv3(view, info): + response.deriv.append('deriv3') + return view + + self.config.add_view_deriver(deriv1, 'deriv1') + self.config.add_view_deriver(deriv2, 'deriv2', INGRESS, 'deriv1') + self.config.add_view_deriver(deriv3, 'deriv3', 'deriv2', 'deriv1') + result = self.config._derive_view(view) + self.assertEqual(response.deriv, ['deriv1', 'deriv3', 'deriv2']) + + def test_add_deriver_without_name(self): + from pyramid.interfaces import IViewDerivers + def deriv1(view, info): pass + self.config.add_view_deriver(deriv1) + derivers = self.config.registry.getUtility(IViewDerivers) + self.assertTrue('deriv1' in derivers.names) + + def test_add_deriver_reserves_ingress(self): + from pyramid.exceptions import ConfigurationError + from pyramid.viewderivers import INGRESS + def deriv1(view, info): pass + self.assertRaises( + ConfigurationError, self.config.add_view_deriver, deriv1, INGRESS) + + def test_add_deriver_enforces_ingress_is_first(self): + from pyramid.exceptions import ConfigurationError + from pyramid.viewderivers import INGRESS + def deriv1(view, info): pass + try: + self.config.add_view_deriver(deriv1, over=INGRESS) + except ConfigurationError as ex: + self.assertTrue('cannot be over INGRESS' in ex.args[0]) + else: # pragma: no cover + raise AssertionError + + def test_add_deriver_enforces_view_is_last(self): + from pyramid.exceptions import ConfigurationError + from pyramid.viewderivers import VIEW + def deriv1(view, info): pass + try: + self.config.add_view_deriver(deriv1, under=VIEW) + except ConfigurationError as ex: + self.assertTrue('cannot be under VIEW' in ex.args[0]) + else: # pragma: no cover + raise AssertionError + + def test_add_deriver_enforces_mapped_view_is_last(self): + from pyramid.exceptions import ConfigurationError + def deriv1(view, info): pass + try: + self.config.add_view_deriver(deriv1, 'deriv1', under='mapped_view') + except ConfigurationError as ex: + self.assertTrue('cannot be under "mapped_view"' in ex.args[0]) + else: # pragma: no cover + raise AssertionError + + +class TestDeriverIntegration(unittest.TestCase): + def setUp(self): + self.config = testing.setUp() + + def tearDown(self): + self.config = None + testing.tearDown() + + def _getViewCallable(self, config, ctx_iface=None, request_iface=None, + name=''): + from zope.interface import Interface + from pyramid.interfaces import IRequest + from pyramid.interfaces import IView + from pyramid.interfaces import IViewClassifier + classifier = IViewClassifier + if ctx_iface is None: + ctx_iface = Interface + if request_iface is None: + request_iface = IRequest + return config.registry.adapters.lookup( + (classifier, request_iface, ctx_iface), IView, name=name, + default=None) + + def _makeRequest(self, config): + request = DummyRequest() + request.registry = config.registry + return request + + def test_view_options(self): + response = DummyResponse() + view = lambda *arg: response + response.deriv = [] + + def deriv1(view, info): + response.deriv.append(info.options['deriv1']) + return view + deriv1.options = ('deriv1',) + + def deriv2(view, info): + response.deriv.append(info.options['deriv2']) + return view + deriv2.options = ('deriv2',) + + self.config.add_view_deriver(deriv1, 'deriv1') + self.config.add_view_deriver(deriv2, 'deriv2') + self.config.add_view(view, deriv1='test1', deriv2='test2') + + wrapper = self._getViewCallable(self.config) + request = self._makeRequest(self.config) + request.method = 'GET' + self.assertEqual(wrapper(None, request), response) + self.assertEqual(['test1', 'test2'], response.deriv) + + def test_unexpected_view_options(self): + from pyramid.exceptions import ConfigurationError + def deriv1(view, info): pass + self.config.add_view_deriver(deriv1, 'deriv1') + self.assertRaises( + ConfigurationError, + lambda: self.config.add_view(lambda r: {}, deriv1='test1')) + +@implementer(IResponse) +class DummyResponse(object): + content_type = None + default_content_type = None + body = None + +class DummyRequest: + subpath = () + matchdict = None + request_iface = IRequest + + def __init__(self, environ=None): + if environ is None: + environ = {} + self.environ = environ + self.params = {} + self.POST = {} + self.cookies = {} + self.headers = {} + self.response = DummyResponse() + +class DummyLogger: + def __init__(self): + self.messages = [] + def info(self, msg): + self.messages.append(msg) + warn = info + debug = info + +class DummySecurityPolicy: + def __init__(self, permitted=True): + self.permitted = permitted + + def effective_principals(self, request): + return [] + + def permits(self, context, principals, permission): + return self.permitted + +class DummySession(dict): + def get_csrf_token(self): + return self['csrf_token'] + +def parse_httpdate(s): + import datetime + # cannot use %Z, must use literal GMT; Jython honors timezone + # but CPython does not + return datetime.datetime.strptime(s, "%a, %d %b %Y %H:%M:%S GMT") + +def assert_similar_datetime(one, two): + for attr in ('year', 'month', 'day', 'hour', 'minute'): + one_attr = getattr(one, attr) + two_attr = getattr(two, attr) + if not one_attr == two_attr: # pragma: no cover + raise AssertionError('%r != %r in %s' % (one_attr, two_attr, attr)) diff --git a/src/pyramid/tests/test_wsgi.py b/src/pyramid/tests/test_wsgi.py new file mode 100644 index 000000000..4ddbc9201 --- /dev/null +++ b/src/pyramid/tests/test_wsgi.py @@ -0,0 +1,130 @@ +import unittest + +class WSGIAppTests(unittest.TestCase): + def _callFUT(self, app): + from pyramid.wsgi import wsgiapp + return wsgiapp(app) + + def test_wsgiapp_none(self): + self.assertRaises(ValueError, self._callFUT, None) + + def test_decorator(self): + context = DummyContext() + request = DummyRequest() + decorator = self._callFUT(dummyapp) + response = decorator(context, request) + self.assertEqual(response, dummyapp) + + def test_decorator_object_instance(self): + context = DummyContext() + request = DummyRequest() + app = DummyApp() + decorator = self._callFUT(app) + response = decorator(context, request) + self.assertEqual(response, app) + +class WSGIApp2Tests(unittest.TestCase): + def _callFUT(self, app): + from pyramid.wsgi import wsgiapp2 + return wsgiapp2(app) + + def test_wsgiapp2_none(self): + self.assertRaises(ValueError, self._callFUT, None) + + def test_decorator_with_subpath_and_view_name(self): + context = DummyContext() + request = DummyRequest() + request.subpath = ('subpath',) + request.environ = {'SCRIPT_NAME':'/foo', + 'PATH_INFO':'/b/view_name/subpath'} + decorator = self._callFUT(dummyapp) + response = decorator(context, request) + self.assertEqual(response, dummyapp) + self.assertEqual(request.environ['PATH_INFO'], '/subpath') + self.assertEqual(request.environ['SCRIPT_NAME'], '/foo/b/view_name') + + def test_decorator_with_subpath_no_view_name(self): + context = DummyContext() + request = DummyRequest() + request.subpath = ('subpath',) + request.environ = {'SCRIPT_NAME':'/foo', 'PATH_INFO':'/b/subpath'} + decorator = self._callFUT(dummyapp) + response = decorator(context, request) + self.assertEqual(response, dummyapp) + self.assertEqual(request.environ['PATH_INFO'], '/subpath') + self.assertEqual(request.environ['SCRIPT_NAME'], '/foo/b') + + def test_decorator_no_subpath_with_view_name(self): + context = DummyContext() + request = DummyRequest() + request.subpath = () + request.environ = {'SCRIPT_NAME':'/foo', 'PATH_INFO':'/b/view_name'} + decorator = self._callFUT(dummyapp) + response = decorator(context, request) + self.assertEqual(response, dummyapp) + self.assertEqual(request.environ['PATH_INFO'], '/') + self.assertEqual(request.environ['SCRIPT_NAME'], '/foo/b/view_name') + + def test_decorator_traversed_empty_with_view_name(self): + context = DummyContext() + request = DummyRequest() + request.subpath = () + request.environ = {'SCRIPT_NAME':'/foo', 'PATH_INFO':'/view_name'} + decorator = self._callFUT(dummyapp) + response = decorator(context, request) + self.assertEqual(response, dummyapp) + self.assertEqual(request.environ['PATH_INFO'], '/') + self.assertEqual(request.environ['SCRIPT_NAME'], '/foo/view_name') + + def test_decorator_traversed_empty_no_view_name(self): + context = DummyContext() + request = DummyRequest() + request.subpath = () + request.environ = {'SCRIPT_NAME':'/foo', 'PATH_INFO':'/'} + decorator = self._callFUT(dummyapp) + response = decorator(context, request) + self.assertEqual(response, dummyapp) + self.assertEqual(request.environ['PATH_INFO'], '/') + self.assertEqual(request.environ['SCRIPT_NAME'], '/foo') + + def test_decorator_traversed_empty_no_view_name_no_script_name(self): + context = DummyContext() + request = DummyRequest() + request.subpath = () + request.environ = {'SCRIPT_NAME':'', 'PATH_INFO':'/'} + decorator = self._callFUT(dummyapp) + response = decorator(context, request) + self.assertEqual(response, dummyapp) + self.assertEqual(request.environ['PATH_INFO'], '/') + self.assertEqual(request.environ['SCRIPT_NAME'], '') + + def test_decorator_on_callable_object_instance(self): + context = DummyContext() + request = DummyRequest() + request.subpath = () + request.environ = {'SCRIPT_NAME':'/foo', 'PATH_INFO':'/'} + app = DummyApp() + decorator = self._callFUT(app) + response = decorator(context, request) + self.assertEqual(response, app) + self.assertEqual(request.environ['PATH_INFO'], '/') + self.assertEqual(request.environ['SCRIPT_NAME'], '/foo') + +def dummyapp(environ, start_response): + """ """ + +class DummyApp(object): + def __call__(self, environ, start_response): + """ """ + +class DummyContext: + pass + +class DummyRequest: + def get_response(self, application): + return application + + def copy(self): + self.copied = True + return self + diff --git a/src/pyramid/threadlocal.py b/src/pyramid/threadlocal.py new file mode 100644 index 000000000..e8f825715 --- /dev/null +++ b/src/pyramid/threadlocal.py @@ -0,0 +1,83 @@ +import threading + +from pyramid.registry import global_registry + +class ThreadLocalManager(threading.local): + def __init__(self, default=None): + # http://code.google.com/p/google-app-engine-django/issues/detail?id=119 + # we *must* use a keyword argument for ``default`` here instead + # of a positional argument to work around a bug in the + # implementation of _threading_local.local in Python, which is + # used by GAE instead of _thread.local + self.stack = [] + self.default = default + + def push(self, info): + self.stack.append(info) + + set = push # b/c + + def pop(self): + if self.stack: + return self.stack.pop() + + def get(self): + try: + return self.stack[-1] + except IndexError: + return self.default() + + def clear(self): + self.stack[:] = [] + +def defaults(): + return {'request': None, 'registry': global_registry} + +manager = ThreadLocalManager(default=defaults) + +def get_current_request(): + """ + Return the currently active request or ``None`` if no request + is currently active. + + This function should be used *extremely sparingly*, usually only + in unit testing code. It's almost always usually a mistake to use + ``get_current_request`` outside a testing context because its + usage makes it possible to write code that can be neither easily + tested nor scripted. + + """ + return manager.get()['request'] + +def get_current_registry(context=None): # context required by getSiteManager API + """ + Return the currently active :term:`application registry` or the + global application registry if no request is currently active. + + This function should be used *extremely sparingly*, usually only + in unit testing code. It's almost always usually a mistake to use + ``get_current_registry`` outside a testing context because its + usage makes it possible to write code that can be neither easily + tested nor scripted. + + """ + return manager.get()['registry'] + +class RequestContext(object): + def __init__(self, request): + self.request = request + + def begin(self): + request = self.request + registry = request.registry + manager.push({'registry': registry, 'request': request}) + return request + + def end(self): + manager.pop() + + def __enter__(self): + return self.begin() + + def __exit__(self, *args): + self.end() diff --git a/src/pyramid/traversal.py b/src/pyramid/traversal.py new file mode 100644 index 000000000..d8f4690fd --- /dev/null +++ b/src/pyramid/traversal.py @@ -0,0 +1,760 @@ +from zope.interface import implementer +from zope.interface.interfaces import IInterface + +from pyramid.interfaces import ( + IResourceURL, + IRequestFactory, + ITraverser, + VH_ROOT_KEY, + ) + +from pyramid.compat import ( + PY2, + native_, + text_, + ascii_native_, + text_type, + binary_type, + is_nonstr_iter, + decode_path_info, + unquote_bytes_to_wsgi, + lru_cache, + ) + +from pyramid.encode import url_quote +from pyramid.exceptions import URLDecodeError +from pyramid.location import lineage +from pyramid.threadlocal import get_current_registry + +PATH_SEGMENT_SAFE = "~!$&'()*+,;=:@" # from webob +PATH_SAFE = PATH_SEGMENT_SAFE + "/" + +empty = text_('') + +def find_root(resource): + """ Find the root node in the resource tree to which ``resource`` + belongs. Note that ``resource`` should be :term:`location`-aware. + Note that the root resource is available in the request object by + accessing the ``request.root`` attribute. + """ + for location in lineage(resource): + if location.__parent__ is None: + resource = location + break + return resource + +def find_resource(resource, path): + """ Given a resource object and a string or tuple representing a path + (such as the return value of :func:`pyramid.traversal.resource_path` or + :func:`pyramid.traversal.resource_path_tuple`), return a resource in this + application's resource tree at the specified path. The resource passed + in *must* be :term:`location`-aware. If the path cannot be resolved (if + the respective node in the resource tree does not exist), a + :exc:`KeyError` will be raised. + + This function is the logical inverse of + :func:`pyramid.traversal.resource_path` and + :func:`pyramid.traversal.resource_path_tuple`; it can resolve any + path string or tuple generated by either of those functions. + + Rules for passing a *string* as the ``path`` argument: if the + first character in the path string is the ``/`` + character, the path is considered absolute and the resource tree + traversal will start at the root resource. If the first character + of the path string is *not* the ``/`` character, the path is + considered relative and resource tree traversal will begin at the resource + object supplied to the function as the ``resource`` argument. If an + empty string is passed as ``path``, the ``resource`` passed in will + be returned. Resource path strings must be escaped in the following + manner: each Unicode path segment must be encoded as UTF-8 and as + each path segment must escaped via Python's :mod:`urllib.quote`. + For example, ``/path/to%20the/La%20Pe%C3%B1a`` (absolute) or + ``to%20the/La%20Pe%C3%B1a`` (relative). The + :func:`pyramid.traversal.resource_path` function generates strings + which follow these rules (albeit only absolute ones). + + Rules for passing *text* (Unicode) as the ``path`` argument are the same + as those for a string. In particular, the text may not have any nonascii + characters in it. + + Rules for passing a *tuple* as the ``path`` argument: if the first + element in the path tuple is the empty string (for example ``('', + 'a', 'b', 'c')``, the path is considered absolute and the resource tree + traversal will start at the resource tree root object. If the first + element in the path tuple is not the empty string (for example + ``('a', 'b', 'c')``), the path is considered relative and resource tree + traversal will begin at the resource object supplied to the function + as the ``resource`` argument. If an empty sequence is passed as + ``path``, the ``resource`` passed in itself will be returned. No + URL-quoting or UTF-8-encoding of individual path segments within + the tuple is required (each segment may be any string or unicode + object representing a resource name). Resource path tuples generated by + :func:`pyramid.traversal.resource_path_tuple` can always be + resolved by ``find_resource``. + """ + if isinstance(path, text_type): + path = ascii_native_(path) + D = traverse(resource, path) + view_name = D['view_name'] + context = D['context'] + if view_name: + raise KeyError('%r has no subelement %s' % (context, view_name)) + return context + +find_model = find_resource # b/w compat (forever) + +def find_interface(resource, class_or_interface): + """ + Return the first resource found in the :term:`lineage` of ``resource`` + which, a) if ``class_or_interface`` is a Python class object, is an + instance of the class or any subclass of that class or b) if + ``class_or_interface`` is a :term:`interface`, provides the specified + interface. Return ``None`` if no resource providing ``interface_or_class`` + can be found in the lineage. The ``resource`` passed in *must* be + :term:`location`-aware. + """ + if IInterface.providedBy(class_or_interface): + test = class_or_interface.providedBy + else: + test = lambda arg: isinstance(arg, class_or_interface) + for location in lineage(resource): + if test(location): + return location + +def resource_path(resource, *elements): + """ Return a string object representing the absolute physical path of the + resource object based on its position in the resource tree, e.g + ``/foo/bar``. Any positional arguments passed in as ``elements`` will be + appended as path segments to the end of the resource path. For instance, + if the resource's path is ``/foo/bar`` and ``elements`` equals ``('a', + 'b')``, the returned string will be ``/foo/bar/a/b``. The first + character in the string will always be the ``/`` character (a leading + ``/`` character in a path string represents that the path is absolute). + + Resource path strings returned will be escaped in the following + manner: each unicode path segment will be encoded as UTF-8 and + each path segment will be escaped via Python's :mod:`urllib.quote`. + For example, ``/path/to%20the/La%20Pe%C3%B1a``. + + This function is a logical inverse of + :mod:`pyramid.traversal.find_resource`: it can be used to generate + path references that can later be resolved via that function. + + The ``resource`` passed in *must* be :term:`location`-aware. + + .. note:: + + Each segment in the path string returned will use the ``__name__`` + attribute of the resource it represents within the resource tree. Each + of these segments *should* be a unicode or string object (as per the + contract of :term:`location`-awareness). However, no conversion or + safety checking of resource names is performed. For instance, if one of + the resources in your tree has a ``__name__`` which (by error) is a + dictionary, the :func:`pyramid.traversal.resource_path` function will + attempt to append it to a string and it will cause a + :exc:`pyramid.exceptions.URLDecodeError`. + + .. note:: + + The :term:`root` resource *must* have a ``__name__`` attribute with a + value of either ``None`` or the empty string for paths to be generated + properly. If the root resource has a non-null ``__name__`` attribute, + its name will be prepended to the generated path rather than a single + leading '/' character. + """ + # joining strings is a bit expensive so we delegate to a function + # which caches the joined result for us + return _join_path_tuple(resource_path_tuple(resource, *elements)) + +model_path = resource_path # b/w compat (forever) + +def traverse(resource, path): + """Given a resource object as ``resource`` and a string or tuple + representing a path as ``path`` (such as the return value of + :func:`pyramid.traversal.resource_path` or + :func:`pyramid.traversal.resource_path_tuple` or the value of + ``request.environ['PATH_INFO']``), return a dictionary with the + keys ``context``, ``root``, ``view_name``, ``subpath``, + ``traversed``, ``virtual_root``, and ``virtual_root_path``. + + A definition of each value in the returned dictionary: + + - ``context``: The :term:`context` (a :term:`resource` object) found + via traversal or url dispatch. If the ``path`` passed in is the + empty string, the value of the ``resource`` argument passed to this + function is returned. + + - ``root``: The resource object at which :term:`traversal` begins. + If the ``resource`` passed in was found via url dispatch or if the + ``path`` passed in was relative (non-absolute), the value of the + ``resource`` argument passed to this function is returned. + + - ``view_name``: The :term:`view name` found during + :term:`traversal` or :term:`url dispatch`; if the ``resource`` was + found via traversal, this is usually a representation of the + path segment which directly follows the path to the ``context`` + in the ``path``. The ``view_name`` will be a Unicode object or + the empty string. The ``view_name`` will be the empty string if + there is no element which follows the ``context`` path. An + example: if the path passed is ``/foo/bar``, and a resource + object is found at ``/foo`` (but not at ``/foo/bar``), the 'view + name' will be ``u'bar'``. If the ``resource`` was found via + urldispatch, the view_name will be the name the route found was + registered with. + + - ``subpath``: For a ``resource`` found via :term:`traversal`, this + is a sequence of path segments found in the ``path`` that follow + the ``view_name`` (if any). Each of these items is a Unicode + object. If no path segments follow the ``view_name``, the + subpath will be the empty sequence. An example: if the path + passed is ``/foo/bar/baz/buz``, and a resource object is found at + ``/foo`` (but not ``/foo/bar``), the 'view name' will be + ``u'bar'`` and the :term:`subpath` will be ``[u'baz', u'buz']``. + For a ``resource`` found via url dispatch, the subpath will be a + sequence of values discerned from ``*subpath`` in the route + pattern matched or the empty sequence. + + - ``traversed``: The sequence of path elements traversed from the + root to find the ``context`` object during :term:`traversal`. + Each of these items is a Unicode object. If no path segments + were traversed to find the ``context`` object (e.g. if the + ``path`` provided is the empty string), the ``traversed`` value + will be the empty sequence. If the ``resource`` is a resource found + via :term:`url dispatch`, traversed will be None. + + - ``virtual_root``: A resource object representing the 'virtual' root + of the resource tree being traversed during :term:`traversal`. + See :ref:`vhosting_chapter` for a definition of the virtual root + object. If no virtual hosting is in effect, and the ``path`` + passed in was absolute, the ``virtual_root`` will be the + *physical* root resource object (the object at which :term:`traversal` + begins). If the ``resource`` passed in was found via :term:`URL + dispatch` or if the ``path`` passed in was relative, the + ``virtual_root`` will always equal the ``root`` object (the + resource passed in). + + - ``virtual_root_path`` -- If :term:`traversal` was used to find + the ``resource``, this will be the sequence of path elements + traversed to find the ``virtual_root`` resource. Each of these + items is a Unicode object. If no path segments were traversed + to find the ``virtual_root`` resource (e.g. if virtual hosting is + not in effect), the ``traversed`` value will be the empty list. + If url dispatch was used to find the ``resource``, this will be + ``None``. + + If the path cannot be resolved, a :exc:`KeyError` will be raised. + + Rules for passing a *string* as the ``path`` argument: if the + first character in the path string is the with the ``/`` + character, the path will considered absolute and the resource tree + traversal will start at the root resource. If the first character + of the path string is *not* the ``/`` character, the path is + considered relative and resource tree traversal will begin at the resource + object supplied to the function as the ``resource`` argument. If an + empty string is passed as ``path``, the ``resource`` passed in will + be returned. Resource path strings must be escaped in the following + manner: each Unicode path segment must be encoded as UTF-8 and + each path segment must escaped via Python's :mod:`urllib.quote`. + For example, ``/path/to%20the/La%20Pe%C3%B1a`` (absolute) or + ``to%20the/La%20Pe%C3%B1a`` (relative). The + :func:`pyramid.traversal.resource_path` function generates strings + which follow these rules (albeit only absolute ones). + + Rules for passing a *tuple* as the ``path`` argument: if the first + element in the path tuple is the empty string (for example ``('', + 'a', 'b', 'c')``, the path is considered absolute and the resource tree + traversal will start at the resource tree root object. If the first + element in the path tuple is not the empty string (for example + ``('a', 'b', 'c')``), the path is considered relative and resource tree + traversal will begin at the resource object supplied to the function + as the ``resource`` argument. If an empty sequence is passed as + ``path``, the ``resource`` passed in itself will be returned. No + URL-quoting or UTF-8-encoding of individual path segments within + the tuple is required (each segment may be any string or unicode + object representing a resource name). + + Explanation of the conversion of ``path`` segment values to + Unicode during traversal: Each segment is URL-unquoted, and + decoded into Unicode. Each segment is assumed to be encoded using + the UTF-8 encoding (or a subset, such as ASCII); a + :exc:`pyramid.exceptions.URLDecodeError` is raised if a segment + cannot be decoded. If a segment name is empty or if it is ``.``, + it is ignored. If a segment name is ``..``, the previous segment + is deleted, and the ``..`` is ignored. As a result of this + process, the return values ``view_name``, each element in the + ``subpath``, each element in ``traversed``, and each element in + the ``virtual_root_path`` will be Unicode as opposed to a string, + and will be URL-decoded. + """ + + if is_nonstr_iter(path): + # the traverser factory expects PATH_INFO to be a string, not + # unicode and it expects path segments to be utf-8 and + # urlencoded (it's the same traverser which accepts PATH_INFO + # from user agents; user agents always send strings). + if path: + path = _join_path_tuple(tuple(path)) + else: + path = '' + + # The user is supposed to pass us a string object, never Unicode. In + # practice, however, users indeed pass Unicode to this API. If they do + # pass a Unicode object, its data *must* be entirely encodeable to ASCII, + # so we encode it here as a convenience to the user and to prevent + # second-order failures from cropping up (all failures will occur at this + # step rather than later down the line as the result of calling + # ``traversal_path``). + + path = ascii_native_(path) + + if path and path[0] == '/': + resource = find_root(resource) + + reg = get_current_registry() + + request_factory = reg.queryUtility(IRequestFactory) + if request_factory is None: + from pyramid.request import Request # avoid circdep + request_factory = Request + + request = request_factory.blank(path) + request.registry = reg + traverser = reg.queryAdapter(resource, ITraverser) + if traverser is None: + traverser = ResourceTreeTraverser(resource) + + return traverser(request) + +def resource_path_tuple(resource, *elements): + """ + Return a tuple representing the absolute physical path of the + ``resource`` object based on its position in a resource tree, e.g + ``('', 'foo', 'bar')``. Any positional arguments passed in as + ``elements`` will be appended as elements in the tuple + representing the resource path. For instance, if the resource's + path is ``('', 'foo', 'bar')`` and elements equals ``('a', 'b')``, + the returned tuple will be ``('', 'foo', 'bar', 'a', 'b')``. The + first element of this tuple will always be the empty string (a + leading empty string element in a path tuple represents that the + path is absolute). + + This function is a logical inverse of + :func:`pyramid.traversal.find_resource`: it can be used to + generate path references that can later be resolved by that function. + + The ``resource`` passed in *must* be :term:`location`-aware. + + .. note:: + + Each segment in the path tuple returned will equal the ``__name__`` + attribute of the resource it represents within the resource tree. Each + of these segments *should* be a unicode or string object (as per the + contract of :term:`location`-awareness). However, no conversion or + safety checking of resource names is performed. For instance, if one of + the resources in your tree has a ``__name__`` which (by error) is a + dictionary, that dictionary will be placed in the path tuple; no warning + or error will be given. + + .. note:: + + The :term:`root` resource *must* have a ``__name__`` attribute with a + value of either ``None`` or the empty string for path tuples to be + generated properly. If the root resource has a non-null ``__name__`` + attribute, its name will be the first element in the generated path tuple + rather than the empty string. + """ + return tuple(_resource_path_list(resource, *elements)) + +model_path_tuple = resource_path_tuple # b/w compat (forever) + +def _resource_path_list(resource, *elements): + """ Implementation detail shared by resource_path and resource_path_tuple""" + path = [loc.__name__ or '' for loc in lineage(resource)] + path.reverse() + path.extend(elements) + return path + +_model_path_list = _resource_path_list # b/w compat, not an API + +def virtual_root(resource, request): + """ + Provided any :term:`resource` and a :term:`request` object, return + the resource object representing the :term:`virtual root` of the + current :term:`request`. Using a virtual root in a + :term:`traversal` -based :app:`Pyramid` application permits + rooting. For example, the resource at the traversal path ``/cms`` will + be found at ``http://example.com/`` instead of rooting it at + ``http://example.com/cms/``. + + If the ``resource`` passed in is a context obtained via + :term:`traversal`, and if the ``HTTP_X_VHM_ROOT`` key is in the + WSGI environment, the value of this key will be treated as a + 'virtual root path': the :func:`pyramid.traversal.find_resource` + API will be used to find the virtual root resource using this path; + if the resource is found, it will be returned. If the + ``HTTP_X_VHM_ROOT`` key is not present in the WSGI environment, + the physical :term:`root` of the resource tree will be returned instead. + + Virtual roots are not useful at all in applications that use + :term:`URL dispatch`. Contexts obtained via URL dispatch don't + really support being virtually rooted (each URL dispatch context + is both its own physical and virtual root). However if this API + is called with a ``resource`` argument which is a context obtained + via URL dispatch, the resource passed in will be returned + unconditionally.""" + try: + reg = request.registry + except AttributeError: + reg = get_current_registry() + url_adapter = reg.queryMultiAdapter((resource, request), IResourceURL) + if url_adapter is None: + url_adapter = ResourceURL(resource, request) + + vpath, rpath = url_adapter.virtual_path, url_adapter.physical_path + if rpath != vpath and rpath.endswith(vpath): + vroot_path = rpath[:-len(vpath)] + return find_resource(resource, vroot_path) + + try: + return request.root + except AttributeError: + return find_root(resource) + +def traversal_path(path): + """ Variant of :func:`pyramid.traversal.traversal_path_info` suitable for + decoding paths that are URL-encoded. + + If this function is passed a Unicode object instead of a sequence of + bytes as ``path``, that Unicode object *must* directly encodeable to + ASCII. For example, u'/foo' will work but u'/' (a + Unicode object with characters that cannot be encoded to ascii) will + not. A :exc:`UnicodeEncodeError` will be raised if the Unicode cannot be + encoded directly to ASCII. + """ + if isinstance(path, text_type): + # must not possess characters outside ascii + path = path.encode('ascii') + # we unquote this path exactly like a PEP 3333 server would + path = unquote_bytes_to_wsgi(path) # result will be a native string + return traversal_path_info(path) # result will be a tuple of unicode + +@lru_cache(1000) +def traversal_path_info(path): + """ Given``path``, return a tuple representing that path which can be + used to traverse a resource tree. ``path`` is assumed to be an + already-URL-decoded ``str`` type as if it had come to us from an upstream + WSGI server as the ``PATH_INFO`` environ variable. + + The ``path`` is first decoded to from its WSGI representation to Unicode; + it is decoded differently depending on platform: + + - On Python 2, ``path`` is decoded to Unicode from bytes using the UTF-8 + decoding directly; a :exc:`pyramid.exc.URLDecodeError` is raised if a the + URL cannot be decoded. + + - On Python 3, as per the PEP 3333 spec, ``path`` is first encoded to + bytes using the Latin-1 encoding; the resulting set of bytes is + subsequently decoded to text using the UTF-8 encoding; a + :exc:`pyramid.exc.URLDecodeError` is raised if a the URL cannot be + decoded. + + The ``path`` is split on slashes, creating a list of segments. If a + segment name is empty or if it is ``.``, it is ignored. If a segment + name is ``..``, the previous segment is deleted, and the ``..`` is + ignored. + + Examples: + + ``/`` + + () + + ``/foo/bar/baz`` + + (u'foo', u'bar', u'baz') + + ``foo/bar/baz`` + + (u'foo', u'bar', u'baz') + + ``/foo/bar/baz/`` + + (u'foo', u'bar', u'baz') + + ``/foo//bar//baz/`` + + (u'foo', u'bar', u'baz') + + ``/foo/bar/baz/..`` + + (u'foo', u'bar') + + ``/my%20archives/hello`` + + (u'my archives', u'hello') + + ``/archives/La%20Pe%C3%B1a`` + + (u'archives', u'') + + .. note:: + + This function does not generate the same type of tuples that + :func:`pyramid.traversal.resource_path_tuple` does. In particular, the + leading empty string is not present in the tuple it returns, unlike tuples + returned by :func:`pyramid.traversal.resource_path_tuple`. As a result, + tuples generated by ``traversal_path`` are not resolveable by the + :func:`pyramid.traversal.find_resource` API. ``traversal_path`` is a + function mostly used by the internals of :app:`Pyramid` and by people + writing their own traversal machinery, as opposed to users writing + applications in :app:`Pyramid`. + """ + try: + path = decode_path_info(path) # result will be Unicode + except UnicodeDecodeError as e: + raise URLDecodeError(e.encoding, e.object, e.start, e.end, e.reason) + return split_path_info(path) # result will be tuple of Unicode + +@lru_cache(1000) +def split_path_info(path): + # suitable for splitting an already-unquoted-already-decoded (unicode) + # path value + path = path.strip('/') + clean = [] + for segment in path.split('/'): + if not segment or segment == '.': + continue + elif segment == '..': + if clean: + del clean[-1] + else: + clean.append(segment) + return tuple(clean) + +_segment_cache = {} + +quote_path_segment_doc = """ \ +Return a quoted representation of a 'path segment' (such as +the string ``__name__`` attribute of a resource) as a string. If the +``segment`` passed in is a unicode object, it is converted to a +UTF-8 string, then it is URL-quoted using Python's +``urllib.quote``. If the ``segment`` passed in is a string, it is +URL-quoted using Python's :mod:`urllib.quote`. If the segment +passed in is not a string or unicode object, an error will be +raised. The return value of ``quote_path_segment`` is always a +string, never Unicode. + +You may pass a string of characters that need not be encoded as +the ``safe`` argument to this function. This corresponds to the +``safe`` argument to :mod:`urllib.quote`. + +.. note:: + + The return value for each segment passed to this + function is cached in a module-scope dictionary for + speed: the cached version is returned when possible + rather than recomputing the quoted version. No cache + emptying is ever done for the lifetime of an + application, however. If you pass arbitrary + user-supplied strings to this function (as opposed to + some bounded set of values from a 'working set' known to + your application), it may become a memory leak. +""" + + +if PY2: + # special-case on Python 2 for speed? unchecked + def quote_path_segment(segment, safe=PATH_SEGMENT_SAFE): + """ %s """ % quote_path_segment_doc + # The bit of this code that deals with ``_segment_cache`` is an + # optimization: we cache all the computation of URL path segments + # in this module-scope dictionary with the original string (or + # unicode value) as the key, so we can look it up later without + # needing to reencode or re-url-quote it + try: + return _segment_cache[(segment, safe)] + except KeyError: + if segment.__class__ is text_type: #isinstance slighly slower (~15%) + result = url_quote(segment.encode('utf-8'), safe) + else: + result = url_quote(str(segment), safe) + # we don't need a lock to mutate _segment_cache, as the below + # will generate exactly one Python bytecode (STORE_SUBSCR) + _segment_cache[(segment, safe)] = result + return result +else: + def quote_path_segment(segment, safe=PATH_SEGMENT_SAFE): + """ %s """ % quote_path_segment_doc + # The bit of this code that deals with ``_segment_cache`` is an + # optimization: we cache all the computation of URL path segments + # in this module-scope dictionary with the original string (or + # unicode value) as the key, so we can look it up later without + # needing to reencode or re-url-quote it + try: + return _segment_cache[(segment, safe)] + except KeyError: + if segment.__class__ not in (text_type, binary_type): + segment = str(segment) + result = url_quote(native_(segment, 'utf-8'), safe) + # we don't need a lock to mutate _segment_cache, as the below + # will generate exactly one Python bytecode (STORE_SUBSCR) + _segment_cache[(segment, safe)] = result + return result + +slash = text_('/') + +@implementer(ITraverser) +class ResourceTreeTraverser(object): + """ A resource tree traverser that should be used (for speed) when + every resource in the tree supplies a ``__name__`` and + ``__parent__`` attribute (ie. every resource in the tree is + :term:`location` aware) .""" + + + VH_ROOT_KEY = VH_ROOT_KEY + VIEW_SELECTOR = '@@' + + def __init__(self, root): + self.root = root + + def __call__(self, request): + environ = request.environ + matchdict = request.matchdict + + if matchdict is not None: + + path = matchdict.get('traverse', slash) or slash + if is_nonstr_iter(path): + # this is a *traverse stararg (not a {traverse}) + # routing has already decoded these elements, so we just + # need to join them + path = '/' + slash.join(path) or slash + + subpath = matchdict.get('subpath', ()) + if not is_nonstr_iter(subpath): + # this is not a *subpath stararg (just a {subpath}) + # routing has already decoded this string, so we just need + # to split it + subpath = split_path_info(subpath) + + else: + # this request did not match a route + subpath = () + try: + # empty if mounted under a path in mod_wsgi, for example + path = request.path_info or slash + except KeyError: + # if environ['PATH_INFO'] is just not there + path = slash + except UnicodeDecodeError as e: + raise URLDecodeError(e.encoding, e.object, e.start, e.end, + e.reason) + + if self.VH_ROOT_KEY in environ: + # HTTP_X_VHM_ROOT + vroot_path = decode_path_info(environ[self.VH_ROOT_KEY]) + vroot_tuple = split_path_info(vroot_path) + vpath = vroot_path + path # both will (must) be unicode or asciistr + vroot_idx = len(vroot_tuple) - 1 + else: + vroot_tuple = () + vpath = path + vroot_idx = -1 + + root = self.root + ob = vroot = root + + if vpath == slash: # invariant: vpath must not be empty + # prevent a call to traversal_path if we know it's going + # to return the empty tuple + vpath_tuple = () + else: + # we do dead reckoning here via tuple slicing instead of + # pushing and popping temporary lists for speed purposes + # and this hurts readability; apologies + i = 0 + view_selector = self.VIEW_SELECTOR + vpath_tuple = split_path_info(vpath) + for segment in vpath_tuple: + if segment[:2] == view_selector: + return {'context': ob, + 'view_name': segment[2:], + 'subpath': vpath_tuple[i + 1:], + 'traversed': vpath_tuple[:vroot_idx + i + 1], + 'virtual_root': vroot, + 'virtual_root_path': vroot_tuple, + 'root': root} + try: + getitem = ob.__getitem__ + except AttributeError: + return {'context': ob, + 'view_name': segment, + 'subpath': vpath_tuple[i + 1:], + 'traversed': vpath_tuple[:vroot_idx + i + 1], + 'virtual_root': vroot, + 'virtual_root_path': vroot_tuple, + 'root': root} + + try: + next = getitem(segment) + except KeyError: + return {'context': ob, + 'view_name': segment, + 'subpath': vpath_tuple[i + 1:], + 'traversed': vpath_tuple[:vroot_idx + i + 1], + 'virtual_root': vroot, + 'virtual_root_path': vroot_tuple, + 'root': root} + if i == vroot_idx: + vroot = next + ob = next + i += 1 + + return {'context':ob, 'view_name':empty, 'subpath':subpath, + 'traversed':vpath_tuple, 'virtual_root':vroot, + 'virtual_root_path':vroot_tuple, 'root':root} + +ModelGraphTraverser = ResourceTreeTraverser # b/w compat, not API, used in wild + +@implementer(IResourceURL) +class ResourceURL(object): + VH_ROOT_KEY = VH_ROOT_KEY + + def __init__(self, resource, request): + physical_path_tuple = resource_path_tuple(resource) + physical_path = _join_path_tuple(physical_path_tuple) + + if physical_path_tuple != ('',): + physical_path_tuple = physical_path_tuple + ('',) + physical_path = physical_path + '/' + + virtual_path = physical_path + virtual_path_tuple = physical_path_tuple + + environ = request.environ + vroot_path = environ.get(self.VH_ROOT_KEY) + + # if the physical path starts with the virtual root path, trim it out + # of the virtual path + if vroot_path is not None: + vroot_path = vroot_path.rstrip('/') + if vroot_path and physical_path.startswith(vroot_path): + vroot_path_tuple = tuple(vroot_path.split('/')) + numels = len(vroot_path_tuple) + virtual_path_tuple = ('',) + physical_path_tuple[numels:] + virtual_path = physical_path[len(vroot_path):] + + self.virtual_path = virtual_path # IResourceURL attr + self.physical_path = physical_path # IResourceURL attr + self.virtual_path_tuple = virtual_path_tuple # IResourceURL attr (1.5) + self.physical_path_tuple = physical_path_tuple # IResourceURL attr (1.5) + +@lru_cache(1000) +def _join_path_tuple(tuple): + return tuple and '/'.join([quote_path_segment(x) for x in tuple]) or '/' + +class DefaultRootFactory: + __parent__ = None + __name__ = None + def __init__(self, request): + pass diff --git a/src/pyramid/tweens.py b/src/pyramid/tweens.py new file mode 100644 index 000000000..740b6961c --- /dev/null +++ b/src/pyramid/tweens.py @@ -0,0 +1,48 @@ +import sys + +from pyramid.compat import reraise +from pyramid.httpexceptions import HTTPNotFound + +def _error_handler(request, exc): + # NOTE: we do not need to delete exc_info because this function + # should never be in the call stack of the exception + exc_info = sys.exc_info() + + try: + response = request.invoke_exception_view(exc_info) + except HTTPNotFound: + # re-raise the original exception as no exception views were + # able to handle the error + reraise(*exc_info) + + return response + +def excview_tween_factory(handler, registry): + """ A :term:`tween` factory which produces a tween that catches an + exception raised by downstream tweens (or the main Pyramid request + handler) and, if possible, converts it into a Response using an + :term:`exception view`. + + .. versionchanged:: 1.9 + The ``request.response`` will be remain unchanged even if the tween + handles an exception. Previously it was deleted after handling an + exception. + + Also, ``request.exception`` and ``request.exc_info`` are only set if + the tween handles an exception and returns a response otherwise they + are left at their original values. + + """ + + def excview_tween(request): + try: + response = handler(request) + except Exception as exc: + response = _error_handler(request, exc) + return response + + return excview_tween + +MAIN = 'MAIN' +INGRESS = 'INGRESS' +EXCVIEW = 'pyramid.tweens.excview_tween_factory' diff --git a/src/pyramid/url.py b/src/pyramid/url.py new file mode 100644 index 000000000..852aa5e55 --- /dev/null +++ b/src/pyramid/url.py @@ -0,0 +1,894 @@ +""" Utility functions for dealing with URLs in pyramid """ + +import os + +from pyramid.interfaces import ( + IResourceURL, + IRoutesMapper, + IStaticURLInfo, + ) + +from pyramid.compat import ( + bytes_, + lru_cache, + string_types, + ) +from pyramid.encode import ( + url_quote, + urlencode, +) +from pyramid.path import caller_package +from pyramid.threadlocal import get_current_registry + +from pyramid.traversal import ( + ResourceURL, + quote_path_segment, + PATH_SAFE, + PATH_SEGMENT_SAFE, + ) + +QUERY_SAFE = "/?:@!$&'()*+,;=" # RFC 3986 +ANCHOR_SAFE = QUERY_SAFE + +def parse_url_overrides(request, kw): + """ + Parse special arguments passed when generating urls. + + The supplied dictionary is mutated when we pop arguments. + Returns a 3-tuple of the format: + + ``(app_url, qs, anchor)``. + + """ + app_url = kw.pop('_app_url', None) + scheme = kw.pop('_scheme', None) + host = kw.pop('_host', None) + port = kw.pop('_port', None) + query = kw.pop('_query', '') + anchor = kw.pop('_anchor', '') + + if app_url is None: + if (scheme is not None or host is not None or port is not None): + app_url = request._partial_application_url(scheme, host, port) + else: + app_url = request.application_url + + qs = '' + if query: + if isinstance(query, string_types): + qs = '?' + url_quote(query, QUERY_SAFE) + else: + qs = '?' + urlencode(query, doseq=True) + + frag = '' + if anchor: + frag = '#' + url_quote(anchor, ANCHOR_SAFE) + + return app_url, qs, frag + +class URLMethodsMixin(object): + """ Request methods mixin for BaseRequest having to do with URL + generation """ + + def _partial_application_url(self, scheme=None, host=None, port=None): + """ + Construct the URL defined by request.application_url, replacing any + of the default scheme, host, or port portions with user-supplied + variants. + + If ``scheme`` is passed as ``https``, and the ``port`` is *not* + passed, the ``port`` value is assumed to ``443``. Likewise, if + ``scheme`` is passed as ``http`` and ``port`` is not passed, the + ``port`` value is assumed to be ``80``. + + """ + e = self.environ + if scheme is None: + scheme = e['wsgi.url_scheme'] + else: + if scheme == 'https': + if port is None: + port = '443' + if scheme == 'http': + if port is None: + port = '80' + if host is None: + host = e.get('HTTP_HOST') + if host is None: + host = e['SERVER_NAME'] + if port is None: + if ':' in host: + host, port = host.split(':', 1) + else: + port = e['SERVER_PORT'] + else: + port = str(port) + if ':' in host: + host, _ = host.split(':', 1) + if scheme == 'https': + if port == '443': + port = None + elif scheme == 'http': + if port == '80': + port = None + url = scheme + '://' + host + if port: + url += ':%s' % port + + url_encoding = getattr(self, 'url_encoding', 'utf-8') # webob 1.2b3+ + bscript_name = bytes_(self.script_name, url_encoding) + return url + url_quote(bscript_name, PATH_SAFE) + + def route_url(self, route_name, *elements, **kw): + """Generates a fully qualified URL for a named :app:`Pyramid` + :term:`route configuration`. + + Use the route's ``name`` as the first positional argument. + Additional positional arguments (``*elements``) are appended to the + URL as path segments after it is generated. + + Use keyword arguments to supply values which match any dynamic + path elements in the route definition. Raises a :exc:`KeyError` + exception if the URL cannot be generated for any reason (not + enough arguments, for example). + + For example, if you've defined a route named "foobar" with the path + ``{foo}/{bar}/*traverse``:: + + request.route_url('foobar', + foo='1') => + request.route_url('foobar', + foo='1', + bar='2') => + request.route_url('foobar', + foo='1', + bar='2', + traverse=('a','b')) => http://e.com/1/2/a/b + request.route_url('foobar', + foo='1', + bar='2', + traverse='/a/b') => http://e.com/1/2/a/b + + Values replacing ``:segment`` arguments can be passed as strings + or Unicode objects. They will be encoded to UTF-8 and URL-quoted + before being placed into the generated URL. + + Values replacing ``*remainder`` arguments can be passed as strings + *or* tuples of Unicode/string values. If a tuple is passed as a + ``*remainder`` replacement value, its values are URL-quoted and + encoded to UTF-8. The resulting strings are joined with slashes + and rendered into the URL. If a string is passed as a + ``*remainder`` replacement value, it is tacked on to the URL + after being URL-quoted-except-for-embedded-slashes. + + If ``_query`` is provided, it will be used to compose a query string + that will be tacked on to the end of the URL. The value of ``_query`` + may be a sequence of two-tuples *or* a data structure with an + ``.items()`` method that returns a sequence of two-tuples + (presumably a dictionary). This data structure will be turned into + a query string per the documentation of the + :func:`pyramid.url.urlencode` function. This will produce a query + string in the ``x-www-form-urlencoded`` format. A + non-``x-www-form-urlencoded`` query string may be used by passing a + *string* value as ``_query`` in which case it will be URL-quoted + (e.g. query="foo bar" will become "foo%20bar"). However, the result + will not need to be in ``k=v`` form as required by + ``x-www-form-urlencoded``. After the query data is turned into a query + string, a leading ``?`` is prepended, and the resulting string is + appended to the generated URL. + + .. note:: + + Python data structures that are passed as ``_query`` which are + sequences or dictionaries are turned into a string under the same + rules as when run through :func:`urllib.urlencode` with the ``doseq`` + argument equal to ``True``. This means that sequences can be passed + as values, and a k=v pair will be placed into the query string for + each value. + + If a keyword argument ``_anchor`` is present, its string + representation will be quoted per :rfc:`3986#section-3.5` and used as + a named anchor in the generated URL + (e.g. if ``_anchor`` is passed as ``foo`` and the route URL is + ``http://example.com/route/url``, the resulting generated URL will + be ``http://example.com/route/url#foo``). + + .. note:: + + If ``_anchor`` is passed as a string, it should be UTF-8 encoded. If + ``_anchor`` is passed as a Unicode object, it will be converted to + UTF-8 before being appended to the URL. + + If both ``_anchor`` and ``_query`` are specified, the anchor + element will always follow the query element, + e.g. ``http://example.com?foo=1#bar``. + + If any of the keyword arguments ``_scheme``, ``_host``, or ``_port`` + is passed and is non-``None``, the provided value will replace the + named portion in the generated URL. For example, if you pass + ``_host='foo.com'``, and the URL that would have been generated + without the host replacement is ``http://example.com/a``, the result + will be ``http://foo.com/a``. + + Note that if ``_scheme`` is passed as ``https``, and ``_port`` is not + passed, the ``_port`` value is assumed to have been passed as + ``443``. Likewise, if ``_scheme`` is passed as ``http`` and + ``_port`` is not passed, the ``_port`` value is assumed to have been + passed as ``80``. To avoid this behavior, always explicitly pass + ``_port`` whenever you pass ``_scheme``. + + If a keyword ``_app_url`` is present, it will be used as the + protocol/hostname/port/leading path prefix of the generated URL. + For example, using an ``_app_url`` of + ``http://example.com:8080/foo`` would cause the URL + ``http://example.com:8080/foo/fleeb/flub`` to be returned from + this function if the expansion of the route pattern associated + with the ``route_name`` expanded to ``/fleeb/flub``. If + ``_app_url`` is not specified, the result of + ``request.application_url`` will be used as the prefix (the + default). + + If both ``_app_url`` and any of ``_scheme``, ``_host``, or ``_port`` + are passed, ``_app_url`` takes precedence and any values passed for + ``_scheme``, ``_host``, and ``_port`` will be ignored. + + This function raises a :exc:`KeyError` if the URL cannot be + generated due to missing replacement names. Extra replacement + names are ignored. + + If the route object which matches the ``route_name`` argument has + a :term:`pregenerator`, the ``*elements`` and ``**kw`` + arguments passed to this function might be augmented or changed. + + .. versionchanged:: 1.5 + Allow the ``_query`` option to be a string to enable alternative + encodings. + + The ``_anchor`` option will be escaped instead of using + its raw string representation. + + .. versionchanged:: 1.9 + If ``_query`` or ``_anchor`` are falsey (such as ``None`` or an + empty string) they will not be included in the generated url. + + """ + try: + reg = self.registry + except AttributeError: + reg = get_current_registry() # b/c + mapper = reg.getUtility(IRoutesMapper) + route = mapper.get_route(route_name) + + if route is None: + raise KeyError('No such route named %s' % route_name) + + if route.pregenerator is not None: + elements, kw = route.pregenerator(self, elements, kw) + + app_url, qs, anchor = parse_url_overrides(self, kw) + + path = route.generate(kw) # raises KeyError if generate fails + + if elements: + suffix = _join_elements(elements) + if not path.endswith('/'): + suffix = '/' + suffix + else: + suffix = '' + + return app_url + path + suffix + qs + anchor + + def route_path(self, route_name, *elements, **kw): + """ + Generates a path (aka a 'relative URL', a URL minus the host, scheme, + and port) for a named :app:`Pyramid` :term:`route configuration`. + + This function accepts the same argument as + :meth:`pyramid.request.Request.route_url` and performs the same duty. + It just omits the host, port, and scheme information in the return + value; only the script_name, path, query parameters, and anchor data + are present in the returned string. + + For example, if you've defined a route named 'foobar' with the path + ``/{foo}/{bar}``, this call to ``route_path``:: + + request.route_path('foobar', foo='1', bar='2') + + Will return the string ``/1/2``. + + .. note:: + + Calling ``request.route_path('route')`` is the same as calling + ``request.route_url('route', _app_url=request.script_name)``. + :meth:`pyramid.request.Request.route_path` is, in fact, + implemented in terms of :meth:`pyramid.request.Request.route_url` + in just this way. As a result, any ``_app_url`` passed within the + ``**kw`` values to ``route_path`` will be ignored. + + """ + kw['_app_url'] = self.script_name + return self.route_url(route_name, *elements, **kw) + + def resource_url(self, resource, *elements, **kw): + """ + Generate a string representing the absolute URL of the + :term:`resource` object based on the ``wsgi.url_scheme``, + ``HTTP_HOST`` or ``SERVER_NAME`` in the request, plus any + ``SCRIPT_NAME``. The overall result of this method is always a + UTF-8 encoded string. + + Examples:: + + request.resource_url(resource) => + + http://example.com/ + + request.resource_url(resource, 'a.html') => + + http://example.com/a.html + + request.resource_url(resource, 'a.html', query={'q':'1'}) => + + http://example.com/a.html?q=1 + + request.resource_url(resource, 'a.html', anchor='abc') => + + http://example.com/a.html#abc + + request.resource_url(resource, app_url='') => + + / + + Any positional arguments passed in as ``elements`` must be strings + Unicode objects, or integer objects. These will be joined by slashes + and appended to the generated resource URL. Each of the elements + passed in is URL-quoted before being appended; if any element is + Unicode, it will converted to a UTF-8 bytestring before being + URL-quoted. If any element is an integer, it will be converted to its + string representation before being URL-quoted. + + .. warning:: if no ``elements`` arguments are specified, the resource + URL will end with a trailing slash. If any + ``elements`` are used, the generated URL will *not* + end in a trailing slash. + + If ``query`` is provided, it will be used to compose a query string + that will be tacked on to the end of the URL. The value of ``query`` + may be a sequence of two-tuples *or* a data structure with an + ``.items()`` method that returns a sequence of two-tuples + (presumably a dictionary). This data structure will be turned into + a query string per the documentation of the + :func:`pyramid.url.urlencode` function. This will produce a query + string in the ``x-www-form-urlencoded`` format. A + non-``x-www-form-urlencoded`` query string may be used by passing a + *string* value as ``query`` in which case it will be URL-quoted + (e.g. query="foo bar" will become "foo%20bar"). However, the result + will not need to be in ``k=v`` form as required by + ``x-www-form-urlencoded``. After the query data is turned into a query + string, a leading ``?`` is prepended, and the resulting string is + appended to the generated URL. + + .. note:: + + Python data structures that are passed as ``query`` which are + sequences or dictionaries are turned into a string under the same + rules as when run through :func:`urllib.urlencode` with the ``doseq`` + argument equal to ``True``. This means that sequences can be passed + as values, and a k=v pair will be placed into the query string for + each value. + + If a keyword argument ``anchor`` is present, its string + representation will be used as a named anchor in the generated URL + (e.g. if ``anchor`` is passed as ``foo`` and the resource URL is + ``http://example.com/resource/url``, the resulting generated URL will + be ``http://example.com/resource/url#foo``). + + .. note:: + + If ``anchor`` is passed as a string, it should be UTF-8 encoded. If + ``anchor`` is passed as a Unicode object, it will be converted to + UTF-8 before being appended to the URL. + + If both ``anchor`` and ``query`` are specified, the anchor element + will always follow the query element, + e.g. ``http://example.com?foo=1#bar``. + + If any of the keyword arguments ``scheme``, ``host``, or ``port`` is + passed and is non-``None``, the provided value will replace the named + portion in the generated URL. For example, if you pass + ``host='foo.com'``, and the URL that would have been generated + without the host replacement is ``http://example.com/a``, the result + will be ``http://foo.com/a``. + + If ``scheme`` is passed as ``https``, and an explicit ``port`` is not + passed, the ``port`` value is assumed to have been passed as ``443``. + Likewise, if ``scheme`` is passed as ``http`` and ``port`` is not + passed, the ``port`` value is assumed to have been passed as + ``80``. To avoid this behavior, always explicitly pass ``port`` + whenever you pass ``scheme``. + + If a keyword argument ``app_url`` is passed and is not ``None``, it + should be a string that will be used as the port/hostname/initial + path portion of the generated URL instead of the default request + application URL. For example, if ``app_url='http://foo'``, then the + resulting url of a resource that has a path of ``/baz/bar`` will be + ``http://foo/baz/bar``. If you want to generate completely relative + URLs with no leading scheme, host, port, or initial path, you can + pass ``app_url=''``. Passing ``app_url=''`` when the resource path is + ``/baz/bar`` will return ``/baz/bar``. + + If ``app_url`` is passed and any of ``scheme``, ``port``, or ``host`` + are also passed, ``app_url`` will take precedence and the values + passed for ``scheme``, ``host``, and/or ``port`` will be ignored. + + If the ``resource`` passed in has a ``__resource_url__`` method, it + will be used to generate the URL (scheme, host, port, path) for the + base resource which is operated upon by this function. + + .. seealso:: + + See also :ref:`overriding_resource_url_generation`. + + If ``route_name`` is passed, this function will delegate its URL + production to the ``route_url`` function. Calling + ``resource_url(someresource, 'element1', 'element2', query={'a':1}, + route_name='blogentry')`` is roughly equivalent to doing:: + + traversal_path = request.resource_path(someobject) + url = request.route_url( + 'blogentry', + 'element1', + 'element2', + _query={'a':'1'}, + traverse=traversal_path, + ) + + It is only sensible to pass ``route_name`` if the route being named has + a ``*remainder`` stararg value such as ``*traverse``. The remainder + value will be ignored in the output otherwise. + + By default, the resource path value will be passed as the name + ``traverse`` when ``route_url`` is called. You can influence this by + passing a different ``route_remainder_name`` value if the route has a + different ``*stararg`` value at its end. For example if the route + pattern you want to replace has a ``*subpath`` stararg ala + ``/foo*subpath``:: + + request.resource_url( + resource, + route_name='myroute', + route_remainder_name='subpath' + ) + + If ``route_name`` is passed, it is also permissible to pass + ``route_kw``, which will passed as additional keyword arguments to + ``route_url``. Saying ``resource_url(someresource, 'element1', + 'element2', route_name='blogentry', route_kw={'id':'4'}, + _query={'a':'1'})`` is roughly equivalent to:: + + traversal_path = request.resource_path_tuple(someobject) + kw = {'id':'4', '_query':{'a':'1'}, 'traverse':traversal_path} + url = request.route_url( + 'blogentry', + 'element1', + 'element2', + **kw, + ) + + If ``route_kw`` or ``route_remainder_name`` is passed, but + ``route_name`` is not passed, both ``route_kw`` and + ``route_remainder_name`` will be ignored. If ``route_name`` + is passed, the ``__resource_url__`` method of the resource passed is + ignored unconditionally. This feature is incompatible with + resources which generate their own URLs. + + .. note:: + + If the :term:`resource` used is the result of a :term:`traversal`, it + must be :term:`location`-aware. The resource can also be the context + of a :term:`URL dispatch`; contexts found this way do not need to be + location-aware. + + .. note:: + + If a 'virtual root path' is present in the request environment (the + value of the WSGI environ key ``HTTP_X_VHM_ROOT``), and the resource + was obtained via :term:`traversal`, the URL path will not include the + virtual root prefix (it will be stripped off the left hand side of + the generated URL). + + .. note:: + + For backwards compatibility purposes, this method is also + aliased as the ``model_url`` method of request. + + .. versionchanged:: 1.3 + Added the ``app_url`` keyword argument. + + .. versionchanged:: 1.5 + Allow the ``query`` option to be a string to enable alternative + encodings. + + The ``anchor`` option will be escaped instead of using + its raw string representation. + + Added the ``route_name``, ``route_kw``, and + ``route_remainder_name`` keyword arguments. + + .. versionchanged:: 1.9 + If ``query`` or ``anchor`` are falsey (such as ``None`` or an + empty string) they will not be included in the generated url. + """ + try: + reg = self.registry + except AttributeError: + reg = get_current_registry() # b/c + + url_adapter = reg.queryMultiAdapter((resource, self), IResourceURL) + if url_adapter is None: + url_adapter = ResourceURL(resource, self) + + virtual_path = getattr(url_adapter, 'virtual_path', None) + + urlkw = {} + for name in ( + 'app_url', 'scheme', 'host', 'port', 'query', 'anchor' + ): + val = kw.get(name, None) + if val is not None: + urlkw['_' + name] = val + + if 'route_name' in kw: + route_name = kw['route_name'] + remainder = getattr(url_adapter, 'virtual_path_tuple', None) + if remainder is None: + # older user-supplied IResourceURL adapter without 1.5 + # virtual_path_tuple + remainder = tuple(url_adapter.virtual_path.split('/')) + remainder_name = kw.get('route_remainder_name', 'traverse') + urlkw[remainder_name] = remainder + + if 'route_kw' in kw: + route_kw = kw.get('route_kw') + if route_kw is not None: + urlkw.update(route_kw) + + return self.route_url(route_name, *elements, **urlkw) + + app_url, qs, anchor = parse_url_overrides(self, urlkw) + + resource_url = None + local_url = getattr(resource, '__resource_url__', None) + + if local_url is not None: + # the resource handles its own url generation + d = dict( + virtual_path=virtual_path, + physical_path=url_adapter.physical_path, + app_url=app_url, + ) + + # allow __resource_url__ to punt by returning None + resource_url = local_url(self, d) + + if resource_url is None: + # the resource did not handle its own url generation or the + # __resource_url__ function returned None + resource_url = app_url + virtual_path + + if elements: + suffix = _join_elements(elements) + else: + suffix = '' + + return resource_url + suffix + qs + anchor + + model_url = resource_url # b/w compat forever + + def resource_path(self, resource, *elements, **kw): + """ + Generates a path (aka a 'relative URL', a URL minus the host, scheme, + and port) for a :term:`resource`. + + This function accepts the same argument as + :meth:`pyramid.request.Request.resource_url` and performs the same + duty. It just omits the host, port, and scheme information in the + return value; only the script_name, path, query parameters, and + anchor data are present in the returned string. + + .. note:: + + Calling ``request.resource_path(resource)`` is the same as calling + ``request.resource_path(resource, app_url=request.script_name)``. + :meth:`pyramid.request.Request.resource_path` is, in fact, + implemented in terms of + :meth:`pyramid.request.Request.resource_url` in just this way. As + a result, any ``app_url`` passed within the ``**kw`` values to + ``route_path`` will be ignored. ``scheme``, ``host``, and + ``port`` are also ignored. + """ + kw['app_url'] = self.script_name + return self.resource_url(resource, *elements, **kw) + + def static_url(self, path, **kw): + """ + Generates a fully qualified URL for a static :term:`asset`. + The asset must live within a location defined via the + :meth:`pyramid.config.Configurator.add_static_view` + :term:`configuration declaration` (see :ref:`static_assets_section`). + + Example:: + + request.static_url('mypackage:static/foo.css') => + + http://example.com/static/foo.css + + + The ``path`` argument points at a file or directory on disk which + a URL should be generated for. The ``path`` may be either a + relative path (e.g. ``static/foo.css``) or an absolute path (e.g. + ``/abspath/to/static/foo.css``) or a :term:`asset specification` + (e.g. ``mypackage:static/foo.css``). + + The purpose of the ``**kw`` argument is the same as the purpose of + the :meth:`pyramid.request.Request.route_url` ``**kw`` argument. See + the documentation for that function to understand the arguments which + you can provide to it. However, typically, you don't need to pass + anything as ``*kw`` when generating a static asset URL. + + This function raises a :exc:`ValueError` if a static view + definition cannot be found which matches the path specification. + + """ + if not os.path.isabs(path): + if ':' not in path: + # if it's not a package:relative/name and it's not an + # /absolute/path it's a relative/path; this means its relative + # to the package in which the caller's module is defined. + package = caller_package() + path = '%s:%s' % (package.__name__, path) + + try: + reg = self.registry + except AttributeError: + reg = get_current_registry() # b/c + + info = reg.queryUtility(IStaticURLInfo) + if info is None: + raise ValueError('No static URL definition matching %s' % path) + + return info.generate(path, self, **kw) + + def static_path(self, path, **kw): + """ + Generates a path (aka a 'relative URL', a URL minus the host, scheme, + and port) for a static resource. + + This function accepts the same argument as + :meth:`pyramid.request.Request.static_url` and performs the + same duty. It just omits the host, port, and scheme information in + the return value; only the script_name, path, query parameters, and + anchor data are present in the returned string. + + Example:: + + request.static_path('mypackage:static/foo.css') => + + /static/foo.css + + .. note:: + + Calling ``request.static_path(apath)`` is the same as calling + ``request.static_url(apath, _app_url=request.script_name)``. + :meth:`pyramid.request.Request.static_path` is, in fact, implemented + in terms of :meth:`pyramid.request.Request.static_url` in just this + way. As a result, any ``_app_url`` passed within the ``**kw`` values + to ``static_path`` will be ignored. + """ + if not os.path.isabs(path): + if ':' not in path: + # if it's not a package:relative/name and it's not an + # /absolute/path it's a relative/path; this means its relative + # to the package in which the caller's module is defined. + package = caller_package() + path = '%s:%s' % (package.__name__, path) + + kw['_app_url'] = self.script_name + return self.static_url(path, **kw) + + def current_route_url(self, *elements, **kw): + """ + Generates a fully qualified URL for a named :app:`Pyramid` + :term:`route configuration` based on the 'current route'. + + This function supplements + :meth:`pyramid.request.Request.route_url`. It presents an easy way to + generate a URL for the 'current route' (defined as the route which + matched when the request was generated). + + The arguments to this method have the same meaning as those with the + same names passed to :meth:`pyramid.request.Request.route_url`. It + also understands an extra argument which ``route_url`` does not named + ``_route_name``. + + The route name used to generate a URL is taken from either the + ``_route_name`` keyword argument or the name of the route which is + currently associated with the request if ``_route_name`` was not + passed. Keys and values from the current request :term:`matchdict` + are combined with the ``kw`` arguments to form a set of defaults + named ``newkw``. Then ``request.route_url(route_name, *elements, + **newkw)`` is called, returning a URL. + + Examples follow. + + If the 'current route' has the route pattern ``/foo/{page}`` and the + current url path is ``/foo/1`` , the matchdict will be + ``{'page':'1'}``. The result of ``request.current_route_url()`` in + this situation will be ``/foo/1``. + + If the 'current route' has the route pattern ``/foo/{page}`` and the + current url path is ``/foo/1``, the matchdict will be + ``{'page':'1'}``. The result of + ``request.current_route_url(page='2')`` in this situation will be + ``/foo/2``. + + Usage of the ``_route_name`` keyword argument: if our routing table + defines routes ``/foo/{action}`` named 'foo' and + ``/foo/{action}/{page}`` named ``fooaction``, and the current url + pattern is ``/foo/view`` (which has matched the ``/foo/{action}`` + route), we may want to use the matchdict args to generate a URL to + the ``fooaction`` route. In this scenario, + ``request.current_route_url(_route_name='fooaction', page='5')`` + Will return string like: ``/foo/view/5``. + + """ + if '_route_name' in kw: + route_name = kw.pop('_route_name') + else: + route = getattr(self, 'matched_route', None) + route_name = getattr(route, 'name', None) + if route_name is None: + raise ValueError('Current request matches no route') + + if '_query' not in kw: + kw['_query'] = self.GET + + newkw = {} + newkw.update(self.matchdict) + newkw.update(kw) + return self.route_url(route_name, *elements, **newkw) + + def current_route_path(self, *elements, **kw): + """ + Generates a path (aka a 'relative URL', a URL minus the host, scheme, + and port) for the :app:`Pyramid` :term:`route configuration` matched + by the current request. + + This function accepts the same argument as + :meth:`pyramid.request.Request.current_route_url` and performs the + same duty. It just omits the host, port, and scheme information in + the return value; only the script_name, path, query parameters, and + anchor data are present in the returned string. + + For example, if the route matched by the current request has the + pattern ``/{foo}/{bar}``, this call to ``current_route_path``:: + + request.current_route_path(foo='1', bar='2') + + Will return the string ``/1/2``. + + .. note:: + + Calling ``request.current_route_path('route')`` is the same + as calling ``request.current_route_url('route', + _app_url=request.script_name)``. + :meth:`pyramid.request.Request.current_route_path` is, in fact, + implemented in terms of + :meth:`pyramid.request.Request.current_route_url` in just this + way. As a result, any ``_app_url`` passed within the ``**kw`` + values to ``current_route_path`` will be ignored. + """ + kw['_app_url'] = self.script_name + return self.current_route_url(*elements, **kw) + + +def route_url(route_name, request, *elements, **kw): + """ + This is a backwards compatibility function. Its result is the same as + calling:: + + request.route_url(route_name, *elements, **kw) + + See :meth:`pyramid.request.Request.route_url` for more information. + """ + return request.route_url(route_name, *elements, **kw) + +def route_path(route_name, request, *elements, **kw): + """ + This is a backwards compatibility function. Its result is the same as + calling:: + + request.route_path(route_name, *elements, **kw) + + See :meth:`pyramid.request.Request.route_path` for more information. + """ + return request.route_path(route_name, *elements, **kw) + +def resource_url(resource, request, *elements, **kw): + """ + This is a backwards compatibility function. Its result is the same as + calling:: + + request.resource_url(resource, *elements, **kw) + + See :meth:`pyramid.request.Request.resource_url` for more information. + """ + return request.resource_url(resource, *elements, **kw) + +model_url = resource_url # b/w compat (forever) + + +def static_url(path, request, **kw): + """ + This is a backwards compatibility function. Its result is the same as + calling:: + + request.static_url(path, **kw) + + See :meth:`pyramid.request.Request.static_url` for more information. + """ + if not os.path.isabs(path): + if ':' not in path: + # if it's not a package:relative/name and it's not an + # /absolute/path it's a relative/path; this means its relative + # to the package in which the caller's module is defined. + package = caller_package() + path = '%s:%s' % (package.__name__, path) + return request.static_url(path, **kw) + + +def static_path(path, request, **kw): + """ + This is a backwards compatibility function. Its result is the same as + calling:: + + request.static_path(path, **kw) + + See :meth:`pyramid.request.Request.static_path` for more information. + """ + if not os.path.isabs(path): + if ':' not in path: + # if it's not a package:relative/name and it's not an + # /absolute/path it's a relative/path; this means its relative + # to the package in which the caller's module is defined. + package = caller_package() + path = '%s:%s' % (package.__name__, path) + return request.static_path(path, **kw) + +def current_route_url(request, *elements, **kw): + """ + This is a backwards compatibility function. Its result is the same as + calling:: + + request.current_route_url(*elements, **kw) + + See :meth:`pyramid.request.Request.current_route_url` for more + information. + """ + return request.current_route_url(*elements, **kw) + +def current_route_path(request, *elements, **kw): + """ + This is a backwards compatibility function. Its result is the same as + calling:: + + request.current_route_path(*elements, **kw) + + See :meth:`pyramid.request.Request.current_route_path` for more + information. + """ + return request.current_route_path(*elements, **kw) + +@lru_cache(1000) +def _join_elements(elements): + return '/'.join([quote_path_segment(s, safe=PATH_SEGMENT_SAFE) for s in elements]) diff --git a/src/pyramid/urldispatch.py b/src/pyramid/urldispatch.py new file mode 100644 index 000000000..a61071845 --- /dev/null +++ b/src/pyramid/urldispatch.py @@ -0,0 +1,249 @@ +import re +from zope.interface import implementer + +from pyramid.interfaces import ( + IRoutesMapper, + IRoute, + ) + +from pyramid.compat import ( + PY2, + native_, + text_, + text_type, + string_types, + binary_type, + is_nonstr_iter, + decode_path_info, + ) + +from pyramid.exceptions import URLDecodeError + +from pyramid.traversal import ( + quote_path_segment, + split_path_info, + PATH_SAFE, + ) + +_marker = object() + +@implementer(IRoute) +class Route(object): + def __init__(self, name, pattern, factory=None, predicates=(), + pregenerator=None): + self.pattern = pattern + self.path = pattern # indefinite b/w compat, not in interface + self.match, self.generate = _compile_route(pattern) + self.name = name + self.factory = factory + self.predicates = predicates + self.pregenerator = pregenerator + +@implementer(IRoutesMapper) +class RoutesMapper(object): + def __init__(self): + self.routelist = [] + self.static_routes = [] + + self.routes = {} + + def has_routes(self): + return bool(self.routelist) + + def get_routes(self, include_static=False): + if include_static is True: + return self.routelist + self.static_routes + + return self.routelist + + def get_route(self, name): + return self.routes.get(name) + + def connect(self, name, pattern, factory=None, predicates=(), + pregenerator=None, static=False): + if name in self.routes: + oldroute = self.routes[name] + if oldroute in self.routelist: + self.routelist.remove(oldroute) + + route = Route(name, pattern, factory, predicates, pregenerator) + if not static: + self.routelist.append(route) + else: + self.static_routes.append(route) + + self.routes[name] = route + return route + + def generate(self, name, kw): + return self.routes[name].generate(kw) + + def __call__(self, request): + environ = request.environ + try: + # empty if mounted under a path in mod_wsgi, for example + path = decode_path_info(environ['PATH_INFO'] or '/') + except KeyError: + path = '/' + except UnicodeDecodeError as e: + raise URLDecodeError(e.encoding, e.object, e.start, e.end, e.reason) + + for route in self.routelist: + match = route.match(path) + if match is not None: + preds = route.predicates + info = {'match':match, 'route':route} + if preds and not all((p(info, request) for p in preds)): + continue + return info + + return {'route':None, 'match':None} + +# stolen from bobo and modified +old_route_re = re.compile(r'(\:[_a-zA-Z]\w*)') +star_at_end = re.compile(r'\*(\w*)$') + +# The tortuous nature of the regex named ``route_re`` below is due to the +# fact that we need to support at least one level of "inner" squigglies +# inside the expr of a {name:expr} pattern. This regex used to be just +# (\{[a-zA-Z][^\}]*\}) but that choked when supplied with e.g. {foo:\d{4}}. +route_re = re.compile(r'(\{[_a-zA-Z][^{}]*(?:\{[^{}]*\}[^{}]*)*\})') + +def update_pattern(matchobj): + name = matchobj.group(0) + return '{%s}' % name[1:] + +def _compile_route(route): + # This function really wants to consume Unicode patterns natively, but if + # someone passes us a bytestring, we allow it by converting it to Unicode + # using the ASCII decoding. We decode it using ASCII because we don't + # want to accept bytestrings with high-order characters in them here as + # we have no idea what the encoding represents. + if route.__class__ is not text_type: + try: + route = text_(route, 'ascii') + except UnicodeDecodeError: + raise ValueError( + 'The pattern value passed to add_route must be ' + 'either a Unicode string or a plain string without ' + 'any non-ASCII characters (you provided %r).' % route) + + if old_route_re.search(route) and not route_re.search(route): + route = old_route_re.sub(update_pattern, route) + + if not route.startswith('/'): + route = '/' + route + + remainder = None + if star_at_end.search(route): + route, remainder = route.rsplit('*', 1) + + pat = route_re.split(route) + + # every element in "pat" will be Unicode (regardless of whether the + # route_re regex pattern is itself Unicode or str) + pat.reverse() + rpat = [] + gen = [] + prefix = pat.pop() # invar: always at least one element (route='/'+route) + + # We want to generate URL-encoded URLs, so we url-quote the prefix, being + # careful not to quote any embedded slashes. We have to replace '%' with + # '%%' afterwards, as the strings that go into "gen" are used as string + # replacement targets. + gen.append(quote_path_segment(prefix, safe='/').replace('%', '%%')) # native + rpat.append(re.escape(prefix)) # unicode + + while pat: + name = pat.pop() # unicode + name = name[1:-1] + if ':' in name: + # reg may contain colons as well, + # so we must strictly split name into two parts + name, reg = name.split(':', 1) + else: + reg = '[^/]+' + gen.append('%%(%s)s' % native_(name)) # native + name = '(?P<%s>%s)' % (name, reg) # unicode + rpat.append(name) + s = pat.pop() # unicode + if s: + rpat.append(re.escape(s)) # unicode + # We want to generate URL-encoded URLs, so we url-quote this + # literal in the pattern, being careful not to quote the embedded + # slashes. We have to replace '%' with '%%' afterwards, as the + # strings that go into "gen" are used as string replacement + # targets. What is appended to gen is a native string. + gen.append(quote_path_segment(s, safe='/').replace('%', '%%')) + + if remainder: + rpat.append('(?P<%s>.*?)' % remainder) # unicode + gen.append('%%(%s)s' % native_(remainder)) # native + + pattern = ''.join(rpat) + '$' # unicode + + match = re.compile(pattern).match + def matcher(path): + # This function really wants to consume Unicode patterns natively, + # but if someone passes us a bytestring, we allow it by converting it + # to Unicode using the ASCII decoding. We decode it using ASCII + # because we don't want to accept bytestrings with high-order + # characters in them here as we have no idea what the encoding + # represents. + if path.__class__ is not text_type: + path = text_(path, 'ascii') + m = match(path) + if m is None: + return None + d = {} + for k, v in m.groupdict().items(): + # k and v will be Unicode 2.6.4 and lower doesnt accept unicode + # kwargs as **kw, so we explicitly cast the keys to native + # strings in case someone wants to pass the result as **kw + nk = native_(k, 'ascii') + if k == remainder: + d[nk] = split_path_info(v) + else: + d[nk] = v + return d + + gen = ''.join(gen) + + def q(v): + return quote_path_segment(v, safe=PATH_SAFE) + + def generator(dict): + newdict = {} + for k, v in dict.items(): + if PY2: + if v.__class__ is text_type: + # url_quote below needs bytes, not unicode on Py2 + v = v.encode('utf-8') + else: + if v.__class__ is binary_type: + # url_quote below needs a native string, not bytes on Py3 + v = v.decode('utf-8') + + if k == remainder: + # a stararg argument + if is_nonstr_iter(v): + v = '/'.join( + [q(x) for x in v] + ) # native + else: + if v.__class__ not in string_types: + v = str(v) + v = q(v) + else: + if v.__class__ not in string_types: + v = str(v) + # v may be bytes (py2) or native string (py3) + v = q(v) + + # at this point, the value will be a native string + newdict[k] = v + + result = gen % newdict # native string result + return result + + return matcher, generator diff --git a/src/pyramid/util.py b/src/pyramid/util.py new file mode 100644 index 000000000..6655455bf --- /dev/null +++ b/src/pyramid/util.py @@ -0,0 +1,651 @@ +from contextlib import contextmanager +import functools +try: + # py2.7.7+ and py3.3+ have native comparison support + from hmac import compare_digest +except ImportError: # pragma: no cover + compare_digest = None +import inspect +import weakref + +from pyramid.exceptions import ( + ConfigurationError, + CyclicDependencyError, + ) + +from pyramid.compat import ( + getargspec, + im_func, + is_nonstr_iter, + integer_types, + string_types, + bytes_, + text_, + PY2, + native_ + ) + +from pyramid.path import DottedNameResolver as _DottedNameResolver + +_marker = object() + + +class DottedNameResolver(_DottedNameResolver): + def __init__(self, package=None): # default to package = None for bw compat + _DottedNameResolver.__init__(self, package) + +def is_string_or_iterable(v): + if isinstance(v, string_types): + return True + if hasattr(v, '__iter__'): + return True + +def as_sorted_tuple(val): + if not is_nonstr_iter(val): + val = (val,) + val = tuple(sorted(val)) + return val + +class InstancePropertyHelper(object): + """A helper object for assigning properties and descriptors to instances. + It is not normally possible to do this because descriptors must be + defined on the class itself. + + This class is optimized for adding multiple properties at once to an + instance. This is done by calling :meth:`.add_property` once + per-property and then invoking :meth:`.apply` on target objects. + + """ + def __init__(self): + self.properties = {} + + @classmethod + def make_property(cls, callable, name=None, reify=False): + """ Convert a callable into one suitable for adding to the + instance. This will return a 2-tuple containing the computed + (name, property) pair. + """ + + is_property = isinstance(callable, property) + if is_property: + fn = callable + if name is None: + raise ValueError('must specify "name" for a property') + if reify: + raise ValueError('cannot reify a property') + elif name is not None: + fn = lambda this: callable(this) + fn.__name__ = get_callable_name(name) + fn.__doc__ = callable.__doc__ + else: + name = callable.__name__ + fn = callable + if reify: + import pyramid.decorator # avoid circular import + fn = pyramid.decorator.reify(fn) + elif not is_property: + fn = property(fn) + + return name, fn + + @classmethod + def apply_properties(cls, target, properties): + """Accept a list or dict of ``properties`` generated from + :meth:`.make_property` and apply them to a ``target`` object. + """ + attrs = dict(properties) + if attrs: + parent = target.__class__ + # fix the module name so it appears to still be the parent + # e.g. pyramid.request instead of pyramid.util + attrs.setdefault('__module__', parent.__module__) + newcls = type(parent.__name__, (parent, object), attrs) + # We assign __provides__ and __implemented__ below to prevent a + # memory leak that results from from the usage of this instance's + # eventual use in an adapter lookup. Adapter lookup results in + # ``zope.interface.implementedBy`` being called with the + # newly-created class as an argument. Because the newly-created + # class has no interface specification data of its own, lookup + # causes new ClassProvides and Implements instances related to our + # just-generated class to be created and set into the newly-created + # class' __dict__. We don't want these instances to be created; we + # want this new class to behave exactly like it is the parent class + # instead. See GitHub issues #1212, #1529 and #1568 for more + # information. + for name in ('__implemented__', '__provides__'): + # we assign these attributes conditionally to make it possible + # to test this class in isolation without having any interfaces + # attached to it + val = getattr(parent, name, _marker) + if val is not _marker: + setattr(newcls, name, val) + target.__class__ = newcls + + @classmethod + def set_property(cls, target, callable, name=None, reify=False): + """A helper method to apply a single property to an instance.""" + prop = cls.make_property(callable, name=name, reify=reify) + cls.apply_properties(target, [prop]) + + def add_property(self, callable, name=None, reify=False): + """Add a new property configuration. + + This should be used in combination with :meth:`.apply` as a + more efficient version of :meth:`.set_property`. + """ + name, fn = self.make_property(callable, name=name, reify=reify) + self.properties[name] = fn + + def apply(self, target): + """ Apply all configured properties to the ``target`` instance.""" + if self.properties: + self.apply_properties(target, self.properties) + +class InstancePropertyMixin(object): + """ Mixin that will allow an instance to add properties at + run-time as if they had been defined via @property or @reify + on the class itself. + """ + + def set_property(self, callable, name=None, reify=False): + """ Add a callable or a property descriptor to the instance. + + Properties, unlike attributes, are lazily evaluated by executing + an underlying callable when accessed. They can be useful for + adding features to an object without any cost if those features + go unused. + + A property may also be reified via the + :class:`pyramid.decorator.reify` decorator by setting + ``reify=True``, allowing the result of the evaluation to be + cached. Using this method, the value of the property is only + computed once for the lifetime of the object. + + ``callable`` can either be a callable that accepts the instance + as its single positional parameter, or it can be a property + descriptor. + + If the ``callable`` is a property descriptor, the ``name`` + parameter must be supplied or a ``ValueError`` will be raised. + Also note that a property descriptor cannot be reified, so + ``reify`` must be ``False``. + + If ``name`` is None, the name of the property will be computed + from the name of the ``callable``. + + .. code-block:: python + :linenos: + + class Foo(InstancePropertyMixin): + _x = 1 + + def _get_x(self): + return _x + + def _set_x(self, value): + self._x = value + + foo = Foo() + foo.set_property(property(_get_x, _set_x), name='x') + foo.set_property(_get_x, name='y', reify=True) + + >>> foo.x + 1 + >>> foo.y + 1 + >>> foo.x = 5 + >>> foo.x + 5 + >>> foo.y # notice y keeps the original value + 1 + """ + InstancePropertyHelper.set_property( + self, callable, name=name, reify=reify) + +class WeakOrderedSet(object): + """ Maintain a set of items. + + Each item is stored as a weakref to avoid extending their lifetime. + + The values may be iterated over or the last item added may be + accessed via the ``last`` property. + + If items are added more than once, the most recent addition will + be remembered in the order: + + order = WeakOrderedSet() + order.add('1') + order.add('2') + order.add('1') + + list(order) == ['2', '1'] + order.last == '1' + """ + + def __init__(self): + self._items = {} + self._order = [] + + def add(self, item): + """ Add an item to the set.""" + oid = id(item) + if oid in self._items: + self._order.remove(oid) + self._order.append(oid) + return + ref = weakref.ref(item, lambda x: self._remove_by_id(oid)) + self._items[oid] = ref + self._order.append(oid) + + def _remove_by_id(self, oid): + """ Remove an item from the set.""" + if oid in self._items: + del self._items[oid] + self._order.remove(oid) + + def remove(self, item): + """ Remove an item from the set.""" + self._remove_by_id(id(item)) + + def empty(self): + """ Clear all objects from the set.""" + self._items = {} + self._order = [] + + def __len__(self): + return len(self._order) + + def __contains__(self, item): + oid = id(item) + return oid in self._items + + def __iter__(self): + return (self._items[oid]() for oid in self._order) + + @property + def last(self): + if self._order: + oid = self._order[-1] + return self._items[oid]() + +def strings_differ(string1, string2, compare_digest=compare_digest): + """Check whether two strings differ while avoiding timing attacks. + + This function returns True if the given strings differ and False + if they are equal. It's careful not to leak information about *where* + they differ as a result of its running time, which can be very important + to avoid certain timing-related crypto attacks: + + http://seb.dbzteam.org/crypto/python-oauth-timing-hmac.pdf + + .. versionchanged:: 1.6 + Support :func:`hmac.compare_digest` if it is available (Python 2.7.7+ + and Python 3.3+). + + """ + len_eq = len(string1) == len(string2) + if len_eq: + invalid_bits = 0 + left = string1 + else: + invalid_bits = 1 + left = string2 + right = string2 + + if compare_digest is not None: + invalid_bits += not compare_digest(left, right) + else: + for a, b in zip(left, right): + invalid_bits += a != b + return invalid_bits != 0 + +def object_description(object): + """ Produce a human-consumable text description of ``object``, + usually involving a Python dotted name. For example: + + >>> object_description(None) + u'None' + >>> from xml.dom import minidom + >>> object_description(minidom) + u'module xml.dom.minidom' + >>> object_description(minidom.Attr) + u'class xml.dom.minidom.Attr' + >>> object_description(minidom.Attr.appendChild) + u'method appendChild of class xml.dom.minidom.Attr' + + If this method cannot identify the type of the object, a generic + description ala ``object `` will be returned. + + If the object passed is already a string, it is simply returned. If it + is a boolean, an integer, a list, a tuple, a set, or ``None``, a + (possibly shortened) string representation is returned. + """ + if isinstance(object, string_types): + return text_(object) + if isinstance(object, integer_types): + return text_(str(object)) + if isinstance(object, (bool, float, type(None))): + return text_(str(object)) + if isinstance(object, set): + if PY2: + return shortrepr(object, ')') + else: + return shortrepr(object, '}') + if isinstance(object, tuple): + return shortrepr(object, ')') + if isinstance(object, list): + return shortrepr(object, ']') + if isinstance(object, dict): + return shortrepr(object, '}') + module = inspect.getmodule(object) + if module is None: + return text_('object %s' % str(object)) + modulename = module.__name__ + if inspect.ismodule(object): + return text_('module %s' % modulename) + if inspect.ismethod(object): + oself = getattr(object, '__self__', None) + if oself is None: # pragma: no cover + oself = getattr(object, 'im_self', None) + return text_('method %s of class %s.%s' % + (object.__name__, modulename, + oself.__class__.__name__)) + + if inspect.isclass(object): + dottedname = '%s.%s' % (modulename, object.__name__) + return text_('class %s' % dottedname) + if inspect.isfunction(object): + dottedname = '%s.%s' % (modulename, object.__name__) + return text_('function %s' % dottedname) + return text_('object %s' % str(object)) + +def shortrepr(object, closer): + r = str(object) + if len(r) > 100: + r = r[:100] + ' ... %s' % closer + return r + +class Sentinel(object): + def __init__(self, repr): + self.repr = repr + + def __repr__(self): + return self.repr + +FIRST = Sentinel('FIRST') +LAST = Sentinel('LAST') + +class TopologicalSorter(object): + """ A utility class which can be used to perform topological sorts against + tuple-like data.""" + def __init__( + self, + default_before=LAST, + default_after=None, + first=FIRST, + last=LAST, + ): + self.names = [] + self.req_before = set() + self.req_after = set() + self.name2before = {} + self.name2after = {} + self.name2val = {} + self.order = [] + self.default_before = default_before + self.default_after = default_after + self.first = first + self.last = last + + def values(self): + return self.name2val.values() + + def remove(self, name): + """ Remove a node from the sort input """ + self.names.remove(name) + del self.name2val[name] + after = self.name2after.pop(name, []) + if after: + self.req_after.remove(name) + for u in after: + self.order.remove((u, name)) + before = self.name2before.pop(name, []) + if before: + self.req_before.remove(name) + for u in before: + self.order.remove((name, u)) + + def add(self, name, val, after=None, before=None): + """ Add a node to the sort input. The ``name`` should be a string or + any other hashable object, the ``val`` should be the sortable (doesn't + need to be hashable). ``after`` and ``before`` represents the name of + one of the other sortables (or a sequence of such named) or one of the + special sentinel values :attr:`pyramid.util.FIRST`` or + :attr:`pyramid.util.LAST` representing the first or last positions + respectively. ``FIRST`` and ``LAST`` can also be part of a sequence + passed as ``before`` or ``after``. A sortable should not be added + after LAST or before FIRST. An example:: + + sorter = TopologicalSorter() + sorter.add('a', {'a':1}, before=LAST, after='b') + sorter.add('b', {'b':2}, before=LAST, after='c') + sorter.add('c', {'c':3}) + + sorter.sorted() # will be {'c':3}, {'b':2}, {'a':1} + + """ + if name in self.names: + self.remove(name) + self.names.append(name) + self.name2val[name] = val + if after is None and before is None: + before = self.default_before + after = self.default_after + if after is not None: + if not is_nonstr_iter(after): + after = (after,) + self.name2after[name] = after + self.order += [(u, name) for u in after] + self.req_after.add(name) + if before is not None: + if not is_nonstr_iter(before): + before = (before,) + self.name2before[name] = before + self.order += [(name, o) for o in before] + self.req_before.add(name) + + + def sorted(self): + """ Returns the sort input values in topologically sorted order""" + order = [(self.first, self.last)] + roots = [] + graph = {} + names = [self.first, self.last] + names.extend(self.names) + + for a, b in self.order: + order.append((a, b)) + + def add_node(node): + if node not in graph: + roots.append(node) + graph[node] = [0] # 0 = number of arcs coming into this node + + def add_arc(fromnode, tonode): + graph[fromnode].append(tonode) + graph[tonode][0] += 1 + if tonode in roots: + roots.remove(tonode) + + for name in names: + add_node(name) + + has_before, has_after = set(), set() + for a, b in order: + if a in names and b in names: # deal with missing dependencies + add_arc(a, b) + has_before.add(a) + has_after.add(b) + + if not self.req_before.issubset(has_before): + raise ConfigurationError( + 'Unsatisfied before dependencies: %s' + % (', '.join(sorted(self.req_before - has_before))) + ) + if not self.req_after.issubset(has_after): + raise ConfigurationError( + 'Unsatisfied after dependencies: %s' + % (', '.join(sorted(self.req_after - has_after))) + ) + + sorted_names = [] + + while roots: + root = roots.pop(0) + sorted_names.append(root) + children = graph[root][1:] + for child in children: + arcs = graph[child][0] + arcs -= 1 + graph[child][0] = arcs + if arcs == 0: + roots.insert(0, child) + del graph[root] + + if graph: + # loop in input + cycledeps = {} + for k, v in graph.items(): + cycledeps[k] = v[1:] + raise CyclicDependencyError(cycledeps) + + result = [] + + for name in sorted_names: + if name in self.names: + result.append((name, self.name2val[name])) + + return result + + +def get_callable_name(name): + """ + Verifies that the ``name`` is ascii and will raise a ``ConfigurationError`` + if it is not. + """ + try: + return native_(name, 'ascii') + except (UnicodeEncodeError, UnicodeDecodeError): + msg = ( + '`name="%s"` is invalid. `name` must be ascii because it is ' + 'used on __name__ of the method' + ) + raise ConfigurationError(msg % name) + +@contextmanager +def hide_attrs(obj, *attrs): + """ + Temporarily delete object attrs and restore afterward. + """ + obj_vals = obj.__dict__ if obj is not None else {} + saved_vals = {} + for name in attrs: + saved_vals[name] = obj_vals.pop(name, _marker) + try: + yield + finally: + for name in attrs: + saved_val = saved_vals[name] + if saved_val is not _marker: + obj_vals[name] = saved_val + elif name in obj_vals: + del obj_vals[name] + + +def is_same_domain(host, pattern): + """ + Return ``True`` if the host is either an exact match or a match + to the wildcard pattern. + Any pattern beginning with a period matches a domain and all of its + subdomains. (e.g. ``.example.com`` matches ``example.com`` and + ``foo.example.com``). Anything else is an exact string match. + """ + if not pattern: + return False + + pattern = pattern.lower() + return (pattern[0] == "." and + (host.endswith(pattern) or host == pattern[1:]) or + pattern == host) + + +def make_contextmanager(fn): + if inspect.isgeneratorfunction(fn): + return contextmanager(fn) + + if fn is None: + fn = lambda *a, **kw: None + + @contextmanager + @functools.wraps(fn) + def wrapper(*a, **kw): + yield fn(*a, **kw) + return wrapper + + +def takes_one_arg(callee, attr=None, argname=None): + ismethod = False + if attr is None: + attr = '__call__' + if inspect.isroutine(callee): + fn = callee + elif inspect.isclass(callee): + try: + fn = callee.__init__ + except AttributeError: + return False + ismethod = hasattr(fn, '__call__') + else: + try: + fn = getattr(callee, attr) + except AttributeError: + return False + + try: + argspec = getargspec(fn) + except TypeError: + return False + + args = argspec[0] + + if hasattr(fn, im_func) or ismethod: + # it's an instance method (or unbound method on py2) + if not args: + return False + args = args[1:] + + if not args: + return False + + if len(args) == 1: + return True + + if argname: + + defaults = argspec[3] + if defaults is None: + defaults = () + + if args[0] == argname: + if len(args) - len(defaults) == 1: + return True + + return False + + +class SimpleSerializer(object): + def loads(self, bstruct): + return native_(bstruct) + + def dumps(self, appstruct): + return bytes_(appstruct) diff --git a/src/pyramid/view.py b/src/pyramid/view.py new file mode 100644 index 000000000..769328344 --- /dev/null +++ b/src/pyramid/view.py @@ -0,0 +1,761 @@ +import itertools +import sys + +import venusian + +from zope.interface import providedBy + +from pyramid.interfaces import ( + IRoutesMapper, + IMultiView, + ISecuredView, + IView, + IViewClassifier, + IRequest, + IExceptionViewClassifier, + ) + +from pyramid.compat import decode_path_info +from pyramid.compat import reraise as reraise_ + +from pyramid.exceptions import ( + ConfigurationError, + PredicateMismatch, +) + +from pyramid.httpexceptions import ( + HTTPNotFound, + HTTPTemporaryRedirect, + default_exceptionresponse_view, + ) + +from pyramid.threadlocal import ( + get_current_registry, + manager, + ) + +from pyramid.util import hide_attrs + +_marker = object() + +def render_view_to_response(context, request, name='', secure=True): + """ Call the :term:`view callable` configured with a :term:`view + configuration` that matches the :term:`view name` ``name`` + registered against the specified ``context`` and ``request`` and + return a :term:`response` object. This function will return + ``None`` if a corresponding :term:`view callable` cannot be found + (when no :term:`view configuration` matches the combination of + ``name`` / ``context`` / and ``request``). + + If `secure`` is ``True``, and the :term:`view callable` found is + protected by a permission, the permission will be checked before calling + the view function. If the permission check disallows view execution + (based on the current :term:`authorization policy`), a + :exc:`pyramid.httpexceptions.HTTPForbidden` exception will be raised. + The exception's ``args`` attribute explains why the view access was + disallowed. + + If ``secure`` is ``False``, no permission checking is done.""" + + registry = getattr(request, 'registry', None) + if registry is None: + registry = get_current_registry() + + context_iface = providedBy(context) + # We explicitly pass in the interfaces provided by the request as + # request_iface to _call_view; we don't want _call_view to use + # request.request_iface, because render_view_to_response and friends are + # pretty much limited to finding views that are not views associated with + # routes, and the only thing request.request_iface is used for is to find + # route-based views. The render_view_to_response API is (and always has + # been) a stepchild API reserved for use of those who actually use + # traversal. Doing this fixes an infinite recursion bug introduced in + # Pyramid 1.6a1, and causes the render_view* APIs to behave as they did in + # 1.5 and previous. We should probably provide some sort of different API + # that would allow people to find views for routes. See + # https://github.com/Pylons/pyramid/issues/1643 for more info. + request_iface = providedBy(request) + + response = _call_view( + registry, + request, + context, + context_iface, + name, + secure=secure, + request_iface=request_iface, + ) + + return response # NB: might be None + + +def render_view_to_iterable(context, request, name='', secure=True): + """ Call the :term:`view callable` configured with a :term:`view + configuration` that matches the :term:`view name` ``name`` + registered against the specified ``context`` and ``request`` and + return an iterable object which represents the body of a response. + This function will return ``None`` if a corresponding :term:`view + callable` cannot be found (when no :term:`view configuration` + matches the combination of ``name`` / ``context`` / and + ``request``). Additionally, this function will raise a + :exc:`ValueError` if a view function is found and called but the + view function's result does not have an ``app_iter`` attribute. + + You can usually get the bytestring representation of the return value of + this function by calling ``b''.join(iterable)``, or just use + :func:`pyramid.view.render_view` instead. + + If ``secure`` is ``True``, and the view is protected by a permission, the + permission will be checked before the view function is invoked. If the + permission check disallows view execution (based on the current + :term:`authentication policy`), a + :exc:`pyramid.httpexceptions.HTTPForbidden` exception will be raised; its + ``args`` attribute explains why the view access was disallowed. + + If ``secure`` is ``False``, no permission checking is + done.""" + response = render_view_to_response(context, request, name, secure) + if response is None: + return None + return response.app_iter + +def render_view(context, request, name='', secure=True): + """ Call the :term:`view callable` configured with a :term:`view + configuration` that matches the :term:`view name` ``name`` + registered against the specified ``context`` and ``request`` + and unwind the view response's ``app_iter`` (see + :ref:`the_response`) into a single bytestring. This function will + return ``None`` if a corresponding :term:`view callable` cannot be + found (when no :term:`view configuration` matches the combination + of ``name`` / ``context`` / and ``request``). Additionally, this + function will raise a :exc:`ValueError` if a view function is + found and called but the view function's result does not have an + ``app_iter`` attribute. This function will return ``None`` if a + corresponding view cannot be found. + + If ``secure`` is ``True``, and the view is protected by a permission, the + permission will be checked before the view is invoked. If the permission + check disallows view execution (based on the current :term:`authorization + policy`), a :exc:`pyramid.httpexceptions.HTTPForbidden` exception will be + raised; its ``args`` attribute explains why the view access was + disallowed. + + If ``secure`` is ``False``, no permission checking is done.""" + iterable = render_view_to_iterable(context, request, name, secure) + if iterable is None: + return None + return b''.join(iterable) + +class view_config(object): + """ A function, class or method :term:`decorator` which allows a + developer to create view registrations nearer to a :term:`view + callable` definition than use :term:`imperative + configuration` to do the same. + + For example, this code in a module ``views.py``:: + + from resources import MyResource + + @view_config(name='my_view', context=MyResource, permission='read', + route_name='site1') + def my_view(context, request): + return 'OK' + + Might replace the following call to the + :meth:`pyramid.config.Configurator.add_view` method:: + + import views + from resources import MyResource + config.add_view(views.my_view, context=MyResource, name='my_view', + permission='read', route_name='site1') + + .. note: :class:`pyramid.view.view_config` is also importable, for + backwards compatibility purposes, as the name + :class:`pyramid.view.bfg_view`. + + :class:`pyramid.view.view_config` supports the following keyword + arguments: ``context``, ``exception``, ``permission``, ``name``, + ``request_type``, ``route_name``, ``request_method``, ``request_param``, + ``containment``, ``xhr``, ``accept``, ``header``, ``path_info``, + ``custom_predicates``, ``decorator``, ``mapper``, ``http_cache``, + ``require_csrf``, ``match_param``, ``check_csrf``, ``physical_path``, and + ``view_options``. + + The meanings of these arguments are the same as the arguments passed to + :meth:`pyramid.config.Configurator.add_view`. If any argument is left + out, its default will be the equivalent ``add_view`` default. + + Two additional keyword arguments which will be passed to the + :term:`venusian` ``attach`` function are ``_depth`` and ``_category``. + + ``_depth`` is provided for people who wish to reuse this class from another + decorator. The default value is ``0`` and should be specified relative to + the ``view_config`` invocation. It will be passed in to the + :term:`venusian` ``attach`` function as the depth of the callstack when + Venusian checks if the decorator is being used in a class or module + context. It's not often used, but it can be useful in this circumstance. + + ``_category`` sets the decorator category name. It can be useful in + combination with the ``category`` argument of ``scan`` to control which + views should be processed. + + See the :py:func:`venusian.attach` function in Venusian for more + information about the ``_depth`` and ``_category`` arguments. + + .. seealso:: + + See also :ref:`mapping_views_using_a_decorator_section` for + details about using :class:`pyramid.view.view_config`. + + .. warning:: + + ``view_config`` will work ONLY on module top level members + because of the limitation of ``venusian.Scanner.scan``. + + """ + venusian = venusian # for testing injection + def __init__(self, **settings): + if 'for_' in settings: + if settings.get('context') is None: + settings['context'] = settings['for_'] + self.__dict__.update(settings) + + def __call__(self, wrapped): + settings = self.__dict__.copy() + depth = settings.pop('_depth', 0) + category = settings.pop('_category', 'pyramid') + + def callback(context, name, ob): + config = context.config.with_package(info.module) + config.add_view(view=ob, **settings) + + info = self.venusian.attach(wrapped, callback, category=category, + depth=depth + 1) + + if info.scope == 'class': + # if the decorator was attached to a method in a class, or + # otherwise executed at class scope, we need to set an + # 'attr' into the settings if one isn't already in there + if settings.get('attr') is None: + settings['attr'] = wrapped.__name__ + + settings['_info'] = info.codeinfo # fbo "action_method" + return wrapped + +bfg_view = view_config # bw compat (forever) + +class view_defaults(view_config): + """ A class :term:`decorator` which, when applied to a class, will + provide defaults for all view configurations that use the class. This + decorator accepts all the arguments accepted by + :meth:`pyramid.view.view_config`, and each has the same meaning. + + See :ref:`view_defaults` for more information. + """ + + def __call__(self, wrapped): + wrapped.__view_defaults__ = self.__dict__.copy() + return wrapped + +class AppendSlashNotFoundViewFactory(object): + """ There can only be one :term:`Not Found view` in any + :app:`Pyramid` application. Even if you use + :func:`pyramid.view.append_slash_notfound_view` as the Not + Found view, :app:`Pyramid` still must generate a ``404 Not + Found`` response when it cannot redirect to a slash-appended URL; + this not found response will be visible to site users. + + If you don't care what this 404 response looks like, and you only + need redirections to slash-appended route URLs, you may use the + :func:`pyramid.view.append_slash_notfound_view` object as the + Not Found view. However, if you wish to use a *custom* notfound + view callable when a URL cannot be redirected to a slash-appended + URL, you may wish to use an instance of this class as the Not + Found view, supplying a :term:`view callable` to be used as the + custom notfound view as the first argument to its constructor. + For instance: + + .. code-block:: python + + from pyramid.httpexceptions import HTTPNotFound + from pyramid.view import AppendSlashNotFoundViewFactory + + def notfound_view(context, request): return HTTPNotFound('nope') + + custom_append_slash = AppendSlashNotFoundViewFactory(notfound_view) + config.add_view(custom_append_slash, context=HTTPNotFound) + + The ``notfound_view`` supplied must adhere to the two-argument + view callable calling convention of ``(context, request)`` + (``context`` will be the exception object). + + .. deprecated:: 1.3 + + """ + def __init__(self, notfound_view=None, redirect_class=HTTPTemporaryRedirect): + if notfound_view is None: + notfound_view = default_exceptionresponse_view + self.notfound_view = notfound_view + self.redirect_class = redirect_class + + def __call__(self, context, request): + path = decode_path_info(request.environ['PATH_INFO'] or '/') + registry = request.registry + mapper = registry.queryUtility(IRoutesMapper) + if mapper is not None and not path.endswith('/'): + slashpath = path + '/' + for route in mapper.get_routes(): + if route.match(slashpath) is not None: + qs = request.query_string + if qs: + qs = '?' + qs + return self.redirect_class(location=request.path + '/' + qs) + return self.notfound_view(context, request) + +append_slash_notfound_view = AppendSlashNotFoundViewFactory() +append_slash_notfound_view.__doc__ = """\ +For behavior like Django's ``APPEND_SLASH=True``, use this view as the +:term:`Not Found view` in your application. + +When this view is the Not Found view (indicating that no view was found), and +any routes have been defined in the configuration of your application, if the +value of the ``PATH_INFO`` WSGI environment variable does not already end in +a slash, and if the value of ``PATH_INFO`` *plus* a slash matches any route's +path, do an HTTP redirect to the slash-appended PATH_INFO. Note that this +will *lose* ``POST`` data information (turning it into a GET), so you +shouldn't rely on this to redirect POST requests. Note also that static +routes are not considered when attempting to find a matching route. + +Use the :meth:`pyramid.config.Configurator.add_view` method to configure this +view as the Not Found view:: + + from pyramid.httpexceptions import HTTPNotFound + from pyramid.view import append_slash_notfound_view + config.add_view(append_slash_notfound_view, context=HTTPNotFound) + +.. deprecated:: 1.3 + +""" + +class notfound_view_config(object): + """ + .. versionadded:: 1.3 + + An analogue of :class:`pyramid.view.view_config` which registers a + :term:`Not Found View` using + :meth:`pyramid.config.Configurator.add_notfound_view`. + + The ``notfound_view_config`` constructor accepts most of the same arguments + as the constructor of :class:`pyramid.view.view_config`. It can be used + in the same places, and behaves in largely the same way, except it always + registers a not found exception view instead of a 'normal' view. + + Example: + + .. code-block:: python + + from pyramid.view import notfound_view_config + from pyramid.response import Response + + @notfound_view_config() + def notfound(request): + return Response('Not found!', status='404 Not Found') + + All arguments except ``append_slash`` have the same meaning as + :meth:`pyramid.view.view_config` and each predicate + argument restricts the set of circumstances under which this notfound + view will be invoked. + + If ``append_slash`` is ``True``, when the Not Found View is invoked, and + the current path info does not end in a slash, the notfound logic will + attempt to find a :term:`route` that matches the request's path info + suffixed with a slash. If such a route exists, Pyramid will issue a + redirect to the URL implied by the route; if it does not, Pyramid will + return the result of the view callable provided as ``view``, as normal. + + If the argument provided as ``append_slash`` is not a boolean but + instead implements :class:`~pyramid.interfaces.IResponse`, the + append_slash logic will behave as if ``append_slash=True`` was passed, + but the provided class will be used as the response class instead of + the default :class:`~pyramid.httpexceptions.HTTPTemporaryRedirect` + response class when a redirect is performed. For example: + + .. code-block:: python + + from pyramid.httpexceptions import ( + HTTPMovedPermanently, + HTTPNotFound + ) + + @notfound_view_config(append_slash=HTTPMovedPermanently) + def aview(request): + return HTTPNotFound('not found') + + The above means that a redirect to a slash-appended route will be + attempted, but instead of :class:`~pyramid.httpexceptions.HTTPTemporaryRedirect` + being used, :class:`~pyramid.httpexceptions.HTTPMovedPermanently will + be used` for the redirect response if a slash-appended route is found. + + See :ref:`changing_the_notfound_view` for detailed usage information. + + .. versionchanged:: 1.9.1 + Added the ``_depth`` and ``_category`` arguments. + + """ + + venusian = venusian + + def __init__(self, **settings): + self.__dict__.update(settings) + + def __call__(self, wrapped): + settings = self.__dict__.copy() + depth = settings.pop('_depth', 0) + category = settings.pop('_category', 'pyramid') + + def callback(context, name, ob): + config = context.config.with_package(info.module) + config.add_notfound_view(view=ob, **settings) + + info = self.venusian.attach(wrapped, callback, category=category, + depth=depth + 1) + + if info.scope == 'class': + # if the decorator was attached to a method in a class, or + # otherwise executed at class scope, we need to set an + # 'attr' into the settings if one isn't already in there + if settings.get('attr') is None: + settings['attr'] = wrapped.__name__ + + settings['_info'] = info.codeinfo # fbo "action_method" + return wrapped + +class forbidden_view_config(object): + """ + .. versionadded:: 1.3 + + An analogue of :class:`pyramid.view.view_config` which registers a + :term:`forbidden view` using + :meth:`pyramid.config.Configurator.add_forbidden_view`. + + The forbidden_view_config constructor accepts most of the same arguments + as the constructor of :class:`pyramid.view.view_config`. It can be used + in the same places, and behaves in largely the same way, except it always + registers a forbidden exception view instead of a 'normal' view. + + Example: + + .. code-block:: python + + from pyramid.view import forbidden_view_config + from pyramid.response import Response + + @forbidden_view_config() + def forbidden(request): + return Response('You are not allowed', status='403 Forbidden') + + All arguments passed to this function have the same meaning as + :meth:`pyramid.view.view_config` and each predicate argument restricts + the set of circumstances under which this notfound view will be invoked. + + See :ref:`changing_the_forbidden_view` for detailed usage information. + + .. versionchanged:: 1.9.1 + Added the ``_depth`` and ``_category`` arguments. + + """ + + venusian = venusian + + def __init__(self, **settings): + self.__dict__.update(settings) + + def __call__(self, wrapped): + settings = self.__dict__.copy() + depth = settings.pop('_depth', 0) + category = settings.pop('_category', 'pyramid') + + def callback(context, name, ob): + config = context.config.with_package(info.module) + config.add_forbidden_view(view=ob, **settings) + + info = self.venusian.attach(wrapped, callback, category=category, + depth=depth + 1) + + if info.scope == 'class': + # if the decorator was attached to a method in a class, or + # otherwise executed at class scope, we need to set an + # 'attr' into the settings if one isn't already in there + if settings.get('attr') is None: + settings['attr'] = wrapped.__name__ + + settings['_info'] = info.codeinfo # fbo "action_method" + return wrapped + +class exception_view_config(object): + """ + .. versionadded:: 1.8 + + An analogue of :class:`pyramid.view.view_config` which registers an + :term:`exception view` using + :meth:`pyramid.config.Configurator.add_exception_view`. + + The ``exception_view_config`` constructor requires an exception context, + and additionally accepts most of the same arguments as the constructor of + :class:`pyramid.view.view_config`. It can be used in the same places, + and behaves in largely the same way, except it always registers an + exception view instead of a "normal" view that dispatches on the request + :term:`context`. + + Example: + + .. code-block:: python + + from pyramid.view import exception_view_config + from pyramid.response import Response + + @exception_view_config(ValueError, renderer='json') + def error_view(request): + return {'error': str(request.exception)} + + All arguments passed to this function have the same meaning as + :meth:`pyramid.view.view_config`, and each predicate argument restricts + the set of circumstances under which this exception view will be invoked. + + .. versionchanged:: 1.9.1 + Added the ``_depth`` and ``_category`` arguments. + + """ + venusian = venusian + + def __init__(self, *args, **settings): + if 'context' not in settings and len(args) > 0: + exception, args = args[0], args[1:] + settings['context'] = exception + if len(args) > 0: + raise ConfigurationError('unknown positional arguments') + self.__dict__.update(settings) + + def __call__(self, wrapped): + settings = self.__dict__.copy() + depth = settings.pop('_depth', 0) + category = settings.pop('_category', 'pyramid') + + def callback(context, name, ob): + config = context.config.with_package(info.module) + config.add_exception_view(view=ob, **settings) + + info = self.venusian.attach(wrapped, callback, category=category, + depth=depth + 1) + + if info.scope == 'class': + # if the decorator was attached to a method in a class, or + # otherwise executed at class scope, we need to set an + # 'attr' in the settings if one isn't already in there + if settings.get('attr') is None: + settings['attr'] = wrapped.__name__ + + settings['_info'] = info.codeinfo # fbo "action_method" + return wrapped + +def _find_views( + registry, + request_iface, + context_iface, + view_name, + view_types=None, + view_classifier=None, + ): + if view_types is None: + view_types = (IView, ISecuredView, IMultiView) + if view_classifier is None: + view_classifier = IViewClassifier + registered = registry.adapters.registered + cache = registry._view_lookup_cache + views = cache.get((request_iface, context_iface, view_name)) + if views is None: + views = [] + for req_type, ctx_type in itertools.product( + request_iface.__sro__, context_iface.__sro__ + ): + source_ifaces = (view_classifier, req_type, ctx_type) + for view_type in view_types: + view_callable = registered( + source_ifaces, + view_type, + name=view_name, + ) + if view_callable is not None: + views.append(view_callable) + if views: + # do not cache view lookup misses. rationale: dont allow cache to + # grow without bound if somebody tries to hit the site with many + # missing URLs. we could use an LRU cache instead, but then + # purposeful misses by an attacker would just blow out the cache + # anyway. downside: misses will almost always consume more CPU than + # hits in steady state. + with registry._lock: + cache[(request_iface, context_iface, view_name)] = views + + return views + +def _call_view( + registry, + request, + context, + context_iface, + view_name, + view_types=None, + view_classifier=None, + secure=True, + request_iface=None, + ): + if request_iface is None: + request_iface = getattr(request, 'request_iface', IRequest) + view_callables = _find_views( + registry, + request_iface, + context_iface, + view_name, + view_types=view_types, + view_classifier=view_classifier, + ) + + pme = None + response = None + + for view_callable in view_callables: + # look for views that meet the predicate criteria + try: + if not secure: + # the view will have a __call_permissive__ attribute if it's + # secured; otherwise it won't. + view_callable = getattr( + view_callable, + '__call_permissive__', + view_callable + ) + + # if this view is secured, it will raise a Forbidden + # appropriately if the executing user does not have the proper + # permission + response = view_callable(context, request) + return response + except PredicateMismatch as _pme: + pme = _pme + + if pme is not None: + raise pme + + return response + +class ViewMethodsMixin(object): + """ Request methods mixin for BaseRequest having to do with executing + views """ + def invoke_exception_view( + self, + exc_info=None, + request=None, + secure=True, + reraise=False, + ): + """ Executes an exception view related to the request it's called upon. + The arguments it takes are these: + + ``exc_info`` + + If provided, should be a 3-tuple in the form provided by + ``sys.exc_info()``. If not provided, + ``sys.exc_info()`` will be called to obtain the current + interpreter exception information. Default: ``None``. + + ``request`` + + If the request to be used is not the same one as the instance that + this method is called upon, it may be passed here. Default: + ``None``. + + ``secure`` + + If the exception view should not be rendered if the current user + does not have the appropriate permission, this should be ``True``. + Default: ``True``. + + ``reraise`` + + A boolean indicating whether the original error should be reraised + if a :term:`response` object could not be created. If ``False`` + then an :class:`pyramid.httpexceptions.HTTPNotFound`` exception + will be raised. Default: ``False``. + + If a response is generated then ``request.exception`` and + ``request.exc_info`` will be left at the values used to render the + response. Otherwise the previous values for ``request.exception`` and + ``request.exc_info`` will be restored. + + .. versionadded:: 1.7 + + .. versionchanged:: 1.9 + The ``request.exception`` and ``request.exc_info`` properties will + reflect the exception used to render the response where previously + they were reset to the values prior to invoking the method. + + Also added the ``reraise`` argument. + + """ + if request is None: + request = self + registry = getattr(request, 'registry', None) + if registry is None: + registry = get_current_registry() + + if registry is None: + raise RuntimeError("Unable to retrieve registry") + + if exc_info is None: + exc_info = sys.exc_info() + + exc = exc_info[1] + attrs = request.__dict__ + context_iface = providedBy(exc) + + # clear old generated request.response, if any; it may + # have been mutated by the view, and its state is not + # sane (e.g. caching headers) + with hide_attrs(request, 'response', 'exc_info', 'exception'): + attrs['exception'] = exc + attrs['exc_info'] = exc_info + # we use .get instead of .__getitem__ below due to + # https://github.com/Pylons/pyramid/issues/700 + request_iface = attrs.get('request_iface', IRequest) + + manager.push({'request': request, 'registry': registry}) + + try: + response = _call_view( + registry, + request, + exc, + context_iface, + '', + view_types=None, + view_classifier=IExceptionViewClassifier, + secure=secure, + request_iface=request_iface.combined, + ) + except Exception: + if reraise: + reraise_(*exc_info) + raise + finally: + manager.pop() + + if response is None: + if reraise: + reraise_(*exc_info) + raise HTTPNotFound + + # successful response, overwrite exception/exc_info + attrs['exception'] = exc + attrs['exc_info'] = exc_info + return response diff --git a/src/pyramid/viewderivers.py b/src/pyramid/viewderivers.py new file mode 100644 index 000000000..d914a4752 --- /dev/null +++ b/src/pyramid/viewderivers.py @@ -0,0 +1,472 @@ +import inspect + +from zope.interface import ( + implementer, + provider, + ) + +from pyramid.security import NO_PERMISSION_REQUIRED +from pyramid.csrf import ( + check_csrf_origin, + check_csrf_token, +) +from pyramid.response import Response + +from pyramid.interfaces import ( + IAuthenticationPolicy, + IAuthorizationPolicy, + IDefaultCSRFOptions, + IDefaultPermission, + IDebugLogger, + IResponse, + IViewMapper, + IViewMapperFactory, + ) + +from pyramid.compat import ( + is_bound_method, + is_unbound_method, + ) + +from pyramid.exceptions import ( + ConfigurationError, + ) +from pyramid.httpexceptions import HTTPForbidden +from pyramid.util import ( + object_description, + takes_one_arg, +) +from pyramid.view import render_view_to_response +from pyramid import renderers + + +def view_description(view): + try: + return view.__text__ + except AttributeError: + # custom view mappers might not add __text__ + return object_description(view) + +def requestonly(view, attr=None): + return takes_one_arg(view, attr=attr, argname='request') + +@implementer(IViewMapper) +@provider(IViewMapperFactory) +class DefaultViewMapper(object): + def __init__(self, **kw): + self.attr = kw.get('attr') + + def __call__(self, view): + if is_unbound_method(view) and self.attr is None: + raise ConfigurationError(( + 'Unbound method calls are not supported, please set the class ' + 'as your `view` and the method as your `attr`' + )) + + if inspect.isclass(view): + view = self.map_class(view) + else: + view = self.map_nonclass(view) + return view + + def map_class(self, view): + ronly = requestonly(view, self.attr) + if ronly: + mapped_view = self.map_class_requestonly(view) + else: + mapped_view = self.map_class_native(view) + mapped_view.__text__ = 'method %s of %s' % ( + self.attr or '__call__', object_description(view)) + return mapped_view + + def map_nonclass(self, view): + # We do more work here than appears necessary to avoid wrapping the + # view unless it actually requires wrapping (to avoid function call + # overhead). + mapped_view = view + ronly = requestonly(view, self.attr) + if ronly: + mapped_view = self.map_nonclass_requestonly(view) + elif self.attr: + mapped_view = self.map_nonclass_attr(view) + if inspect.isroutine(mapped_view): + # This branch will be true if the view is a function or a method. + # We potentially mutate an unwrapped object here if it's a + # function. We do this to avoid function call overhead of + # injecting another wrapper. However, we must wrap if the + # function is a bound method because we can't set attributes on a + # bound method. + if is_bound_method(view): + _mapped_view = mapped_view + def mapped_view(context, request): + return _mapped_view(context, request) + if self.attr is not None: + mapped_view.__text__ = 'attr %s of %s' % ( + self.attr, object_description(view)) + else: + mapped_view.__text__ = object_description(view) + return mapped_view + + def map_class_requestonly(self, view): + # its a class that has an __init__ which only accepts request + attr = self.attr + def _class_requestonly_view(context, request): + inst = view(request) + request.__view__ = inst + if attr is None: + response = inst() + else: + response = getattr(inst, attr)() + return response + return _class_requestonly_view + + def map_class_native(self, view): + # its a class that has an __init__ which accepts both context and + # request + attr = self.attr + def _class_view(context, request): + inst = view(context, request) + request.__view__ = inst + if attr is None: + response = inst() + else: + response = getattr(inst, attr)() + return response + return _class_view + + def map_nonclass_requestonly(self, view): + # its a function that has a __call__ which accepts only a single + # request argument + attr = self.attr + def _requestonly_view(context, request): + if attr is None: + response = view(request) + else: + response = getattr(view, attr)(request) + return response + return _requestonly_view + + def map_nonclass_attr(self, view): + # its a function that has a __call__ which accepts both context and + # request, but still has an attr + def _attr_view(context, request): + response = getattr(view, self.attr)(context, request) + return response + return _attr_view + + +def wraps_view(wrapper): + def inner(view, info): + wrapper_view = wrapper(view, info) + return preserve_view_attrs(view, wrapper_view) + return inner + +def preserve_view_attrs(view, wrapper): + if view is None: + return wrapper + + if wrapper is view: + return view + + original_view = getattr(view, '__original_view__', None) + + if original_view is None: + original_view = view + + wrapper.__wraps__ = view + wrapper.__original_view__ = original_view + wrapper.__module__ = view.__module__ + wrapper.__doc__ = view.__doc__ + + try: + wrapper.__name__ = view.__name__ + except AttributeError: + wrapper.__name__ = repr(view) + + # attrs that may not exist on "view", but, if so, must be attached to + # "wrapped view" + for attr in ('__permitted__', '__call_permissive__', '__permission__', + '__predicated__', '__predicates__', '__accept__', + '__order__', '__text__'): + try: + setattr(wrapper, attr, getattr(view, attr)) + except AttributeError: + pass + + return wrapper + +def mapped_view(view, info): + mapper = info.options.get('mapper') + if mapper is None: + mapper = getattr(view, '__view_mapper__', None) + if mapper is None: + mapper = info.registry.queryUtility(IViewMapperFactory) + if mapper is None: + mapper = DefaultViewMapper + + mapped_view = mapper(**info.options)(view) + return mapped_view + +mapped_view.options = ('mapper', 'attr') + +def owrapped_view(view, info): + wrapper_viewname = info.options.get('wrapper') + viewname = info.options.get('name') + if not wrapper_viewname: + return view + def _owrapped_view(context, request): + response = view(context, request) + request.wrapped_response = response + request.wrapped_body = response.body + request.wrapped_view = view + wrapped_response = render_view_to_response(context, request, + wrapper_viewname) + if wrapped_response is None: + raise ValueError( + 'No wrapper view named %r found when executing view ' + 'named %r' % (wrapper_viewname, viewname)) + return wrapped_response + return _owrapped_view + +owrapped_view.options = ('name', 'wrapper') + +def http_cached_view(view, info): + if info.settings.get('prevent_http_cache', False): + return view + + seconds = info.options.get('http_cache') + + if seconds is None: + return view + + options = {} + + if isinstance(seconds, (tuple, list)): + try: + seconds, options = seconds + except ValueError: + raise ConfigurationError( + 'If http_cache parameter is a tuple or list, it must be ' + 'in the form (seconds, options); not %s' % (seconds,)) + + def wrapper(context, request): + response = view(context, request) + prevent_caching = getattr(response.cache_control, 'prevent_auto', + False) + if not prevent_caching: + response.cache_expires(seconds, **options) + return response + + return wrapper + +http_cached_view.options = ('http_cache',) + +def secured_view(view, info): + for wrapper in (_secured_view, _authdebug_view): + view = wraps_view(wrapper)(view, info) + return view + +secured_view.options = ('permission',) + +def _secured_view(view, info): + permission = explicit_val = info.options.get('permission') + if permission is None: + permission = info.registry.queryUtility(IDefaultPermission) + if permission == NO_PERMISSION_REQUIRED: + # allow views registered within configurations that have a + # default permission to explicitly override the default + # permission, replacing it with no permission at all + permission = None + + wrapped_view = view + authn_policy = info.registry.queryUtility(IAuthenticationPolicy) + authz_policy = info.registry.queryUtility(IAuthorizationPolicy) + + # no-op on exception-only views without an explicit permission + if explicit_val is None and info.exception_only: + return view + + if authn_policy and authz_policy and (permission is not None): + def permitted(context, request): + principals = authn_policy.effective_principals(request) + return authz_policy.permits(context, principals, permission) + def secured_view(context, request): + result = permitted(context, request) + if result: + return view(context, request) + view_name = getattr(view, '__name__', view) + msg = getattr( + request, 'authdebug_message', + 'Unauthorized: %s failed permission check' % view_name) + raise HTTPForbidden(msg, result=result) + wrapped_view = secured_view + wrapped_view.__call_permissive__ = view + wrapped_view.__permitted__ = permitted + wrapped_view.__permission__ = permission + + return wrapped_view + +def _authdebug_view(view, info): + wrapped_view = view + settings = info.settings + permission = explicit_val = info.options.get('permission') + if permission is None: + permission = info.registry.queryUtility(IDefaultPermission) + authn_policy = info.registry.queryUtility(IAuthenticationPolicy) + authz_policy = info.registry.queryUtility(IAuthorizationPolicy) + logger = info.registry.queryUtility(IDebugLogger) + + # no-op on exception-only views without an explicit permission + if explicit_val is None and info.exception_only: + return view + + if settings and settings.get('debug_authorization', False): + def authdebug_view(context, request): + view_name = getattr(request, 'view_name', None) + + if authn_policy and authz_policy: + if permission is NO_PERMISSION_REQUIRED: + msg = 'Allowed (NO_PERMISSION_REQUIRED)' + elif permission is None: + msg = 'Allowed (no permission registered)' + else: + principals = authn_policy.effective_principals(request) + msg = str(authz_policy.permits( + context, principals, permission)) + else: + msg = 'Allowed (no authorization policy in use)' + + view_name = getattr(request, 'view_name', None) + url = getattr(request, 'url', None) + msg = ('debug_authorization of url %s (view name %r against ' + 'context %r): %s' % (url, view_name, context, msg)) + if logger: + logger.debug(msg) + if request is not None: + request.authdebug_message = msg + return view(context, request) + wrapped_view = authdebug_view + + return wrapped_view + +def rendered_view(view, info): + # one way or another this wrapper must produce a Response (unless + # the renderer is a NullRendererHelper) + renderer = info.options.get('renderer') + if renderer is None: + # register a default renderer if you want super-dynamic + # rendering. registering a default renderer will also allow + # override_renderer to work if a renderer is left unspecified for + # a view registration. + def viewresult_to_response(context, request): + result = view(context, request) + if result.__class__ is Response: # common case + response = result + else: + response = info.registry.queryAdapterOrSelf(result, IResponse) + if response is None: + if result is None: + append = (' You may have forgotten to return a value ' + 'from the view callable.') + elif isinstance(result, dict): + append = (' You may have forgotten to define a ' + 'renderer in the view configuration.') + else: + append = '' + + msg = ('Could not convert return value of the view ' + 'callable %s into a response object. ' + 'The value returned was %r.' + append) + + raise ValueError(msg % (view_description(view), result)) + + return response + + return viewresult_to_response + + if renderer is renderers.null_renderer: + return view + + def rendered_view(context, request): + result = view(context, request) + if result.__class__ is Response: # potential common case + response = result + else: + # this must adapt, it can't do a simple interface check + # (avoid trying to render webob responses) + response = info.registry.queryAdapterOrSelf(result, IResponse) + if response is None: + attrs = getattr(request, '__dict__', {}) + if 'override_renderer' in attrs: + # renderer overridden by newrequest event or other + renderer_name = attrs.pop('override_renderer') + view_renderer = renderers.RendererHelper( + name=renderer_name, + package=info.package, + registry=info.registry) + else: + view_renderer = renderer.clone() + if '__view__' in attrs: + view_inst = attrs.pop('__view__') + else: + view_inst = getattr(view, '__original_view__', view) + response = view_renderer.render_view( + request, result, view_inst, context) + return response + + return rendered_view + +rendered_view.options = ('renderer',) + +def decorated_view(view, info): + decorator = info.options.get('decorator') + if decorator is None: + return view + return decorator(view) + +decorated_view.options = ('decorator',) + +def csrf_view(view, info): + explicit_val = info.options.get('require_csrf') + defaults = info.registry.queryUtility(IDefaultCSRFOptions) + if defaults is None: + default_val = False + token = 'csrf_token' + header = 'X-CSRF-Token' + safe_methods = frozenset(["GET", "HEAD", "OPTIONS", "TRACE"]) + callback = None + else: + default_val = defaults.require_csrf + token = defaults.token + header = defaults.header + safe_methods = defaults.safe_methods + callback = defaults.callback + + enabled = ( + explicit_val is True or + # fallback to the default val if not explicitly enabled + # but only if the view is not an exception view + ( + explicit_val is not False and default_val and + not info.exception_only + ) + ) + # disable if both header and token are disabled + enabled = enabled and (token or header) + wrapped_view = view + if enabled: + def csrf_view(context, request): + if ( + request.method not in safe_methods and + (callback is None or callback(request)) + ): + check_csrf_origin(request, raises=True) + check_csrf_token(request, token, header, raises=True) + return view(context, request) + wrapped_view = csrf_view + return wrapped_view + +csrf_view.options = ('require_csrf',) + +VIEW = 'VIEW' +INGRESS = 'INGRESS' diff --git a/src/pyramid/wsgi.py b/src/pyramid/wsgi.py new file mode 100644 index 000000000..1c1bded32 --- /dev/null +++ b/src/pyramid/wsgi.py @@ -0,0 +1,85 @@ +from functools import wraps +from pyramid.request import call_app_with_subpath_as_path_info + +def wsgiapp(wrapped): + """ Decorator to turn a WSGI application into a :app:`Pyramid` + :term:`view callable`. This decorator differs from the + :func:`pyramid.wsgi.wsgiapp2` decorator inasmuch as fixups of + ``PATH_INFO`` and ``SCRIPT_NAME`` within the WSGI environment *are + not* performed before the application is invoked. + + E.g., the following in a ``views.py`` module:: + + @wsgiapp + def hello_world(environ, start_response): + body = 'Hello world' + start_response('200 OK', [ ('Content-Type', 'text/plain'), + ('Content-Length', len(body)) ] ) + return [body] + + Allows the following call to + :meth:`pyramid.config.Configurator.add_view`:: + + from views import hello_world + config.add_view(hello_world, name='hello_world.txt') + + The ``wsgiapp`` decorator will convert the result of the WSGI + application to a :term:`Response` and return it to + :app:`Pyramid` as if the WSGI app were a :app:`Pyramid` + view. + + """ + + if wrapped is None: + raise ValueError('wrapped can not be None') + + def decorator(context, request): + return request.get_response(wrapped) + + # Support case where wrapped is a callable object instance + if getattr(wrapped, '__name__', None): + return wraps(wrapped)(decorator) + return wraps(wrapped, ('__module__', '__doc__'))(decorator) + +def wsgiapp2(wrapped): + """ Decorator to turn a WSGI application into a :app:`Pyramid` + view callable. This decorator differs from the + :func:`pyramid.wsgi.wsgiapp` decorator inasmuch as fixups of + ``PATH_INFO`` and ``SCRIPT_NAME`` within the WSGI environment + *are* performed before the application is invoked. + + E.g. the following in a ``views.py`` module:: + + @wsgiapp2 + def hello_world(environ, start_response): + body = 'Hello world' + start_response('200 OK', [ ('Content-Type', 'text/plain'), + ('Content-Length', len(body)) ] ) + return [body] + + Allows the following call to + :meth:`pyramid.config.Configurator.add_view`:: + + from views import hello_world + config.add_view(hello_world, name='hello_world.txt') + + The ``wsgiapp2`` decorator will convert the result of the WSGI + application to a Response and return it to :app:`Pyramid` as if the WSGI + app were a :app:`Pyramid` view. The ``SCRIPT_NAME`` and ``PATH_INFO`` + values present in the WSGI environment are fixed up before the + application is invoked. In particular, a new WSGI environment is + generated, and the :term:`subpath` of the request passed to ``wsgiapp2`` + is used as the new request's ``PATH_INFO`` and everything preceding the + subpath is used as the ``SCRIPT_NAME``. The new environment is passed to + the downstream WSGI application.""" + + if wrapped is None: + raise ValueError('wrapped can not be None') + + def decorator(context, request): + return call_app_with_subpath_as_path_info(request, wrapped) + + # Support case where wrapped is a callable object instance + if getattr(wrapped, '__name__', None): + return wraps(wrapped)(decorator) + return wraps(wrapped, ('__module__', '__doc__'))(decorator) -- cgit v1.2.3