pax_global_header00006660000000000000000000000064144545304450014522gustar00rootroot0000000000000052 comment=fbd720c7cc7f035bb4436ffea869d7d7d8a09792 pecan-1.5.1/000077500000000000000000000000001445453044500126145ustar00rootroot00000000000000pecan-1.5.1/.coveragerc000066400000000000000000000000231445453044500147300ustar00rootroot00000000000000[run] source=pecan pecan-1.5.1/.github/000077500000000000000000000000001445453044500141545ustar00rootroot00000000000000pecan-1.5.1/.github/workflows/000077500000000000000000000000001445453044500162115ustar00rootroot00000000000000pecan-1.5.1/.github/workflows/run-tests.yml000066400000000000000000000014751445453044500207070ustar00rootroot00000000000000name: run pull request CI on: schedule: - cron: '0 12 * * *' # daily ~8am est workflow_dispatch: pull_request: jobs: unit-tests: runs-on: "ubuntu-20.04" # see: https://github.com/actions/setup-python/issues/544 timeout-minutes: 10 strategy: matrix: python: [3.6, 3.7, 3.8, 3.9, "3.10", "3.11"] command: ["py", "scaffolds", "sqlalchemy-1.4", "sqlalchemy-2"] exclude: # SQLAlchemy2 requires >= py3.7 - python: 3.6 command: "sqlalchemy-2" steps: - uses: actions/checkout@v2 - name: setup Python uses: actions/setup-python@v2 with: python-version: ${{ matrix.python }} - name: install tox run: pip install tox - name: run tox unit tests run: tox -e ${{ matrix.command }} pecan-1.5.1/.gitignore000066400000000000000000000003241445453044500146030ustar00rootroot00000000000000# From GitHub's .gitignore template collection *.py[co] # Packages *.egg *.egg-info dist build eggs develop-eggs # Installer logs pip-log.txt # Unit test / coverage reports .coverage .tox htmlcov .DS_Store pecan-1.5.1/.readthedocs.yaml000066400000000000000000000006471445453044500160520ustar00rootroot00000000000000# .readthedocs.yaml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details # Required version: 2 # Set the version of Python and other tools you might need build: os: ubuntu-22.04 tools: python: "3.11" # Build documentation in the docs/ directory with Sphinx sphinx: configuration: docs/source/conf.py python: install: - method: pip path: . pecan-1.5.1/AUTHORS000066400000000000000000000004061445453044500136640ustar00rootroot00000000000000Pecan is written by various contributors (by date of contribution): Jonathan LaCour Alfredo Deza Mark McClain Ryan Petrello Yoann Roman John Anderson Jeremy Jones Benjamin W. Smith Pete Chudykowski Mike Perez Justin Barber Wesley Spikes Steven Berler Chad Lung pecan-1.5.1/CONTRIBUTING.rst000066400000000000000000000011421445453044500152530ustar00rootroot00000000000000Contributing to Pecan --------------------- Pecan uses GitHub pull requests for bug fixes and feature additions. All contributions must: * Include accompanying tests. * Include narrative and API documentation if new features are added. * Be (generally) compliant with `PEP8 `_. * Not break the test or build. Before submitting a review, ``$ pip install tox && tox`` from your source to ensure that all tests still pass across multiple versions of Python. Bugs should be filed on GitHub: https://github.com/pecan/pecan/issues/new pecan-1.5.1/LICENSE000066400000000000000000000027411445453044500136250ustar00rootroot00000000000000Copyright (c) <2011>, Jonathan LaCour All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. pecan-1.5.1/MANIFEST.in000066400000000000000000000004371445453044500143560ustar00rootroot00000000000000recursive-include pecan/scaffolds/base * include pecan/scaffolds/base/* recursive-include pecan/scaffolds/rest-api * include pecan/scaffolds/rest-api/* include pecan/middleware/resources/* include LICENSE README.rst requirements.txt test-requirements.txt recursive-include pecan/tests * pecan-1.5.1/README.rst000066400000000000000000000023271445453044500143070ustar00rootroot00000000000000Pecan ===== A WSGI object-dispatching web framework, designed to be lean and fast with few dependencies. .. image:: https://badge.fury.io/py/pecan.png :target: https://pypi.python.org/pypi/pecan/ :alt: Latest PyPI version Installing ---------- :: $ pip install pecan ...or, for the latest (unstable) tip:: $ git clone https://github.com/pecan/pecan.git $ cd pecan && python setup.py install Running Tests ------------- :: $ python setup.py test ...or, to run all tests across all supported environments:: $ pip install tox && tox Viewing Documentation --------------------- `Available online `_, or to build manually:: $ cd docs && make html $ open docs/build/html/index.html ...or:: $ cd docs && make man $ man docs/build/man/pecan.1 Contributing ------------ For information on contributing to Pecan, please read our `Contributing Guidelines `_. Bugs should be reported at: https://github.com/pecan/pecan/issues/new Additional Help/Support ----------------------- Most Pecan interaction is done via the `pecan-dev Mailing List `_. pecan-1.5.1/docs/000077500000000000000000000000001445453044500135445ustar00rootroot00000000000000pecan-1.5.1/docs/Makefile000066400000000000000000000107561445453044500152150ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Pecan.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Pecan.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/Pecan" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Pecan" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." make -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." pecan-1.5.1/docs/source/000077500000000000000000000000001445453044500150445ustar00rootroot00000000000000pecan-1.5.1/docs/source/changes.rst000066400000000000000000000352471445453044500172210ustar00rootroot000000000000001.5.1 ===== * addressed an installation bug caused by a duplicate entry script (https://github.com/pecan/pecan/pull/142) 1.5.0 ===== * pecan no longer has a dependency on six (https://github.com/pecan/pecan/issues/144) * pecan now supports SQLAlchemy 2.0 (https://github.com/pecan/pecan/issues/143) * pecan no longer supports SQLAlchemy 1.3 1.4.2 ===== * pecan no longer depends on webtest (https://github.com/pecan/pecan/issues/139) 1.4.1 ===== * add support for Python 3.10 * added trove classifiers for Python 3.6 - 3.9 * fixed a bug related to setuptools as a dependency (https://github.com/pecan/pecan/pull/122) * fixed a bug that broke pecan when used with certain versions of SQLAlchemy (https://github.com/pecan/pecan/pulls) 1.4.0 ===== * pecan now requires webob >= 1.8 * fixed a bug when parsing certain Accept headers (https://github.com/Pylons/webob/issues/403) * removed official support for Python 3.5 1.3.3 ===== * fixed a bug in RestController that incorrectly routed certain @secure requests (https://github.com/pecan/pecan/pull/105) * removed official support for Python 3.3 1.3.2 ===== * pecan now works with webob < and > 1.8 (https://github.com/pecan/pecan/pull/99) 1.3.1 ===== * pinned webob to <1.8 due to breaking changes in Accept header parsing (https://github.com/pecan/pecan/pull/97) (https://github.com/Pylons/webob/pull/338) 1.3.0 ===== * pecan is now officially supported for Python 3.6 * pecan is no longer supported for Python 2.6 1.2.1 ===== * Reverts a stable API change/regression (in the 1.2 release) (https://github.com/pecan/pecan/issues/72). This change will re-released in a future major version upgrade. 1.2 === * Added a better error message when an invalid template renderer is specified in `pecan.expose()` (https://github.com/pecan/pecan/issues/81). * Pecan controllers that return `None` are now treated as an `HTTP 204 No Content` (https://github.com/pecan/pecan/issues/72). * The `method` argument to `pecan.expose()` for generic controllers is no longer optional (https://github.com/pecan/pecan/pull/77). 1.1.2 ===== * Fixed a bug where JSON-formatted HTTP response bodies were not making use of pecan's JSON type registration functionality (http://pecan.readthedocs.io/en/latest/jsonify.html) (https://github.com/pecan/pecan/issues/68). * Updated code and documentation examples to support readthedoc's move from `readthedocs.org` to `readthedocs.io`. 1.1.1 ===== * Pecan now officially supports Python 3.5. * Pecan now uses `inspect.signature` instead of `inspect.getargspec` in Python 3.5 and higher (because `inspect.getargspec` is deprecated in these versions of Python 3). * Fixed a bug that caused "after" hooks to run multiple times when `pecan.redirect(..., internal=True)` was used (https://github.com/pecan/pecan/issues/58). 1.1.0 ===== * `pecan.middleware.debug.DebugMiddleware` now logs exceptions at the ERROR level (https://github.com/pecan/pecan/pull/56). * Fix a Javascript bug in the default project scaffold (https://github.com/pecan/pecan/pull/55). 1.0.5 ===== * Fix a bug in controller argspec detection when class-based decorators are used (https://github.com/pecan/pecan/issues/47). 1.0.4 ===== * Removed an open file handle leak when pecan renders errors for Jinja2 and Genshi templates (https://github.com/pecan/pecan/issues/30). * Resolved a bug which caused log output to be duplicated in projects created with `pecan create` (https://github.com/pecan/pecan/issues/39). 1.0.3 ===== * Fixed a bug in `pecan.hooks.HookController` for newer versions of Python3.4 (https://github.com/pecan/pecan/issues/19). 1.0.2 ===== * Fixed an edge case in `pecan.util.getargspec` that caused the incorrect argspec to be returned in certain situations when using Python 2.6. * Added a `threading.lock` to the file system monitoring in `pecan serve --reload` to avoid extraneous server reloads. 1.0.1 ===== * Fixed a bug wherein the file extension for URLs with a trailing slash (`file.html` vs `file.html/`) were not correctly guessed, thus resulting in incorrect Content-Type headers. * Fixed a subtle bug in `pecan.config.Configuration` attribute/item assignment that caused some types of configuration changes to silently fail. 1.0.0 ===== * Replaced pecan's debugger middleware with an (optional) dependency on the `backlash` package. Developers who want to debug application-level tracebacks interactively should `pip install backlash` in their development environment. * Fixed a Content-Type related bug: when an explicit content_type is specified as an argument to `pecan.expose()`, it is now given precedence over the application-level default renderer. * Fixed a bug that prevented the usage of certain RFC3986-specified characters in path segments. * Fixed a bug in `pecan.abort` which suppressed the original traceback (and prevented monitoring tools like NewRelic from working as effectively). 0.9.0 ===== * Support for Python 3.2 has been dropped. * Added a new feature which allows users to specify custom path segments for controllers. This is especially useful for path segments that are not valid Python identifiers (such as path segments that include certain punctuation characters, like `/some/~path~/`). * Added a new configuration option, `app.debugger`, which allows developers to specify an alternative debugger to `pdb` (e.g., `ipdb`) when performing interactive debugging with pecan's `DebugMiddleware`. * Changed new quickstart pecan projects to default the `pecan` log level to `DEBUG` for development. * Fixed a bug that prevented `staticmethods` from being used as controllers. * Fixed a decoding bug in the way pecan handles certain quoted URL path segments and query strings. * Fixed several bugs in the way pecan handles Unicode path segments (for example, now you can define pecan routes that contain emoji characters). * Fixed several bugs in RestController that caused it to return `HTTP 404 Not Found` rather than `HTTP 405 Method Not Allowed`. Additionally, RestController now returns valid `Allow` headers when `HTTP 405 Method Not Allowed` is returned. * Fixed a bug which allowed special pecan methods (`_route`, `_lookup`, `_default`) to be marked as generic REST methods. * Added more emphasis in pecan's documentation to the need for `debug=False` in production deployments. 0.8.3 ===== * Changed pecan to more gracefully handle a few odd request encoding edge cases. Now pecan applications respond with an HTTP 400 (rather than an uncaught UnicodeDecodeError, resulting in an HTTP 500) when: - HTTP POST requests are composed of non-Unicode data - Request paths contain invalid percent-encoded characters, e.g., ``/some/path/%aa/`` * Improved verbosity for import-related errors in pecan configuration files, especially those involving relative imports. 0.8.2 ===== * Fixes a bug that breaks support for multi-value query string variables (e.g., `?check=a&check=b`). 0.8.1 ===== * Improved detection of infinite recursion for PecanHook and pypy. This fixes a bug discovered in pecan + pypy that could result in infinite recursion when using the PecanHook metaclass. * Fixed a bug that prevented @exposed controllers from using @staticmethod. * Fixed a minor bug in the controller argument calculation. 0.8.0 ===== * For HTTP POSTs, map JSON request bodies to controller keyword arguments. * Improved argspec detection and leniency for wrapped controllers. * When path arguments are incorrect for RestController, return HTTP 404, not 400. * When detecting non-content for HTTP 204, properly catch UnicodeDecodeError. * Fixed a routing bug for generic subcontrollers. * Fixed a bug in generic function handling when context locals are disabled. * Fixed a bug that mixes up argument order for generic functions. * Removed `assert` for flow control; it can be optimized away with `python -O`. 0.7.0 ===== * Fixed an edge case in RestController routing which should have returned an HTTP 400 but was instead raising an exception (and thus, HTTP 500). * Fixed an incorrect root logger configuration for quickstarted pecan projects. * Added `pecan.state.arguments`, a new feature for inspecting controller call arguments. * Fixed an infinite recursion error in PecanHook application. Subclassing both `rest.RestController` and `hooks.HookController` resulted in an infinite recursion error in hook application (which prevented applications from starting). * Pecan's tests are now included in its source distribution. 0.6.1 ===== * Fixed a bug which causes pecan to mistakenly return HTTP 204 for non-empty response bodies. 0.6.0 ===== * Added support for disabling the `pecan.request` and `pecan.response` threadlocals at the WSGI application level in favor of explicit reference passing. For more information, see :ref:`contextlocals`. * Added better support for hook composition via subclassing and mixins. For more information, see :ref:`attaching_hooks`. * Added support for specifying custom request and response implementations at the WSGI application level for people who want to extend the functionality provided by the base classes in `webob`. * Pecan controllers may now return an explicit `webob.Response` instance to short-circuit Pecan's template rendering and serialization. * For generic methods that return HTTP 405, pecan now generates an `Allow` header to communicate acceptable methods to the client. * Fixed a bug in adherence to RFC2616: if an exposed method returns no response body (or namespace), pecan will now enforce an HTTP 204 response (instead of HTTP 200). * Fixed a bug in adherence to RFC2616: when pecan responds with HTTP 204 or HTTP 304, the `Content-Type` header is automatically stripped (because these types of HTTP responses do not contain body content). * Fixed a bug: now when clients request JSON via an `Accept` header, `webob` HTTP exceptions are serialized as JSON, not their native HTML representation. * Fixed a bug that broke applications which specified `default_renderer = json`. 0.5.0 ===== * This release adds formal support for pypy. * Added colored request logging to the `pecan serve` command. * Added a scaffold for easily generating a basic REST API. * Added the ability to pass arbitrary keyword arguments to `pecan.testing.load_test_app`. * Fixed a recursion-related bug in the error document middleware. * Fixed a bug in the `gunicorn_pecan` command that caused `threading.local` data to leak between eventlet/gevent green threads. * Improved documentation through fixes and narrative tutorials for sample pecan applications. 0.4.5 ===== * Fixed a trailing slash bug for `RestController`s that have a `_lookup` method. * Cleaned up the WSGI app reference from the threadlocal state on every request (to avoid potential memory leaks, especially when testing). * Improved pecan documentation and corrected intersphinx references. * pecan supports Python 3.4. 0.4.4 ===== * Removed memoization of certain controller attributes, which can lead to a memory leak in dynamic controller lookups. 0.4.3 ===== * Fixed several bugs for RestController. * Fixed a bug in security handling for generic controllers. * Resolved a bug in `_default` handlers used in `RestController`. * Persist `pecan.request.context` across internal redirects. 0.4.2 ===== * Remove a routing optimization that breaks the WSME pecan plugin. 0.4.1 ===== * Moved the project to `StackForge infrastructure `_, including Gerrit code review, Jenkins continuous integration, and GitHub mirroring. * Added a pecan plugin for the popular `uwsgi server `_. * Replaced the ``simplegeneric`` dependency with the new ``functools.singledispatch`` function in preparation for Python 3.4 support. * Optimized pecan's core dispatch routing for notably faster response times. 0.3.2 ===== * Made some changes to simplify how ``pecan.conf.app`` is passed to new apps. * Fixed a routing bug for certain ``_lookup`` controller configurations. * Improved documentation for handling file uploads. * Deprecated the ``pecan.conf.requestviewer`` configuration option. 0.3.1 ===== * ``on_error`` hooks can now return a Pecan Response objects. * Minor documentation and release tooling updates. 0.3.0 ===== * Pecan now supports Python 2.6, 2.7, 3.2, and 3.3. 0.2.4 ===== * Add support for ``_lookup`` methods as a fallback in RestController. * A variety of improvements to project documentation. 0.2.3 ===== * Add a variety of optimizations to ``pecan.core`` that improve request handling time by approximately 30% for simple object dispatch routing. * Store exceptions raised by ``abort`` in the WSGI environ so they can be accessed later in the request handling (e.g., by other middleware or pecan hooks). * Make TransactionHook more robust so that it isn't as susceptible to failure when exceptions occur in *other* pecan hooks within a request. * Rearrange quickstart verbiage so users don't miss a necessary step. 0.2.2 ===== * Unobfuscate syntax highlighting JavaScript for debian packaging. * Extract the scaffold-building tests into tox. * Add support for specifying a pecan configuration file via the ``PECAN_CONFIG`` environment variable. * Fix a bug in ``DELETE`` methods in two (or more) nested ``RestControllers``. * Add documentation for returning specific HTTP status codes. 0.2.1 ===== * Include a license, readme, and ``requirements.txt`` in distributions. * Improve inspection with ``dir()`` for ``pecan.request`` and ``pecan.response`` * Fix a bug which prevented pecan applications from being mounted at WSGI virtual paths. 0.2.0 ===== * Update base project scaffolding tests to be more repeatable. * Add an application-level configuration option to disable content-type guessing by URL * Fix the wrong test dependency on Jinja, it's Jinja2. * Fix a routing-related bug in ``RestController``. Fixes #156 * Add an explicit ``CONTRIBUTING.rst`` document. * Improve visibility of deployment-related docs. * Add support for a ``gunicorn_pecan`` console script. * Remove and annotate a few unused (and py26 alternative) imports. * Bug fix: don't strip a dotted extension from the path unless it has a matching mimetype. * Add a test to the scaffold project buildout that ensures pep8 passes. * Fix misleading output for ``$ pecan --version``. 0.2.0b ====== * Fix a bug in ``SecureController``. Resolves #131. * Extract debug middleware static file dependencies into physical files. * Improve a test that can fail due to a race condition. * Improve documentation about configation format and ``app.py``. * Add support for content type detection via HTTP Accept headers. * Correct source installation instructions in ``README``. * Fix an incorrect code example in the Hooks documentation. * docs: Fix minor typo in ``*args`` Routing example. pecan-1.5.1/docs/source/commands.rst000066400000000000000000000201341445453044500173770ustar00rootroot00000000000000.. _commands: Command Line Pecan ================== Any Pecan application can be controlled and inspected from the command line using the built-in :command:`pecan` command. The usage examples of :command:`pecan` in this document are intended to be invoked from your project's root directory. Serving a Pecan App For Development ----------------------------------- Pecan comes bundled with a lightweight WSGI development server based on Python's :py:mod:`wsgiref.simple_server` module. Serving your Pecan app is as simple as invoking the ``pecan serve`` command:: $ pecan serve config.py Starting server in PID 000. serving on 0.0.0.0:8080, view at http://127.0.0.1:8080 and then visiting it in your browser. The server ``host`` and ``port`` in your configuration file can be changed as described in :ref:`server_configuration`. .. include:: reload.rst :start-after: #reload The Interactive Shell --------------------- Pecan applications also come with an interactive Python shell which can be used to execute expressions in an environment very similar to the one your application runs in. To invoke an interactive shell, use the ``pecan shell`` command:: $ pecan shell config.py Pecan Interactive Shell Python 2.7.1 (r271:86832, Jul 31 2011, 19:30:53) [GCC 4.2.1 (Based on Apple Inc. build 5658) The following objects are available: wsgiapp - This project's WSGI App instance conf - The current configuration app - webtest.TestApp wrapped around wsgiapp >>> conf Config({ 'app': Config({ 'root': 'myapp.controllers.root.RootController', 'modules': ['myapp'], 'static_root': '/Users/somebody/myapp/public', 'template_path': '/Users/somebody/myapp/project/templates', 'errors': {'404': '/error/404'}, 'debug': True }), 'server': Config({ 'host': '0.0.0.0', 'port': '8080' }) }) >>> app >>> app.get('/') <200 OK text/html body='\n ...\n\n'/936> Press ``Ctrl-D`` to exit the interactive shell (or ``Ctrl-Z`` on Windows). Using an Alternative Shell ++++++++++++++++++++++++++ ``pecan shell`` has optional support for the `IPython `_ and `bpython `_ alternative shells, each of which can be specified with the ``--shell`` flag (or its abbreviated alias, ``-s``), e.g., :: $ pecan shell --shell=ipython config.py $ pecan shell -s bpython config.py .. _env_config: Configuration from an environment variable ------------------------------------------ In all the examples shown, you will see that the :command:`pecan` commands accepted a file path to the configuration file. An alternative to this is to specify the configuration file in an environment variable (:envvar:`PECAN_CONFIG`). This is completely optional; if a file path is passed in explicitly, Pecan will honor that before looking for an environment variable. For example, to serve a Pecan application, a variable could be exported and subsequently be re-used when no path is passed in. :: $ export PECAN_CONFIG=/path/to/app/config.py $ pecan serve Starting server in PID 000. serving on 0.0.0.0:8080, view at http://127.0.0.1:8080 Note that the path needs to reference a valid pecan configuration file, otherwise the command will error out with a message indicating that the path is invalid (for example, if a directory is passed in). If :envvar:`PECAN_CONFIG` is not set and no configuration is passed in, the command will error out because it will not be able to locate a configuration file. Extending ``pecan`` with Custom Commands ---------------------------------------- While the commands packaged with Pecan are useful, the real utility of its command line toolset lies in its extensibility. It's convenient to be able to write a Python script that can work "in a Pecan environment" with access to things like your application's parsed configuration file or a simulated instance of your application itself (like the one provided in the ``pecan shell`` command). Writing a Custom Pecan Command ++++++++++++++++++++++++++++++ As an example, let's create a command that can be used to issue a simulated HTTP GET to your application and print the result. Its invocation from the command line might look something like this:: $ pecan wget config.py /path/to/some/resource Let's say you have a distribution with a package in it named ``myapp``, and that within this package is a ``wget.py`` module:: # myapp/myapp/wget.py import pecan from webtest import TestApp class GetCommand(pecan.commands.BaseCommand): ''' Issues a (simulated) HTTP GET and returns the request body. ''' arguments = pecan.commands.BaseCommand.arguments + ({ 'name': 'path', 'help': 'the URI path of the resource to request' },) def run(self, args): super(GetCommand, self).run(args) app = TestApp(self.load_app()) print app.get(args.path).body Let's analyze this piece-by-piece. Overriding the ``run`` Method ,,,,,,,,,,,,,,,,,,,,,,,,,,,,, First, we're subclassing :class:`~pecan.commands.base.BaseCommand` and extending the :func:`~pecan.commands.base.BaseCommandParent.run` method to: * Load a Pecan application - :func:`~pecan.core.load_app` * Wrap it in a fake WGSI environment - :class:`~webtest.app.TestApp` * Issue an HTTP GET request against it - :meth:`~webtest.app.TestApp.get` Defining Custom Arguments ,,,,,,,,,,,,,,,,,,,,,,,,, The :attr:`arguments` class attribute is used to define command line arguments specific to your custom command. You'll notice in this example that we're *adding* to the arguments list provided by :class:`~pecan.commands.base.BaseCommand` (which already provides an argument for the ``config_file``), rather than overriding it entirely. The format of the :attr:`arguments` class attribute is a :class:`tuple` of dictionaries, with each dictionary representing an argument definition in the same format accepted by Python's :py:mod:`argparse` module (more specifically, :meth:`~argparse.ArgumentParser.add_argument`). By providing a list of arguments in this format, the :command:`pecan` command can include your custom commands in the help and usage output it provides. :: $ pecan -h usage: pecan [-h] command ... positional arguments: command wget Issues a (simulated) HTTP GET and returns the request body serve Open an interactive shell with the Pecan app loaded ... $ pecan wget -h usage: pecan wget [-h] config_file path $ pecan wget config.py /path/to/some/resource Additionally, you'll notice that the first line of the docstring from :class:`GetCommand` -- ``Issues a (simulated) HTTP GET and returns the request body`` -- is automatically used to describe the :command:`wget` command in the output for ``$ pecan -h``. Following this convention allows you to easily integrate a summary for your command into the Pecan command line tool. Registering a Custom Command ++++++++++++++++++++++++++++ Now that you've written your custom command, you’ll need to tell your distribution’s ``setup.py`` about its existence and reinstall. Within your distribution’s ``setup.py`` file, you'll find a call to :func:`~setuptools.setup`. :: # myapp/setup.py ... setup( name='myapp', version='0.1', author='Joe Somebody', ... ) Assuming it doesn't exist already, we'll add the ``entry_points`` argument to the :func:`~setuptools.setup` call, and define a ``[pecan.command]`` definition for your custom command:: # myapp/setup.py ... setup( name='myapp', version='0.1', author='Joe Somebody', ... entry_points=""" [pecan.command] wget = myapp.wget:GetCommand """ ) Once you've done this, reinstall your project in development to register the new entry point. :: $ python setup.py develop Then give it a try. :: $ pecan wget config.py /path/to/some/resource pecan-1.5.1/docs/source/conf.py000066400000000000000000000163601445453044500163510ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # Pecan documentation build configuration file, created by # sphinx-quickstart on Sat Oct 9 14:41:27 2010. # # This file is execfile()d w/ the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import pkg_resources import sys import os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.append(os.path.abspath('.')) # -- General configuration ---------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx'] intersphinx_mapping = { 'python': ('http://docs.python.org', None), 'webob': ('http://docs.webob.org/en/latest', None), 'webtest': ('https://webtest.readthedocs.io/en/latest/', None), 'beaker': ('https://beaker.readthedocs.io/en/latest/', None), 'paste': ('http://pythonpaste.org', None), } # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'Pecan' copyright = u'2010, Jonathan LaCour' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. dist = pkg_resources.get_distribution('pecan') version = release = dist.version # The full version, including alpha/beta/rc tags. #release = '0.3.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- Options for HTML output -------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'nature' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = '' # Output file base name for HTML help builder. htmlhelp_basename = 'Pecandoc' # -- Options for LaTeX output ------------------------------------------------- # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]) latex_documents = [ ('index', 'Pecan.tex', u'Pecan Documentation', u'Jonathan LaCour', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Additional stuff for the LaTeX preamble. #latex_preamble = '' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output ------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'pecan', u'Pecan Documentation', [u'Jonathan LaCour'], 1) ] pecan-1.5.1/docs/source/configuration.rst000066400000000000000000000135531445453044500204540ustar00rootroot00000000000000.. _configuration: Configuring Pecan Applications ============================== Pecan is very easy to configure. As long as you follow certain conventions, using, setting and dealing with configuration should be very intuitive. Pecan configuration files are pure Python. Each "section" of the configuration is a dictionary assigned to a variable name in the configuration module. Default Values --------------- Below is the complete list of default values the framework uses:: server = { 'port' : '8080', 'host' : '0.0.0.0' } app = { 'root' : None, 'modules' : [], 'static_root' : 'public', 'template_path' : '' } .. _application_configuration: Application Configuration ------------------------- The ``app`` configuration values are used by Pecan to wrap your application into a valid `WSGI app `_. The ``app`` configuration is specific to your application, and includes values like the root controller class location. A typical application configuration might look like this:: app = { 'root' : 'project.controllers.root.RootController', 'modules' : ['project'], 'static_root' : '%(confdir)s/public', 'template_path' : '%(confdir)s/project/templates', 'debug' : True } Let's look at each value and what it means: **modules** A list of modules where pecan will search for applications. Generally this should contain a single item, the name of your project's python package. At least one of the listed modules must contain an ``app.setup_app`` function which is called to create the WSGI app. In other words, this package should be where your ``app.py`` file is located, and this file should contain a ``setup_app`` function. **root** The root controller of your application. Remember to provide a string representing a Python path to some callable (e.g., ``"yourapp.controllers.root.RootController"``). **static_root** The directory where your static files can be found (relative to the project root). Pecan comes with middleware that can be used to serve static files (like CSS and Javascript files) during development. **template_path** Points to the directory where your template files live (relative to the project root). **debug** Enables the ability to display tracebacks in the browser and interactively debug during development. .. warning:: ``app`` is a reserved variable name for that section of the configuration, so make sure you don't override it. .. warning:: Make sure **debug** is *always* set to ``False`` in production environments. .. seealso:: * :ref:`app_template` .. _server_configuration: Server Configuration -------------------- Pecan provides some sane defaults. Change these to alter the host and port your WSGI app is served on. :: server = { 'port' : '8080', 'host' : '0.0.0.0' } Additional Configuration ------------------------ Your application may need access to other configuration values at runtime (like third-party API credentials). Put these settings in their own blocks in your configuration file. :: twitter = { 'api_key' : 'FOO', 'api_secret' : 'SECRET' } .. _accessibility: Accessing Configuration at Runtime ---------------------------------- You can access any configuration value at runtime via :py:mod:`pecan.conf`. This includes custom, application, and server-specific values. For example, if you needed to specify a global administrator, you could do so like this within the configuration file. :: administrator = 'foo_bar_user' And it would be accessible in :py:mod:`pecan.conf` as:: >>> from pecan import conf >>> conf.administrator 'foo_bar_user' Dictionary Conversion --------------------- In certain situations you might want to deal with keys and values, but in strict dictionary form. The :class:`~pecan.configuration.Config` object has a helper method for this purpose that will return a dictionary representation of the configuration, including nested values. Below is a representation of how you can access the :meth:`~pecan.configuration.Config.to_dict` method and what it returns as a result (shortened for brevity): :: >>> from pecan import conf >>> conf Config({'app': Config({'errors': {}, 'template_path': '', 'static_root': 'public', [...] >>> conf.to_dict() {'app': {'errors': {}, 'template_path': '', 'static_root': 'public', [...] Prefixing Dictionary Keys ------------------------- :func:`~pecan.configuration.Config.to_dict` allows you to pass an optional string argument if you need to prefix the keys in the returned dictionary. :: >>> from pecan import conf >>> conf Config({'app': Config({'errors': {}, 'template_path': '', 'static_root': 'public', [...] >>> conf.to_dict('prefixed_') {'prefixed_app': {'prefixed_errors': {}, 'prefixed_template_path': '', 'prefixed_static_root': 'prefixed_public', [...] Dotted Keys, Non-Python Idenfitiers, and Native Dictionaries ------------------------------------------------------------ Sometimes you want to specify a configuration option that includes dotted keys or is not a valid Python idenfitier, such as ``()``. These situations are especially common when configuring Python logging. By passing a special key, ``__force_dict__``, individual configuration blocks can be treated as native dictionaries. :: logging = { 'root': {'level': 'INFO', 'handlers': ['console']}, 'loggers': { 'sqlalchemy.engine': {'level': 'INFO', 'handlers': ['console']}, '__force_dict__': True }, 'formatters': { 'custom': { '()': 'my.package.customFormatter' } } } from myapp import conf assert isinstance(conf.logging.loggers, dict) assert isinstance(conf.logging.loggers['sqlalchemy.engine'], dict) pecan-1.5.1/docs/source/contextlocals.rst000066400000000000000000000043331445453044500204630ustar00rootroot00000000000000.. _contextlocals: Context/Thread-Locals vs. Explicit Argument Passing =================================================== In any pecan application, the module-level ``pecan.request`` and ``pecan.response`` are proxy objects that always refer to the request and response being handled in the current thread. This `thread locality` ensures that you can safely access a global reference to the current request and response in a multi-threaded environment without constantly having to pass object references around in your code; it's a feature of pecan that makes writing traditional web applications easier and less verbose. Some people feel thread-locals are too implicit or magical, and that explicit reference passing is much clearer and more maintainable in the long run. Additionally, the default implementation provided by pecan uses :func:`threading.local` to associate these context-local proxy objects with the `thread identifier` of the current server thread. In asynchronous server models - where lots of tasks run for short amounts of time on a `single` shared thread - supporting this mechanism involves monkeypatching :func:`threading.local` to behave in a greenlet-local manner. Disabling Thread-Local Proxies ------------------------------ If you're certain that you `do not` want to utilize context/thread-locals in your project, you can do so by passing the argument ``use_context_locals=False`` in your application's configuration file:: app = { 'root': 'project.controllers.root.RootController', 'modules': ['project'], 'static_root': '%(confdir)s/public', 'template_path': '%(confdir)s/project/templates', 'debug': True, 'use_context_locals': False } Additionally, you'll need to update **all** of your pecan controllers to accept positional arguments for the current request and response:: class RootController(object): @pecan.expose('json') def index(self, req, resp): return dict(method=req.method) # path: / @pecan.expose() def greet(self, req, resp, name): return name # path: /greet/joe It is *imperative* that the request and response arguments come **after** ``self`` and before any positional form arguments. pecan-1.5.1/docs/source/databases.rst000066400000000000000000000176501445453044500175360ustar00rootroot00000000000000.. _databases: Working with Databases, Transactions, and ORM's =============================================== Pecan provides no opinionated support for working with databases, but it's easy to hook into your ORM of choice. This article details best practices for integrating the popular Python ORM, SQLAlchemy_, into your Pecan project. .. _SQLAlchemy: http://sqlalchemy.org .. _init_model: ``init_model`` and Preparing Your Model --------------------------------------- Pecan's default quickstart project includes an empty stub directory for implementing your model as you see fit. :: . └── test_project ├── app.py ├── __init__.py ├── controllers ├── model │   ├── __init__.py └── templates By default, this module contains a special method, :func:`init_model`. :: from pecan import conf def init_model(): """ This is a stub method which is called at application startup time. If you need to bind to a parsed database configuration, set up tables or ORM classes, or perform any database initialization, this is the recommended place to do it. For more information working with databases, and some common recipes, see https://pecan.readthedocs.io/en/latest/databases.html """ pass The purpose of this method is to determine bindings from your configuration file and create necessary engines, pools, etc. according to your ORM or database toolkit of choice. Additionally, your project's :py:mod:`model` module can be used to define functions for common binding operations, such as starting transactions, committing or rolling back work, and clearing a session. This is also the location in your project where object and relation definitions should be defined. Here's what a sample Pecan configuration file with database bindings might look like. :: # Server Specific Configurations server = { ... } # Pecan Application Configurations app = { ... } # Bindings and options to pass to SQLAlchemy's ``create_engine`` sqlalchemy = { 'url' : 'mysql://root:@localhost/dbname?charset=utf8&use_unicode=0', 'echo' : False, 'echo_pool' : False, 'pool_recycle' : 3600, 'encoding' : 'utf-8' } And a basic model implementation that can be used to configure and bind using SQLAlchemy. :: from pecan import conf from sqlalchemy import create_engine, MetaData from sqlalchemy.orm import scoped_session, sessionmaker Session = scoped_session(sessionmaker()) metadata = MetaData() def _engine_from_config(configuration): configuration = dict(configuration) url = configuration.pop('url') return create_engine(url, **configuration) def init_model(): conf.sqlalchemy.engine = _engine_from_config(conf.sqlalchemy) def start(): Session.bind = conf.sqlalchemy.engine metadata.bind = Session.bind def commit(): Session.commit() def rollback(): Session.rollback() def clear(): Session.remove() Binding Within the Application ------------------------------ There are several approaches to wrapping your application's requests with calls to appropriate model function calls. One approach is WSGI middleware. We also recommend Pecan :ref:`hooks`. Pecan comes with :class:`~pecan.hooks.TransactionHook`, a hook which can be used to wrap requests in database transactions for you. To use it, simply include it in your project's ``app.py`` file and pass it a set of functions related to database binding. :: from pecan import conf, make_app from pecan.hooks import TransactionHook from test_project import model app = make_app( conf.app.root, static_root = conf.app.static_root, template_path = conf.app.template_path, debug = conf.app.debug, hooks = [ TransactionHook( model.start, model.start_read_only, model.commit, model.rollback, model.clear ) ] ) In the above example, on HTTP ``POST``, ``PUT``, and ``DELETE`` requests, :class:`~pecan.hooks.TransactionHook` takes care of the transaction automatically by following these rules: #. Before controller routing has been determined, :func:`model.start` is called. This function should bind to the appropriate SQLAlchemy engine and start a transaction. #. Controller code is run and returns. #. If your controller or template rendering fails and raises an exception, :func:`model.rollback` is called and the original exception is re-raised. This allows you to rollback your database transaction to avoid committing work when exceptions occur in your application code. #. If the controller returns successfully, :func:`model.commit` and :func:`model.clear` are called. On idempotent operations (like HTTP ``GET`` and ``HEAD`` requests), :class:`~pecan.hooks.TransactionHook` handles transactions following different rules. #. ``model.start_read_only()`` is called. This function should bind to your SQLAlchemy engine. #. Controller code is run and returns. #. If the controller returns successfully, ``model.clear()`` is called. Also note that there is a useful :func:`~pecan.decorators.after_commit` decorator provided in :ref:`pecan_decorators`. Splitting Reads and Writes -------------------------- Employing the strategy above with :class:`~pecan.hooks.TransactionHook` makes it very simple to split database reads and writes based upon HTTP methods (i.e., GET/HEAD requests are read-only and would potentially be routed to a read-only database slave, while POST/PUT/DELETE requests require writing, and would always bind to a master database with read/write privileges). It's also possible to extend :class:`~pecan.hooks.TransactionHook` or write your own hook implementation for more refined control over where and when database bindings are called. Assuming a master/standby setup, where the master accepts write requests and the standby can only get read requests, a Pecan configuration for sqlalchemy could be:: # Server Specific Configurations server = { ... } # Pecan Application Configurations app = { ... } # Master database sqlalchemy_w = { 'url': 'postgresql+psycopg2://root:@master_host/dbname', 'pool_recycle': 3600, 'encoding': 'utf-8' } # Read Only database sqlalchemy_ro = { 'url': 'postgresql+psycopg2://root:@standby_host/dbname', 'pool_recycle': 3600, 'encoding': 'utf-8' } Given the unique configuration settings for each database, the bindings would need to change from what Pecan's default quickstart provides (see :ref:`init_model` section) to accommodate for both write and read only requests:: from pecan import conf from sqlalchemy import create_engine, MetaData from sqlalchemy.orm import scoped_session, sessionmaker Session = scoped_session(sessionmaker()) metadata = MetaData() def init_model(): conf.sqlalchemy_w.engine = _engine_from_config(conf.sqlalchemy_w) conf.sqlalchemy_ro.engine = _engine_from_config(conf.sqlalchemy_ro) def _engine_from_config(configuration): configuration = dict(configuration) url = configuration.pop('url') return create_engine(url, **configuration) def start(): Session.bind = conf.sqlalchemy_w.engine metadata.bind = conf.sqlalchemy_w.engine def start_read_only(): Session.bind = conf.sqlalchemy_ro.engine metadata.bind = conf.sqlalchemy_ro.engine def commit(): Session.commit() def rollback(): Session.rollback() def clear(): Session.close() def flush(): Session.flush() pecan-1.5.1/docs/source/deployment.rst000066400000000000000000000231301445453044500177550ustar00rootroot00000000000000.. _deployment: Deploying Pecan in Production ============================= There are a variety of ways to deploy a Pecan project to a production environment. The following examples are meant to provide *direction*, not explicit instruction; deployment is usually heavily dependent upon the needs and goals of individual applications, so your mileage will probably vary. .. note:: While Pecan comes packaged with a simple server *for development use* (:command:`pecan serve`), using a *production-ready* server similar to the ones described in this document is **very highly encouraged**. Installing Pecan ---------------- A few popular options are available for installing Pecan in production environments: * Using `setuptools `_. Manage Pecan as a dependency in your project's ``setup.py`` file so that it's installed alongside your project (e.g., ``python /path/to/project/setup.py install``). The default Pecan project described in :ref:`quick_start` facilitates this by including Pecan as a dependency for your project. * Using `pip `_. Use ``pip freeze`` and ``pip install`` to create and install from a ``requirements.txt`` file for your project. * Via the manual instructions found in :ref:`Installation`. .. note:: Regardless of the route you choose, it's highly recommended that all deployment installations be done in a Python `virtual environment `_. Disabling Debug Mode -------------------- .. warning:: One of the most important steps to take before deploying a Pecan app into production is to ensure that you have disabled **Debug Mode**, which provides a development-oriented debugging environment for tracebacks encountered at runtime. Failure to disable this development tool in your production environment *will* result in serious security issues. In your production configuration file, ensure that ``debug`` is set to ``False``. :: # myapp/production_config.py app = { ... 'debug': False } Pecan and WSGI -------------- WSGI is a Python standard that describes a standard interface between servers and an application. Any Pecan application is also known as a "WSGI application" because it implements the WSGI interface, so any server that is "WSGI compatible" may be used to serve your application. A few popular examples are: * `mod_wsgi `__ * `uWSGI `__ * `Gunicorn `__ * `waitress `__ * `CherryPy `__ Generally speaking, the WSGI entry point to any Pecan application can be generated using :func:`~pecan.deploy.deploy`:: from pecan.deploy import deploy application = deploy('/path/to/some/app/config.py') Considerations for Static Files ------------------------------- Pecan comes with static file serving (e.g., CSS, Javascript, images) middleware which is **not** recommended for use in production. In production, Pecan doesn't serve media files itself; it leaves that job to whichever web server you choose. For serving static files in production, it's best to separate your concerns by serving static files separately from your WSGI application (primarily for performance reasons). There are several popular ways to accomplish this. Here are two: 1. Set up a proxy server (such as `nginx `__, `cherokee `__, :ref:`cherrypy`, or `lighttpd `__) to serve static files and proxy application requests through to your WSGI application: :: ─── , e.g., Apache, nginx, cherokee (0.0.0.0:80) ─── │ ├── Instance e.g., mod_wsgi, Gunicorn, uWSGI (127.0.0.1:5000 or /tmp/some.sock) ├── Instance e.g., mod_wsgi, Gunicorn, uWSGI (127.0.0.1:5001 or /tmp/some.sock) ├── Instance e.g., mod_wsgi, Gunicorn, uWSGI (127.0.0.1:5002 or /tmp/some.sock) └── Instance e.g., mod_wsgi, Gunicorn, uWSGI (127.0.0.1:5003 or /tmp/some.sock) 2. Serve static files via a separate service, virtual host, or CDN. Common Recipes -------------- Apache + mod_wsgi +++++++++++++++++ `mod_wsgi `_ is a popular Apache module which can be used to host any WSGI-compatible Python application (including your Pecan application). To get started, check out the `installation and configuration documentation `_ for mod_wsgi. For the sake of example, let's say that our project, ``simpleapp``, lives at ``/var/www/simpleapp``, and that a `virtualenv `_ has been created at ``/var/www/venv`` with any necessary dependencies installed (including Pecan). Additionally, for security purposes, we've created a user, ``user1``, and a group, ``group1`` to execute our application under. The first step is to create a ``.wsgi`` file which mod_wsgi will use as an entry point for your application:: # /var/www/simpleapp/app.wsgi from pecan.deploy import deploy application = deploy('/var/www/simpleapp/config.py') Next, add Apache configuration for your application. Here's a simple example:: ServerName example.com WSGIDaemonProcess simpleapp user=user1 group=group1 threads=5 python-path=/var/www/venv/lib/python2.7/site-packages WSGIScriptAlias / /var/www/simpleapp/app.wsgi WSGIProcessGroup simpleapp WSGIApplicationGroup %{GLOBAL} Order deny,allow Allow from all For more instructions and examples of mounting WSGI applications using mod_wsgi, consult the `mod_wsgi Documentation`_. .. _mod_wsgi Documentation: http://code.google.com/p/modwsgi/wiki/QuickConfigurationGuide#Mounting_The_WSGI_Application Finally, restart Apache and give it a try. uWSGI +++++ `uWSGI `_ is a fast, self-healing and developer/sysadmin-friendly application container server coded in pure C. It uses the `uwsgi `__ protocol, but can speak other protocols as well (http, fastcgi...). Running Pecan applications with uWSGI is a snap:: $ pip install uwsgi $ pecan create simpleapp && cd simpleapp $ python setup.py develop $ uwsgi --http-socket :8080 --venv /path/to/virtualenv --pecan config.py or using a Unix socket (that nginx, for example, could be configured to `proxy to `_):: $ uwsgi -s /tmp/uwsgi.sock --venv /path/to/virtualenv --pecan config.py Gunicorn ++++++++ `Gunicorn `__, or "Green Unicorn", is a WSGI HTTP Server for UNIX. It’s a pre-fork worker model ported from Ruby’s Unicorn project. It supports both eventlet and greenlet. Running a Pecan application on Gunicorn is simple. Let's walk through it with Pecan's default project:: $ pip install gunicorn $ pecan create simpleapp && cd simpleapp $ python setup.py develop $ gunicorn_pecan config.py .. _cherrypy: CherryPy ++++++++ `CherryPy `__ offers a pure Python HTTP/1.1-compliant WSGI thread-pooled web server. It can support Pecan applications easily and even serve static files like a production server would do. The examples that follow are geared towards using CherryPy as the server in charge of handling a Pecan app along with serving static files. :: $ pip install cherrypy $ pecan create simpleapp && cd simpleapp $ python setup.py develop To run with CherryPy, the easiest approach is to create a script in the root of the project (alongside ``setup.py``), so that we can describe how our example application should be served. This is how the script (named ``run.py``) looks:: import os import cherrypy from cheroot import wsgi from cheroot.wsgi import PathInfoDispatcher from pecan.deploy import deploy simpleapp_wsgi_app = deploy('/path/to/production_config.py') public_path = os.path.abspath(os.path.join(os.path.dirname(__file__), 'public')) # A dummy class for our Root object # necessary for some CherryPy machinery class Root(object): pass def make_static_config(static_dir_name): """ All custom static configurations are set here, since most are common, it makes sense to generate them just once. """ static_path = os.path.join('/', static_dir_name) path = os.path.join(public_path, static_dir_name) configuration = { static_path: { 'tools.staticdir.on': True, 'tools.staticdir.dir': path } } return cherrypy.tree.mount(Root(), '/', config=configuration) # Assuming your app has media on different paths, like 'css', and 'images' application = PathInfoDispatcher({ '/': simpleapp_wsgi_app, '/css': make_static_config('css'), '/images': make_static_config('images') }) server = wsgi.Server(('0.0.0.0', 8080), application, server_name='simpleapp') try: server.start() except KeyboardInterrupt: print("Terminating server...") server.stop() To start the server, simply call it with the Python executable:: $ python run.py pecan-1.5.1/docs/source/development.rst000066400000000000000000000034061445453044500201230ustar00rootroot00000000000000.. _development: Developing Pecan Applications Locally ===================================== .. include:: reload.rst :start-after: #reload Debugging Pecan Applications ---------------------------- Pecan comes with simple debugging middleware for helping diagnose problems in your applications. To enable the debugging middleware, simply set the ``debug`` flag to ``True`` in your configuration file:: app = { ... 'debug': True, ... } Once enabled, the middleware will automatically catch exceptions raised by your application and display the Python stack trace and WSGI environment in your browser when runtime exceptions are raised. To improve debugging, including support for an interactive browser-based console, Pecan makes use of the Python `backlash ` library. You’ll need to install it for development use before continuing:: $ pip install backlash Downloading/unpacking backlash ... Successfully installed backlash Serving Static Files -------------------- Pecan comes with simple file serving middleware for serving CSS, Javascript, images, and other static files. You can configure it by ensuring that the following options are specified in your configuration file: :: app = { ... 'debug': True, 'static_root': '%(confdir)/public } where ``static_root`` is an absolute pathname to the directory in which your static files live. For convenience, the path may include the ``%(confdir)`` variable, which Pecan will substitute with the absolute path of your configuration file at runtime. .. note:: In production, ``app.debug`` should *never* be set to ``True``, so you'll need to serve your static files via your production web server. pecan-1.5.1/docs/source/errors.rst000066400000000000000000000072671445453044500171260ustar00rootroot00000000000000.. _errors: Custom Error Documents ====================== In this article we will configure a Pecan application to display a custom error page whenever the server returns a ``404 Page Not Found`` status. This article assumes that you have already created a test application as described in :ref:`quick_start`. .. note:: While this example focuses on the ``HTTP 404`` message, the same technique may be applied to define custom actions for any of the ``HTTP`` status response codes in the 400 and 500 range. You are well advised to use this power judiciously. .. _overview: Overview -------- Pecan makes it simple to customize error documents in two simple steps: * :ref:`configure` of the HTTP status messages you want to handle in your application's ``config.py`` * :ref:`controllers` to handle the status messages you have configured .. _configure: Configure Routing ----------------- Let's configure our application ``test_project`` to route ``HTTP 404 Page Not Found`` messages to a custom controller. First, let's update ``test_project/config.py`` to specify a new error-handler. :: # Pecan Application Configurations app = { 'root' : 'test_project.controllers.root.RootController', 'modules' : ['test_project'], 'static_root' : '%(confdir)s/public', 'template_path' : '%(confdir)s/test_project/templates', 'reload' : True, 'debug' : True, # modify the 'errors' key to direct HTTP status codes to a custom # controller 'errors' : { #404 : '/error/404', 404 : '/notfound', '__force_dict__' : True } } Instead of the default error page, Pecan will now route 404 messages to the controller method ``notfound``. .. _controllers: Write Custom Controllers ------------------------ The easiest way to implement the error handler is to add it to :class:`test_project.root.RootController` class (typically in ``test_project/controllers/root.py``). :: from pecan import expose from webob.exc import status_map class RootController(object): @expose(generic=True, template='index.html') def index(self): return dict() @index.when(method='POST') def index_post(self, q): redirect('https://pecan.readthedocs.io/en/latest/search.html?q=%s' % q) ## custom handling of '404 Page Not Found' messages @expose('error.html') def notfound(self): return dict(status=404, message="test_project does not have this page") @expose('error.html') def error(self, status): try: status = int(status) except ValueError: status = 0 message = getattr(status_map.get(status), 'explanation', '') return dict(status=status, message=message) And that's it! Notice that the only bit of code we added to our :class:`RootController` was:: ## custom handling of '404 Page Not Found' messages @expose('error.html') def notfound(self): return dict(status=404, message="test_project does not have this page") We simply :func:`~pecan.decorators.expose` the ``notfound`` controller with the ``error.html`` template (which was conveniently generated for us and placed under ``test_project/templates/`` when we created ``test_project``). As with any Pecan controller, we return a dictionary of variables for interpolation by the template renderer. Now we can modify the error template, or write a brand new one to make the 404 error status page of ``test_project`` as pretty or fancy as we want. pecan-1.5.1/docs/source/forms.rst000066400000000000000000000050511445453044500167250ustar00rootroot00000000000000.. _forms: Generating and Validating Forms =============================== Pecan provides no opinionated support for working with form generation and validation libraries, but it’s easy to import your library of choice with minimal effort. This article details best practices for integrating the popular forms library, `WTForms `_, into your Pecan project. Defining a Form Definition -------------------------- Let's start by building a basic form with a required ``first_name`` field and an optional ``last_name`` field. :: from wtforms import Form, TextField, validators class MyForm(Form): first_name = TextField(u'First Name', validators=[validators.required()]) last_name = TextField(u'Last Name', validators=[validators.optional()]) class SomeController(object): pass Rendering a Form in a Template ------------------------------ Next, let's add a controller, and pass a form instance to the template. :: from pecan import expose from wtforms import Form, TextField, validators class MyForm(Form): first_name = TextField(u'First Name', validators=[validators.required()]) last_name = TextField(u'Last Name', validators=[validators.optional()]) class SomeController(object): @expose(template='index.html') def index(self): return dict(form=MyForm()) Here's the Mako_ template file: .. _Mako: http://www.makeotemplates.org/ .. code-block:: html
${form.first_name.label}: ${form.first_name}
${form.last_name.label}: ${form.last_name}
Validating POST Values ---------------------- Using the same :class:`MyForm` definition, let's redirect the user if the form is validated, otherwise, render the form again. .. code-block:: python from pecan import expose, request from wtforms import Form, TextField, validators class MyForm(Form): first_name = TextField(u'First Name', validators=[validators.required()]) last_name = TextField(u'Last Name', validators=[validators.optional()]) class SomeController(object): @expose(template='index.html') def index(self): my_form = MyForm(request.POST) if request.method == 'POST' and my_form.validate(): # save_values() redirect('/success') else: return dict(form=my_form) pecan-1.5.1/docs/source/hooks.rst000066400000000000000000000325141445453044500167260ustar00rootroot00000000000000.. _hooks: Pecan Hooks =========== Although it is easy to use WSGI middleware with Pecan, it can be hard (sometimes impossible) to have access to Pecan's internals from within middleware. Pecan Hooks are a way to interact with the framework, without having to write separate middleware. Hooks allow you to execute code at key points throughout the life cycle of your request: * :func:`~pecan.hooks.PecanHook.on_route`: called before Pecan attempts to route a request to a controller * :func:`~pecan.hooks.PecanHook.before`: called after routing, but before controller code is run * :func:`~pecan.hooks.PecanHook.after`: called after controller code has been run * :func:`~pecan.hooks.PecanHook.on_error`: called when a request generates an exception Implementating a Pecan Hook --------------------------- In the below example, a simple hook will gather some information about the request and print it to ``stdout``. Your hook implementation needs to import :class:`~pecan.hooks.PecanHook` so it can be used as a base class. From there, you'll want to override the :func:`~pecan.hooks.PecanHook.on_route`, :func:`~pecan.hooks.PecanHook.before`, :func:`~pecan.hooks.PecanHook.after`, or :func:`~pecan.hooks.PecanHook.on_error` methods to define behavior. :: from pecan.hooks import PecanHook class SimpleHook(PecanHook): def before(self, state): print "\nabout to enter the controller..." def after(self, state): print "\nmethod: \t %s" % state.request.method print "\nresponse: \t %s" % state.response.status :func:`~pecan.hooks.PecanHook.on_route`, :func:`~pecan.hooks.PecanHook.before`, and :func:`~pecan.hooks.PecanHook.after` are each passed a shared state object which includes useful information, such as the request and response objects, and which controller was selected by Pecan's routing:: class SimpleHook(PecanHook): def on_route(self, state): print "\nabout to map the URL to a Python method (controller)..." assert state.controller is None # Routing hasn't occurred yet assert isinstance(state.request, webob.Request) assert isinstance(state.response, webob.Response) assert isinstance(state.hooks, list) # A list of hooks to apply def before(self, state): print "\nabout to enter the controller..." if state.request.path == '/': # # `state.controller` is a reference to the actual # `@pecan.expose()`-ed controller that will be routed to # and used to generate the response body # assert state.controller.__func__ is RootController.index.__func__ assert isinstance(state.arguments, inspect.Arguments) print state.arguments.args print state.arguments.varargs print state.arguments.keywords assert isinstance(state.request, webob.Request) assert isinstance(state.response, webob.Response) assert isinstance(state.hooks, list) :func:`~pecan.hooks.PecanHook.on_error` is passed a shared state object **and** the original exception. If an :func:`~pecan.hooks.PecanHook.on_error` handler returns a Response object, this response will be returned to the end user and no furthur :func:`~pecan.hooks.PecanHook.on_error` hooks will be executed:: class CustomErrorHook(PecanHook): def on_error(self, state, exc): if isinstance(exc, SomeExceptionType): return webob.Response('Custom Error!', status=500) .. _attaching_hooks: Attaching Hooks --------------- Hooks can be attached in a project-wide manner by specifying a list of hooks in your project's configuration file. :: app = { 'root' : '...' # ... 'hooks': lambda: [SimpleHook()] } Hooks can also be applied selectively to controllers and their sub-controllers using the :attr:`__hooks__` attribute on one or more controllers and subclassing :class:`~pecan.hooks.HookController`. :: from pecan import expose from pecan.hooks import HookController from my_hooks import SimpleHook class SimpleController(HookController): __hooks__ = [SimpleHook()] @expose('json') def index(self): print "DO SOMETHING!" return dict() Now that :class:`SimpleHook` is included, let's see what happens when we run the app and browse the application from our web browser. :: pecan serve config.py serving on 0.0.0.0:8080 view at http://127.0.0.1:8080 about to enter the controller... DO SOMETHING! method: GET response: 200 OK Hooks can be inherited from parent class or mixins. Just make sure to subclass from :class:`~pecan.hooks.HookController`. :: from pecan import expose from pecan.hooks import PecanHook, HookController class ParentHook(PecanHook): priority = 1 def before(self, state): print "\nabout to enter the parent controller..." class CommonHook(PecanHook): priority = 2 def before(self, state): print "\njust a common hook..." class SubHook(PecanHook): def before(self, state): print "\nabout to enter the subcontroller..." class SubMixin(object): __hooks__ = [SubHook()] # We'll use the same instance for both controllers, # to avoid double calls common = CommonHook() class SubController(HookController, SubMixin): __hooks__ = [common] @expose('json') def index(self): print "\nI AM THE SUB!" return dict() class RootController(HookController): __hooks__ = [common, ParentHook()] @expose('json') def index(self): print "\nI AM THE ROOT!" return dict() sub = SubController() Let's see what happens when we run the app. First loading the root controller: :: pecan serve config.py serving on 0.0.0.0:8080 view at http://127.0.0.1:8080 GET / HTTP/1.1" 200 about to enter the parent controller... just a common hook I AM THE ROOT! Then loading the sub controller: :: pecan serve config.py serving on 0.0.0.0:8080 view at http://127.0.0.1:8080 GET /sub HTTP/1.1" 200 about to enter the parent controller... just a common hook about to enter the subcontroller... I AM THE SUB! .. note:: Make sure to set proper priority values for nested hooks in order to get them executed in the desired order. .. warning:: Two hooks of the same type will be added/executed twice, if passed as different instances to a parent and a child controller. If passed as one instance variable - will be invoked once for both controllers. Hooks That Come with Pecan -------------------------- Pecan includes some hooks in its core. This section will describe their different uses, how to configure them, and examples of common scenarios. .. _requestviewerhook: RequestViewerHook ''''''''''''''''' This hook is useful for debugging purposes. It has access to every attribute the ``response`` object has plus a few others that are specific to the framework. There are two main ways that this hook can provide information about a request: #. Terminal or logging output (via an file-like stream like ``stdout``) #. Custom header keys in the actual response. By default, both outputs are enabled. .. seealso:: * :ref:`pecan_hooks` Configuring RequestViewerHook ............................. There are a few ways to get this hook properly configured and running. However, it is useful to know that no actual configuration is needed to have it up and running. By default it will output information about these items: * path : Displays the url that was used to generate this response * status : The response from the server (e.g. '200 OK') * method : The method for the request (e.g. 'GET', 'POST', 'PUT or 'DELETE') * controller : The actual controller method in Pecan responsible for the response * params : A list of tuples for the params passed in at request time * hooks : Any hooks that are used in the app will be listed here. The default configuration will show those values in the terminal via ``stdout`` and it will also add them to the response headers (in the form of ``X-Pecan-item_name``). This is how the terminal output might look for a `/favicon.ico` request:: path - /favicon.ico status - 404 Not Found method - GET controller - The resource could not be found. params - [] hooks - ['RequestViewerHook'] In the above case, the file was not found, and the information was printed to `stdout`. Additionally, the following headers would be present in the HTTP response:: X-Pecan-path /favicon.ico X-Pecan-status 404 Not Found X-Pecan-method GET X-Pecan-controller The resource could not be found. X-Pecan-params [] X-Pecan-hooks ['RequestViewerHook'] The configuration dictionary is flexible (none of the keys are required) and can hold two keys: ``items`` and ``blacklist``. This is how the hook would look if configured directly (shortened for brevity):: ... 'hooks': lambda: [ RequestViewerHook({'items':['path']}) ] Modifying Output Format ....................... The ``items`` list specify the information that the hook will return. Sometimes you will need a specific piece of information or a certain bunch of them according to the development need so the defaults will need to be changed and a list of items specified. .. note:: When specifying a list of items, this list overrides completely the defaults, so if a single item is listed, only that item will be returned by the hook. The hook has access to every single attribute the request object has and not only to the default ones that are displayed, so you can fine tune the information displayed. These is a list containing all the possible attributes the hook has access to (directly from `webob`): ====================== ========================== ====================== ========================== accept make_tempfile accept_charset max_forwards accept_encoding method accept_language params application_url path as_string path_info authorization path_info_peek blank path_info_pop body path_qs body_file path_url body_file_raw postvars body_file_seekable pragma cache_control query_string call_application queryvars charset range content_length referer content_type referrer cookies relative_url copy remote_addr copy_body remote_user copy_get remove_conditional_headers date request_body_tempfile_limit decode_param_names scheme environ script_name from_file server_name from_string server_port get_response str_GET headers str_POST host str_cookies host_url str_params http_version str_postvars if_match str_queryvars if_modified_since unicode_errors if_none_match upath_info if_range url if_unmodified_since urlargs is_body_readable urlvars is_body_seekable uscript_name is_xhr user_agent make_body_seekable ====================== ========================== And these are the specific ones from Pecan and the hook: * controller * hooks * params (params is actually available from `webob` but it is parsed by the hook for redability) Blacklisting Certain Paths .......................... Sometimes it's annoying to get information about *every* single request. To limit the output, pass the list of URL paths for which you do not want data as the ``blacklist``. The matching is done at the start of the URL path, so be careful when using this feature. For example, if you pass a configuration like this one:: { 'blacklist': ['/f'] } It would not show *any* url that starts with ``f``, effectively behaving like a globbing regular expression (but not quite as powerful). For any number of blocking you may need, just add as many items as wanted:: { 'blacklist' : ['/favicon.ico', '/javascript', '/images'] } Again, the ``blacklist`` key can be used along with the ``items`` key or not (it is not required). pecan-1.5.1/docs/source/index.rst000066400000000000000000000074171445453044500167160ustar00rootroot00000000000000Introduction and History ======================== Welcome to Pecan, a lean Python web framework inspired by CherryPy, TurboGears, and Pylons. Pecan was originally created by the developers of `ShootQ `_ while working at `Pictage `_. Pecan was created to fill a void in the Python web-framework world – a very lightweight framework that provides object-dispatch style routing. Pecan does not aim to be a "full stack" framework, and therefore includes no out of the box support for things like sessions or databases (although tutorials are included for integrating these yourself in just a few lines of code). Pecan instead focuses on HTTP itself. Although it is lightweight, Pecan does offer an extensive feature set for building HTTP-based applications, including: * Object-dispatch for easy routing * Full support for REST-style controllers * Extensible security framework * Extensible template language support * Extensible JSON support * Easy Python-based configuration Narrative Documentation ======================= .. toctree:: :maxdepth: 2 installation.rst quick_start.rst routing.rst templates.rst rest.rst configuration.rst secure_controller.rst hooks.rst jsonify.rst contextlocals.rst commands.rst development.rst deployment.rst logging.rst testing.rst Cookbook and Common Patterns ============================ .. toctree:: :maxdepth: 2 forms.rst sessions.rst databases.rst errors.rst simple_forms_processing.rst simple_ajax.rst API Documentation ================= Pecan's source code is well documented using Python docstrings and comments. In addition, we have generated API documentation from the docstrings here: .. toctree:: :maxdepth: 2 pecan_core.rst pecan_commands.rst pecan_configuration.rst pecan_decorators.rst pecan_deploy.rst pecan_hooks.rst pecan_middleware_debug.rst pecan_jsonify.rst pecan_rest.rst pecan_routing.rst pecan_secure.rst pecan_templating.rst pecan_testing.rst pecan_util.rst Change History ================= .. toctree:: :maxdepth: 1 changes.rst License ------- The Pecan framework and the documentation is BSD Licensed:: Copyright (c) <2010>, Pecan Framework All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. pecan-1.5.1/docs/source/installation.rst000066400000000000000000000026561445453044500203100ustar00rootroot00000000000000.. _installation: Installation ============ Stable Version -------------- We recommend installing Pecan with `pip `_, but you can also try with :command:`easy_install`. Creating a spot in your environment where Pecan can be isolated from other packages is best practice. To get started with an environment for Pecan, we recommend creating a new `virtual environment `_ using `virtualenv `_:: $ virtualenv pecan-env $ cd pecan-env $ source bin/activate The above commands create a virtual environment and *activate* it. This will isolate Pecan's dependency installations from your system packages, making it easier to debug problems if needed. Next, let's install Pecan:: $ pip install pecan Development (Unstable) Version ------------------------------ If you want to run the latest development version of Pecan you will need to install git and clone the repo from GitHub:: $ git clone https://github.com/pecan/pecan.git Assuming your virtual environment is still activated, call ``setup.py`` to install the development version.:: $ cd pecan $ python setup.py develop .. note:: The ``master`` development branch is volatile and is generally not recommended for production use. Alternatively, you can also install from GitHub directly with ``pip``.:: $ pip install -e git://github.com/pecan/pecan.git#egg=pecan pecan-1.5.1/docs/source/jsonify.rst000066400000000000000000000045631445453044500172670ustar00rootroot00000000000000.. _jsonify: JSON Serialization ================== Pecan includes a simple, easy-to-use system for generating and serving JSON. To get started, create a file in your project called ``json.py`` and import it in your project's ``app.py``. Your ``json`` module will contain a series of rules for generating JSON from objects you return in your controller. Let's say that we have a controller in our Pecan application which we want to use to return JSON output for a :class:`User` object:: from myproject.lib import get_current_user class UsersController(object): @expose('json') def current_user(self): ''' return an instance of myproject.model.User which represents the current authenticated user ''' return get_current_user() In order for this controller to function, Pecan will need to know how to convert the :class:`User` object into data types compatible with JSON. One way to tell Pecan how to convert an object into JSON is to define a rule in your ``json.py``:: from pecan.jsonify import jsonify from myproject import model @jsonify.register(model.User) def jsonify_user(user): return dict( name = user.name, email = user.email, birthday = user.birthday.isoformat() ) In this example, when an instance of the :class:`model.User` class is returned from a controller which is configured to return JSON, the :func:`jsonify_user` rule will be called to convert the object to JSON-compatible data. Note that the rule does not generate a JSON string, but rather generates a Python dictionary which contains only JSON friendly data types. Alternatively, the rule can be specified on the object itself, by specifying a :func:`__json__` method in the class:: class User(object): def __init__(self, name, email, birthday): self.name = name self.email = email self.birthday = birthday def __json__(self): return dict( name = self.name, email = self.email, birthday = self.birthday.isoformat() ) The benefit of using a ``json.py`` module is having all of your JSON rules defined in a central location, but some projects prefer the simplicity of keeping the JSON rules attached directly to their model objects. pecan-1.5.1/docs/source/logging.rst000066400000000000000000000110151445453044500172220ustar00rootroot00000000000000.. _logging: Logging ======= Pecan uses the Python standard library's :py:mod:`logging` module by passing logging configuration options into the `logging.config.dictConfig`_ function. The full documentation for the :func:`dictConfig` format is the best source of information for logging configuration, but to get you started, this chapter will provide you with a few simple examples. .. _logging.config.dictConfig: http://docs.python.org/library/logging.config.html#configuration-dictionary-schema Configuring Logging ------------------- Sample logging configuration is provided with the quickstart project introduced in :ref:`quick_start`: :: $ pecan create myapp The default configuration defines one handler and two loggers. :: # myapp/config.py app = { ... } server = { ... } logging = { 'root' : {'level': 'INFO', 'handlers': ['console']}, 'loggers': { 'myapp': {'level': 'DEBUG', 'handlers': ['console']} }, 'handlers': { 'console': { 'level': 'DEBUG', 'class': 'logging.StreamHandler', 'formatter': 'simple' } }, 'formatters': { 'simple': { 'format': ('%(asctime)s %(levelname)-5.5s [%(name)s]' '[%(threadName)s] %(message)s') } } } * ``console`` logs messages to ``stderr`` using the ``simple`` formatter. * ``myapp`` logs messages sent at a level above or equal to ``DEBUG`` to the ``console`` handler * ``root`` logs messages at a level above or equal to the ``INFO`` level to the ``console`` handler Writing Log Messages in Your Application ---------------------------------------- The logger named ``myapp`` is reserved for your usage in your Pecan application. Once you have configured your logging, you can place logging calls in your code. Using the logging framework is very simple. :: # myapp/myapp/controllers/root.py from pecan import expose import logging logger = logging.getLogger(__name__) class RootController(object): @expose() def index(self): if bad_stuff(): logger.error('Uh-oh!') return dict() Logging to Files and Other Locations ------------------------------------ Python's :py:mod:`logging` library defines a variety of handlers that assist in writing logs to file. A few interesting ones are: * :class:`~logging.FileHandler` - used to log messages to a file on the filesystem * :class:`~logging.handlers.RotatingFileHandler` - similar to :class:`~logging.FileHandler`, but also rotates logs periodically * :class:`~logging.handlers.SysLogHandler` - used to log messages to a UNIX syslog * :class:`~logging.handlers.SMTPHandler` - used to log messages to an email address via SMTP Using any of them is as simple as defining a new handler in your application's ``logging`` block and assigning it to one of more loggers. Logging Requests with Paste Translogger --------------------------------------- `Paste `_ (which is not included with Pecan) includes the :class:`~paste.translogger.TransLogger` middleware for logging requests in `Apache Combined Log Format `_. Combined with file-based logging, TransLogger can be used to create an ``access.log`` file similar to ``Apache``. To add this middleware, modify your the ``setup_app`` method in your project's ``app.py`` as follows:: # myapp/myapp/app.py from pecan import make_app from paste.translogger import TransLogger def setup_app(config): # ... app = make_app( config.app.root # ... ) app = TransLogger(app, setup_console_handler=False) return app By default, :class:`~paste.translogger.TransLogger` creates a logger named ``wsgi``, so you'll need to specify a new (file-based) handler for this logger in our Pecan configuration file:: # myapp/config.py app = { ... } server = { ... } logging = { 'loggers': { # ... 'wsgi': {'level': 'INFO', 'handlers': ['logfile'], 'qualname': 'wsgi'} }, 'handlers': { # ... 'logfile': { 'class': 'logging.FileHandler', 'filename': '/etc/access.log', 'level': 'INFO', 'formatter': 'messageonly' } }, 'formatters': { # ... 'messageonly': {'format': '%(message)s'} } } pecan-1.5.1/docs/source/pecan_commands.rst000066400000000000000000000012771445453044500205540ustar00rootroot00000000000000.. _pecan_commands: :mod:`pecan.commands` -- Pecan Commands ======================================= The :mod:`pecan.commands` module implements the ``pecan`` console script used to provide (for example) ``pecan serve`` and ``pecan shell`` command line utilities. .. automodule:: pecan.commands.base :members: :show-inheritance: :mod:`pecan.commands.server` -- Pecan Development Server ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ .. automodule:: pecan.commands.serve :members: :show-inheritance: :mod:`pecan.commands.shell` -- Pecan Interactive Shell ++++++++++++++++++++++++++++++++++++++++++++++++++++++ .. automodule:: pecan.commands.shell :members: :show-inheritance: pecan-1.5.1/docs/source/pecan_configuration.rst000066400000000000000000000005221445453044500216120ustar00rootroot00000000000000.. _pecan_configuration: :mod:`pecan.configuration` -- Pecan Configuration Engine ======================================================== The :mod:`pecan.configuration` module provides an implementation of a Python-based configuration engine for Pecan applications. .. automodule:: pecan.configuration :members: :show-inheritance:pecan-1.5.1/docs/source/pecan_core.rst000066400000000000000000000004461445453044500177000ustar00rootroot00000000000000.. _pecan_core: :mod:`pecan.core` -- Pecan Core =============================== The :mod:`pecan.core` module is the base module for creating and extending Pecan. The core logic for processing HTTP requests and responses lives here. .. automodule:: pecan.core :members: :show-inheritance:pecan-1.5.1/docs/source/pecan_decorators.rst000066400000000000000000000004211445453044500211060ustar00rootroot00000000000000.. _pecan_decorators: :mod:`pecan.decorators` -- Pecan Decorators =========================================== The :mod:`pecan.decorators` module includes useful decorators for creating Pecan applications. .. automodule:: pecan.decorators :members: :show-inheritance:pecan-1.5.1/docs/source/pecan_deploy.rst000066400000000000000000000003671445453044500202460ustar00rootroot00000000000000.. _pecan_deploy: :mod:`pecan.deploy` -- Pecan Deploy =========================================== The :mod:`pecan.deploy` module includes fixtures to help deploy Pecan applications. .. automodule:: pecan.deploy :members: :show-inheritance: pecan-1.5.1/docs/source/pecan_hooks.rst000066400000000000000000000005441445453044500200720ustar00rootroot00000000000000.. _pecan_hooks: :mod:`pecan.hooks` -- Pecan Hooks ================================= The :mod:`pecan.hooks` module includes support for creating Pecan ``hooks`` which are a simple way to hook into the request processing of requests coming into your application to perform cross-cutting tasks. .. automodule:: pecan.hooks :members: :show-inheritance:pecan-1.5.1/docs/source/pecan_jsonify.rst000066400000000000000000000004301445453044500204220ustar00rootroot00000000000000.. _pecan_jsonify: :mod:`pecan.jsonify` -- Pecan ``JSON`` Support ============================================== The :mod:`pecan.jsonify` module includes support for ``JSON`` rule creation using generic functions. .. automodule:: pecan.jsonify :members: :show-inheritance:pecan-1.5.1/docs/source/pecan_middleware_debug.rst000066400000000000000000000003401445453044500222240ustar00rootroot00000000000000.. _pecan_middleware_debug: :mod:`pecan.middleware.debug` -- Pecan Debugging Middleware =========================================================== .. automodule:: pecan.middleware.debug :members: :show-inheritance: pecan-1.5.1/docs/source/pecan_rest.rst000066400000000000000000000004371445453044500177250ustar00rootroot00000000000000.. _pecan_rest: :mod:`pecan.rest` -- Pecan ``REST`` Controller ============================================== The :mod:`pecan.rest` module includes support for writing fully ``RESTful`` controllers in your Pecan application. .. automodule:: pecan.rest :members: :show-inheritance:pecan-1.5.1/docs/source/pecan_routing.rst000066400000000000000000000003671445453044500204410ustar00rootroot00000000000000.. _pecan_routing: :mod:`pecan.routing` -- Pecan Routing ===================================== The :mod:`pecan.routing` module is the basis for all object-dispatch routing in Pecan. .. automodule:: pecan.routing :members: :show-inheritance:pecan-1.5.1/docs/source/pecan_secure.rst000066400000000000000000000006751445453044500202420ustar00rootroot00000000000000.. _pecan_secure: :mod:`pecan.secure` -- Pecan Secure Controllers =============================================== The :mod:`pecan.secure` module includes a basic framework for building security into your applications. .. automodule:: pecan.secure :members: :show-inheritance: .. autoclass:: pecan.secure.SecureControllerBase :members: :show-inheritance: .. autoclass:: pecan.secure.SecureController :members: :show-inheritance: pecan-1.5.1/docs/source/pecan_templating.rst000066400000000000000000000005011445453044500211040ustar00rootroot00000000000000.. _pecan_templating: :mod:`pecan.templating` -- Pecan Templating =========================================== The :mod:`pecan.templating` module includes support for a variety of templating engines, plus the ability to create your own template engines. .. automodule:: pecan.templating :members: :show-inheritance:pecan-1.5.1/docs/source/pecan_testing.rst000066400000000000000000000003721445453044500204230ustar00rootroot00000000000000.. _pecan_testing: :mod:`pecan.testing` -- Pecan Testing =========================================== The :mod:`pecan.testing` module includes fixtures to help test Pecan applications. .. automodule:: pecan.testing :members: :show-inheritance: pecan-1.5.1/docs/source/pecan_util.rst000066400000000000000000000003231445453044500177170ustar00rootroot00000000000000.. _pecan_util: :mod:`pecan.util` -- Pecan Utils ================================ The :mod:`pecan.util` module includes utility functions for Pecan. .. automodule:: pecan.util :members: :show-inheritance:pecan-1.5.1/docs/source/quick_start.rst000066400000000000000000000225141445453044500201330ustar00rootroot00000000000000.. _quick_start: Creating Your First Pecan Application ===================================== Let's create a small sample project with Pecan. .. note:: This guide does not cover the installation of Pecan. If you need instructions for installing Pecan, refer to :ref:`installation`. .. _app_template: Base Application Template ------------------------- Pecan includes a basic template for starting a new project. From your shell, type:: $ pecan create test_project This example uses *test_project* as your project name, but you can replace it with any valid Python package name you like. Go ahead and change into your newly created project directory.:: $ cd test_project You'll want to deploy it in "development mode", such that it’s available on :mod:`sys.path`, yet can still be edited directly from its source distribution:: $ python setup.py develop Your new project contain these files:: $ ls ├── MANIFEST.in ├── config.py ├── public │   ├── css │   │   └── style.css │   └── images ├── setup.cfg ├── setup.py └── test_project    ├── __init__.py    ├── app.py    ├── controllers    │   ├── __init__.py    │   └── root.py    ├── model    │   └── __init__.py    ├── templates    │   ├── error.html    │   ├── index.html    │   └── layout.html    └── tests    ├── __init__.py    ├── config.py    ├── test_functional.py    └── test_units.py The number of files and directories may vary based on the version of Pecan, but the above structure should give you an idea of what to expect. Let's review the files created by the template. **public** All your static files (like CSS, Javascript, and images) live here. Pecan comes with a simple file server that serves these static files as you develop. Pecan application structure generally follows the MVC_ pattern. The directories under ``test_project`` encompass your models, controllers and templates. .. _MVC: http://en.wikipedia.org/wiki/Model–view–controller **test_project/controllers** The container directory for your controller files. **test_project/templates** All your templates go in here. **test_project/model** Container for your model files. Finally, a directory to house unit and integration tests: **test_project/tests** All of the tests for your application. The ``test_project/app.py`` file controls how the Pecan application will be created. This file must contain a :func:`setup_app` function which returns the WSGI application object. Generally you will not need to modify the ``app.py`` file provided by the base application template unless you need to customize your app in a way that cannot be accomplished using config. See :ref:`python_based_config` below. To avoid unneeded dependencies and to remain as flexible as possible, Pecan doesn't impose any database or ORM (`Object Relational Mapper`_). If your project will interact with a database, you can add code to ``model/__init__.py`` to load database bindings from your configuration file and define tables and ORM definitions. .. _Object Relational Mapper: http://en.wikipedia.org/wiki/Object-relational_mapping .. _running_application: Running the Application ----------------------- The base project template creates the configuration file with the basic settings you need to run your Pecan application in ``config.py``. This file includes the host and port to run the server on, the location where your controllers and templates are stored on disk, and the name of the directory containing any static files. If you just run :command:`pecan serve`, passing ``config.py`` as the configuration file, it will bring up the development server and serve the app:: $ pecan serve config.py Starting server in PID 000. serving on 0.0.0.0:8080, view at http://127.0.0.1:8080 The location for the configuration file and the argument itself are very flexible - you can pass an absolute or relative path to the file. .. _python_based_config: Python-Based Configuration -------------------------- For ease of use, Pecan configuration files are pure Python--they're even saved as ``.py`` files. This is how your default (generated) configuration file should look:: # Server Specific Configurations server = { 'port': '8080', 'host': '0.0.0.0' } # Pecan Application Configurations app = { 'root': '${package}.controllers.root.RootController', 'modules': ['${package}'], 'static_root': '%(confdir)s/public', 'template_path': '%(confdir)s/${package}/templates', 'debug': True, 'errors': { '404': '/error/404', '__force_dict__': True } } logging = { 'loggers': { 'root' : {'level': 'INFO', 'handlers': ['console']}, '${package}': {'level': 'DEBUG', 'handlers': ['console']} }, 'handlers': { 'console': { 'level': 'DEBUG', 'class': 'logging.StreamHandler', 'formatter': 'simple' } }, 'formatters': { 'simple': { 'format': ('%(asctime)s %(levelname)-5.5s [%(name)s]' '[%(threadName)s] %(message)s') } } } # Custom Configurations must be in Python dictionary format:: # # foo = {'bar':'baz'} # # All configurations are accessible at:: # pecan.conf You can also add your own configuration as Python dictionaries. There's a lot to cover here, so we'll come back to configuration files in a later chapter (:ref:`Configuration`). The Application Root -------------------- The **Root Controller** is the entry point for your application. You can think of it as being analogous to your application's root URL path (in our case, ``http://localhost:8080/``). This is how it looks in the project template (``test_project.controllers.root.RootController``):: from pecan import expose from webob.exc import status_map class RootController(object): @expose(generic=True, template='index.html') def index(self): return dict() @index.when(method='POST') def index_post(self, q): redirect('https://pecan.readthedocs.io/en/latest/search.html?q=%s' % q) @expose('error.html') def error(self, status): try: status = int(status) except ValueError: status = 0 message = getattr(status_map.get(status), 'explanation', '') return dict(status=status, message=message) You can specify additional classes and methods if you need to do so, but for now, let's examine the sample project, controller by controller:: @expose(generic=True, template='index.html') def index(self): return dict() The :func:`index` method is marked as *publicly available* via the :func:`~pecan.decorators.expose` decorator (which in turn uses the ``index.html`` template) at the root of the application (http://127.0.0.1:8080/), so any HTTP ``GET`` that hits the root of your application (``/``) will be routed to this method. Notice that the :func:`index` method returns a Python dictionary. This dictionary is used as a namespace to render the specified template (``index.html``) into HTML, and is the primary mechanism by which data is passed from controller to template. :: @index.when(method='POST') def index_post(self, q): redirect('https://pecan.readthedocs.io/en/latest/search.html?q=%s' % q) The :func:`index_post` method receives one HTTP ``POST`` argument (``q``). Because the argument ``method`` to :func:`@index.when` has been set to ``'POST'``, any HTTP ``POST`` to the application root (in the example project, a form submission) will be routed to this method. :: @expose('error.html') def error(self, status): try: status = int(status) except ValueError: status = 0 message = getattr(status_map.get(status), 'explanation', '') return dict(status=status, message=message) Finally, we have the :func:`error` method, which allows the application to display custom pages for certain HTTP errors (``404``, etc...). Running the Tests For Your Application -------------------------------------- Your application comes with a few example tests that you can run, replace, and add to. To run them:: $ python setup.py test -q running test running egg_info writing requirements to sam.egg-info/requires.txt writing sam.egg-info/PKG-INFO writing top-level names to sam.egg-info/top_level.txt writing dependency_links to sam.egg-info/dependency_links.txt reading manifest file 'sam.egg-info/SOURCES.txt' reading manifest template 'MANIFEST.in' writing manifest file 'sam.egg-info/SOURCES.txt' running build_ext .... ---------------------------------------------------------------------- Ran 4 tests in 0.009s OK The tests themselves can be found in the ``tests`` module in your project. Deploying to a Web Server ------------------------- Ready to deploy your new Pecan app? Take a look at :ref:`deployment`. pecan-1.5.1/docs/source/reload.rst000066400000000000000000000015411445453044500170450ustar00rootroot00000000000000:orphan: #reload Reloading Automatically as Files Change --------------------------------------- Pausing to restart your development server as you work can be interruptive, so :command:`pecan serve` provides a ``--reload`` flag to make life easier. To provide this functionality, Pecan makes use of the Python `watchdog `_ library. You'll need to install it for development use before continuing:: $ pip install watchdog Downloading/unpacking watchdog ... Successfully installed watchdog :: $ pecan serve --reload config.py Monitoring for changes... Starting server in PID 000. serving on 0.0.0.0:8080, view at http://127.0.0.1:8080 As you work, Pecan will listen for any file or directory modification events in your project and silently restart your server process in the background. pecan-1.5.1/docs/source/rest.rst000066400000000000000000000213661445453044500165630ustar00rootroot00000000000000.. _rest: Writing RESTful Web Services with Generic Controllers ===================================================== Pecan simplifies RESTful web services by providing a way to overload URLs based on the request method. For most API's, the use of `generic controller` definitions give you everything you need to build out robust RESTful interfaces (and is the *recommended* approach to writing RESTful web services in pecan): :: from pecan import abort, expose # Note: this is *not* thread-safe. In real life, use a persistent data store. BOOKS = { '0': 'The Last of the Mohicans', '1': 'Catch-22' } class BookController(object): def __init__(self, id_): self.id_ = id_ assert self.book @property def book(self): if self.id_ in BOOKS: return dict(id=self.id_, name=BOOKS[self.id_]) abort(404) # HTTP GET // @expose(generic=True, template='json') def index(self): return self.book # HTTP PUT // @index.when(method='PUT', template='json') def index_PUT(self, **kw): BOOKS[self.id_] = kw['name'] return self.book # HTTP DELETE // @index.when(method='DELETE', template='json') def index_DELETE(self): del BOOKS[self.id_] return dict() class RootController(object): @expose() def _lookup(self, id_, *remainder): return BookController(id_), remainder # HTTP GET / @expose(generic=True, template='json') def index(self): return [dict(id=k, name=v) for k, v in BOOKS.items()] # HTTP POST / @index.when(method='POST', template='json') def index_POST(self, **kw): id_ = str(len(BOOKS)) BOOKS[id_] = kw['name'] return dict(id=id_, name=kw['name']) Writing RESTful Web Services with RestController ================================================ .. _TurboGears2: http://turbogears.org For compatability with the TurboGears2_ library, Pecan also provides a class-based solution to RESTful routing, :class:`~pecan.rest.RestController`: :: from pecan import expose from pecan.rest import RestController from mymodel import Book class BooksController(RestController): @expose() def get(self, id): book = Book.get(id) if not book: abort(404) return book.title URL Mapping ----------- By default, :class:`~pecan.rest.RestController` routes as follows: +-----------------+--------------------------------------------------------------+--------------------------------------------+ | Method | Description | Example Method(s) / URL(s) | +=================+==============================================================+============================================+ | get_one | Display one record. | GET /books/1 | +-----------------+--------------------------------------------------------------+--------------------------------------------+ | get_all | Display all records in a resource. | GET /books/ | +-----------------+--------------------------------------------------------------+--------------------------------------------+ | get | A combo of get_one and get_all. | GET /books/ | | | +--------------------------------------------+ | | | GET /books/1 | +-----------------+--------------------------------------------------------------+--------------------------------------------+ | new | Display a page to create a new resource. | GET /books/new | +-----------------+--------------------------------------------------------------+--------------------------------------------+ | edit | Display a page to edit an existing resource. | GET /books/1/edit | +-----------------+--------------------------------------------------------------+--------------------------------------------+ | post | Create a new record. | POST /books/ | +-----------------+--------------------------------------------------------------+--------------------------------------------+ | put | Update an existing record. | POST /books/1?_method=put | | | +--------------------------------------------+ | | | PUT /books/1 | +-----------------+--------------------------------------------------------------+--------------------------------------------+ | get_delete | Display a delete confirmation page. | GET /books/1/delete | +-----------------+--------------------------------------------------------------+--------------------------------------------+ | delete | Delete an existing record. | POST /books/1?_method=delete | | | +--------------------------------------------+ | | | DELETE /books/1 | +-----------------+--------------------------------------------------------------+--------------------------------------------+ Pecan's :class:`~pecan.rest.RestController` uses the ``?_method=`` query string to work around the lack of support for the PUT and DELETE verbs when submitting forms in most current browsers. In addition to handling REST, the :class:`~pecan.rest.RestController` also supports the :meth:`index`, :meth:`_default`, and :meth:`_lookup` routing overrides. .. warning:: If you need to override :meth:`_route`, make sure to call :func:`RestController._route` at the end of your custom method so that the REST routing described above still occurs. Nesting ``RestController`` --------------------------- :class:`~pecan.rest.RestController` instances can be nested so that child resources receive the parameters necessary to look up parent resources. For example:: from pecan import expose from pecan.rest import RestController from mymodel import Author, Book class BooksController(RestController): @expose() def get(self, author_id, id): author = Author.get(author_id) if not author_id: abort(404) book = author.get_book(id) if not book: abort(404) return book.title class AuthorsController(RestController): books = BooksController() @expose() def get(self, id): author = Author.get(id) if not author: abort(404) return author.name class RootController(object): authors = AuthorsController() Accessing ``/authors/1/books/2`` invokes :func:`BooksController.get` with ``author_id`` set to ``1`` and ``id`` set to ``2``. To determine which arguments are associated with the parent resource, Pecan looks at the :func:`get_one` then :func:`get` method signatures, in that order, in the parent controller. If the parent resource takes a variable number of arguments, Pecan will pass it everything up to the child resource controller name (e.g., ``books`` in the above example). Defining Custom Actions ----------------------- In addition to the default methods defined above, you can add additional behaviors to a :class:`~pecan.rest.RestController` by defining a special :attr:`_custom_actions` dictionary. For example:: from pecan import expose from pecan.rest import RestController from mymodel import Book class BooksController(RestController): _custom_actions = { 'checkout': ['POST'] } @expose() def checkout(self, id): book = Book.get(id) if not book: abort(404) book.checkout() :attr:`_custom_actions` maps method names to the list of valid HTTP verbs for those custom actions. In this case :func:`checkout` supports ``POST``. pecan-1.5.1/docs/source/routing.rst000066400000000000000000000455441445453044500173010ustar00rootroot00000000000000.. _routing: Controllers and Routing ======================= Pecan uses a routing strategy known as **object-dispatch** to map an HTTP request to a controller, and then the method to call. Object-dispatch begins by splitting the path into a list of components and then walking an object path, starting at the root controller. You can imagine your application's controllers as a tree of objects (branches of the object tree map directly to URL paths). Let's look at a simple bookstore application: :: from pecan import expose class BooksController(object): @expose() def index(self): return "Welcome to book section." @expose() def bestsellers(self): return "We have 5 books in the top 10." class CatalogController(object): @expose() def index(self): return "Welcome to the catalog." books = BooksController() class RootController(object): @expose() def index(self): return "Welcome to store.example.com!" @expose() def hours(self): return "Open 24/7 on the web." catalog = CatalogController() A request for ``/catalog/books/bestsellers`` from the online store would begin with Pecan breaking the request up into ``catalog``, ``books``, and ``bestsellers``. Next, Pecan would lookup ``catalog`` on the root controller. Using the ``catalog`` object, Pecan would then lookup ``books``, followed by ``bestsellers``. What if the URL ends in a slash? Pecan will check for an ``index`` method on the last controller object. To illustrate further, the following paths: ::    └── /    ├── /hours    └── /catalog    └── /catalog/books    └── /catalog/books/bestsellers route to the following controller methods: ::    └── RootController.index    ├── RootController.hours    └── CatalogController.index    └── BooksController.index    └── BooksController.bestsellers Exposing Controllers -------------------- You tell Pecan which methods in a class are publically-visible via :func:`~pecan.decorators.expose`. If a method is *not* decorated with :func:`~pecan.decorators.expose`, Pecan will never route a request to it. :func:`~pecan.decorators.expose` can be used in a variety of ways. The simplest case involves passing no arguments. In this scenario, the controller returns a string representing the HTML response body. :: from pecan import expose class RootController(object): @expose() def hello(self): return 'Hello World' A more common use case is to :ref:`specify a template and a namespace `:: from pecan import expose class RootController(object): @expose('html_template.mako') def hello(self): return {'msg': 'Hello!'} :: ${msg} Pecan also has built-in support for a special :ref:`JSON renderer `, which translates template namespaces into rendered JSON text:: from pecan import expose class RootController(object): @expose('json') def hello(self): return {'msg': 'Hello!'} :func:`~pecan.decorators.expose` calls can also be stacked, which allows you to serialize content differently depending on how the content is requested:: from pecan import expose class RootController(object): @expose('json') @expose('text_template.mako', content_type='text/plain') @expose('html_template.mako') def hello(self): return {'msg': 'Hello!'} You'll notice that we called :func:`~pecan.decorators.expose` three times, with different arguments. :: @expose('json') The first tells Pecan to serialize the response namespace using JSON serialization when the client requests ``/hello.json`` or if an ``Accept: application/json`` header is present. :: @expose('text_template.mako', content_type='text/plain') The second tells Pecan to use the ``text_template.mako`` template file when the client requests ``/hello.txt`` or asks for text/plain via an ``Accept`` header. :: @expose('html_template.mako') The third tells Pecan to use the ``html_template.mako`` template file when the client requests ``/hello.html``. If the client requests ``/hello``, Pecan will use the ``text/html`` content type by default; in the absense of an explicit content type, Pecan assumes the client wants HTML. .. seealso:: * :ref:`pecan_decorators` Specifying Explicit Path Segments --------------------------------- Occasionally, you may want to use a path segment in your routing that doesn't work with Pecan's declarative approach to routing because of restrictions in Python's syntax. For example, if you wanted to route for a path that includes dashes, such as ``/some-path/``, the following is *not* valid Python:: class RootController(object): @pecan.expose() def some-path(self): return dict() To work around this, pecan allows you to specify an explicit path segment in the :func:`~pecan.decorators.expose` decorator:: class RootController(object): @pecan.expose(route='some-path') def some_path(self): return dict() In this example, the pecan application will reply with an ``HTTP 200`` for requests made to ``/some-path/``, but requests made to ``/some_path/`` will yield an ``HTTP 404``. :func:`~pecan.routing.route` can also be used explicitly as an alternative to the ``route`` argument in :func:`~pecan.decorators.expose`:: class RootController(object): @pecan.expose() def some_path(self): return dict() pecan.route('some-path', RootController.some_path) Routing to child controllers can be handled simliarly by utilizing :func:`~pecan.routing.route`:: class ChildController(object): @pecan.expose() def child(self): return dict() class RootController(object): pass pecan.route(RootController, 'child-path', ChildController()) In this example, the pecan application will reply with an ``HTTP 200`` for requests made to ``/child-path/child/``. Routing Based on Request Method ------------------------------- The ``generic`` argument to :func:`~pecan.decorators.expose` provides support for overloading URLs based on the request method. In the following example, the same URL can be serviced by two different methods (one for handling HTTP ``GET``, another for HTTP ``POST``) using `generic controllers`: :: from pecan import expose class RootController(object): # HTTP GET / @expose(generic=True, template='json') def index(self): return dict() # HTTP POST / @index.when(method='POST', template='json') def index_POST(self, **kw): uuid = create_something() return dict(uuid=uuid) Pecan's Routing Algorithm ------------------------- Sometimes, the standard object-dispatch routing isn't adequate to properly route a URL to a controller. Pecan provides several ways to short-circuit the object-dispatch system to process URLs with more control, including the special :func:`_lookup`, :func:`_default`, and :func:`_route` methods. Defining these methods on your controller objects provides additional flexibility for processing all or part of a URL. Routing to Subcontrollers with ``_lookup`` ------------------------------------------ The :func:`_lookup` special method provides a way to process a portion of a URL, and then return a new controller object to route to for the remainder. A :func:`_lookup` method may accept one or more arguments, segments of the URL path to be processed (split on ``/``). :func:`_lookup` should also take variable positional arguments representing the rest of the path, and it should include any portion of the path it does not process in its return value. The example below uses a ``*remainder`` list which will be passed to the returned controller when the object-dispatch algorithm continues. In addition to being used for creating controllers dynamically, :func:`_lookup` is called as a last resort, when no other controller method matches the URL and there is no :func:`_default` method. :: from pecan import expose, abort from somelib import get_student_by_name class StudentController(object): def __init__(self, student): self.student = student @expose() def name(self): return self.student.name class RootController(object): @expose() def _lookup(self, primary_key, *remainder): student = get_student_by_primary_key(primary_key) if student: return StudentController(student), remainder else: abort(404) An HTTP GET request to ``/8/name`` would return the name of the student where ``primary_key == 8``. Falling Back with ``_default`` ------------------------------ The :func:`_default` method is called as a last resort when no other controller methods match the URL via standard object-dispatch. :: from pecan import expose class RootController(object): @expose() def english(self): return 'hello' @expose() def french(self): return 'bonjour' @expose() def _default(self): return 'I cannot say hello in that language' In the example above, a request to ``/spanish`` would route to :func:`RootController._default`. Defining Customized Routing with ``_route`` ------------------------------------------- The :func:`_route` method allows a controller to completely override the routing mechanism of Pecan. Pecan itself uses the :func:`_route` method to implement its :class:`~pecan.rest.RestController`. If you want to design an alternative routing system on top of Pecan, defining a base controller class that defines a :func:`_route` method will enable you to have total control. Interacting with the Request and Response Object ================================================ For every HTTP request, Pecan maintains a :ref:`thread-local reference ` to the request and response object, ``pecan.request`` and ``pecan.response``. These are instances of :class:`pecan.Request` and :class:`pecan.Response`, respectively, and can be interacted with from within Pecan controller code:: @pecan.expose() def login(self): assert pecan.request.path == '/login' username = pecan.request.POST.get('username') password = pecan.request.POST.get('password') pecan.response.status = 403 pecan.response.text = 'Bad Login!' While Pecan abstracts away much of the need to interact with these objects directly, there may be situations where you want to access them, such as: * Inspecting components of the URI * Determining aspects of the request, such as the user's IP address, or the referer header * Setting specific response headers * Manually rendering a response body Specifying a Custom Response ---------------------------- Set a specific HTTP response code (such as ``203 Non-Authoritative Information``) by modifying the ``status`` attribute of the response object. :: from pecan import expose, response class RootController(object): @expose('json') def hello(self): response.status = 203 return {'foo': 'bar'} Use the utility function :func:`~pecan.core.abort` to raise HTTP errors. :: from pecan import expose, abort class RootController(object): @expose('json') def hello(self): abort(404) :func:`~pecan.core.abort` raises an instance of :class:`~webob.exc.WSGIHTTPException` which is used by Pecan to render default response bodies for HTTP errors. This exception is stored in the WSGI request environ at ``pecan.original_exception``, where it can be accessed later in the request cycle (by, for example, other middleware or :ref:`errors`). If you'd like to return an explicit response, you can do so using :class:`~pecan.core.Response`: :: from pecan import expose, Response class RootController(object): @expose() def hello(self): return Response('Hello, World!', 202) Extending Pecan's Request and Response Object --------------------------------------------- The request and response implementations provided by WebOb are powerful, but at times, it may be useful to extend application-specific behavior onto your request and response (such as specialized parsing of request headers or customized response body serialization). To do so, define custom classes that inherit from ``pecan.Request`` and ``pecan.Response``, respectively:: class MyRequest(pecan.Request): pass class MyResponse(pecan.Response): pass and modify your application configuration to use them:: from myproject import MyRequest, MyResponse app = { 'root' : 'project.controllers.root.RootController', 'modules' : ['project'], 'static_root' : '%(confdir)s/public', 'template_path' : '%(confdir)s/project/templates', 'request_cls': MyRequest, 'response_cls': MyResponse } Mapping Controller Arguments ---------------------------- In Pecan, HTTP ``GET`` and ``POST`` variables that are not consumed during the routing process can be passed onto the controller method as arguments. Depending on the signature of the method, these arguments can be mapped explicitly to arguments: :: from pecan import expose class RootController(object): @expose() def index(self, arg): return arg @expose() def kwargs(self, **kwargs): return str(kwargs) :: $ curl http://localhost:8080/?arg=foo foo $ curl http://localhost:8080/kwargs?a=1&b=2&c=3 {u'a': u'1', u'c': u'3', u'b': u'2'} or can be consumed positionally: :: from pecan import expose class RootController(object): @expose() def args(self, *args): return ','.join(args) :: $ curl http://localhost:8080/args/one/two/three one,two,three The same effect can be achieved with HTTP ``POST`` body variables: :: from pecan import expose class RootController(object): @expose() def index(self, arg): return arg :: $ curl -X POST "http://localhost:8080/" -H "Content-Type: application/x-www-form-urlencoded" -d "arg=foo" foo Static File Serving ------------------- Because Pecan gives you direct access to the underlying :class:`~webob.request.Request`, serving a static file download is as simple as setting the WSGI ``app_iter`` and specifying the content type:: import os from random import choice from webob.static import FileIter from pecan import expose, response class RootController(object): @expose(content_type='image/gif') def gifs(self): filepath = choice(( "/path/to/funny/gifs/catdance.gif", "/path/to/funny/gifs/babydance.gif", "/path/to/funny/gifs/putindance.gif" )) f = open(filepath, 'rb') response.app_iter = FileIter(f) response.headers[ 'Content-Disposition' ] = 'attachment; filename="%s"' % os.path.basename(f.name) If you don't know the content type ahead of time (for example, if you're retrieving files and their content types from a data store), you can specify it via ``response.headers`` rather than in the :func:`~pecan.decorators.expose` decorator:: import os from mimetypes import guess_type from webob.static import FileIter from pecan import expose, response class RootController(object): @expose() def download(self): f = open('/path/to/some/file', 'rb') response.app_iter = FileIter(f) response.headers['Content-Type'] = guess_type(f.name) response.headers[ 'Content-Disposition' ] = 'attachment; filename="%s"' % os.path.basename(f.name) Handling File Uploads --------------------- Pecan makes it easy to handle file uploads via standard multipart forms. Simply define your form with a file input: .. code-block:: html
You can then read the uploaded file off of the request object in your application's controller: :: from pecan import expose, request class RootController(object): @expose() def upload(self): assert isinstance(request.POST['file'], cgi.FieldStorage) data = request.POST['file'].file.read() Thread-Safe Per-Request Storage ------------------------------- For convenience, Pecan provides a Python dictionary on every request which can be accessed and modified in a thread-safe manner throughout the life-cycle of an individual request:: pecan.request.context['current_user'] = some_user print pecan.request.context.items() This is particularly useful in situations where you want to store metadata/context about a request (e.g., in middleware, or per-routing hooks) and access it later (e.g., in controller code). For more fine-grained control of the request, the underlying WSGI environ for a given Pecan request can be accessed and modified via ``pecan.request.environ``. Helper Functions ---------------- Pecan also provides several useful helper functions for moving between different routes. The :func:`~pecan.core.redirect` function allows you to issue internal or ``HTTP 302`` redirects. .. seealso:: The :func:`redirect` utility, along with several other useful helpers, are documented in :ref:`pecan_core`. Determining the URL for a Controller ------------------------------------ Given the ability for routing to be drastically changed at runtime, it is not always possible to correctly determine a mapping between a controller method and a URL. For example, in the following code that makes use of :func:`_lookup` to alter the routing depending on a condition:: from pecan import expose, abort from somelib import get_user_region class DefaultRegionController(object): @expose() def name(self): return "Default Region" class USRegionController(object): @expose() def name(self): return "US Region" class RootController(object): @expose() def _lookup(self, user_id, *remainder): if get_user_region(user_id) == 'us': return USRegionController(), remainder else: return DefaultRegionController(), remainder This logic depends on the geolocation of a given user and returning a completely different class given the condition. A helper to determine what URL ``USRegionController.name`` belongs to would fail to do it correctly. pecan-1.5.1/docs/source/secure_controller.rst000066400000000000000000000165501445453044500213360ustar00rootroot00000000000000.. _secure_controller: Security and Authentication =========================== Pecan provides no out-of-the-box support for authentication, but it does give you the necessary tools to handle authentication and authorization as you see fit. ``secure`` Decorator Basics --------------------------- You can wrap entire controller subtrees *or* individual method calls with access controls using the :func:`~pecan.secure.secure` decorator. To decorate a method, use one argument:: secure('') To secure a class, invoke with two arguments:: secure(object_instance, '') :: from pecan import expose from pecan.secure import secure class HighlyClassifiedController(object): pass class UnclassifiedController(object): pass class RootController(object): @classmethod def check_permissions(cls): if user_is_admin(): return True return False @expose() def index(self): # # This controller is unlocked to everyone, # and will not run any security checks. # return dict() @secure('check_permissions') @expose() def topsecret(self): # # This controller is top-secret, and should # only be reachable by administrators. # return dict() highly_classified = secure(HighlyClassifiedController(), 'check_permissions') unclassified = UnclassifiedController() ``SecureController`` -------------------- Alternatively, the same functionality can also be accomplished by subclassing Pecan's :class:`~pecan.secure.SecureController`. Implementations of :class:`~pecan.secure.SecureController` should extend the :meth:`~pecan.secure.SecureControllerBase.check_permissions` class method to return ``True`` if the user has permissions to the controller branch and ``False`` if they do not. :: from pecan import expose from pecan.secure import SecureController, unlocked class HighlyClassifiedController(object): pass class UnclassifiedController(object): pass class RootController(SecureController): @classmethod def check_permissions(cls): if user_is_admin(): return True return False @expose() @unlocked def index(self): # # This controller is unlocked to everyone, # and will not run any security checks. # return dict() @expose() def topsecret(self): # # This controller is top-secret, and should # only be reachable by administrators. # return dict() highly_classified = HighlyClassifiedController() unclassified = unlocked(UnclassifiedController()) Also note the use of the :func:`~pecan.secure.unlocked` decorator in the above example, which can be used similarly to explicitly unlock a controller for public access without any security checks. Writing Authentication/Authorization Methods -------------------------------------------- The :meth:`~pecan.secure.SecureControllerBase.check_permissions` method should be used to determine user authentication and authorization. The code you implement here could range from simple session assertions (the existing user is authenticated as an administrator) to connecting to an LDAP service. More on ``secure`` ------------------ The :func:`~pecan.secure.secure` method has several advanced uses that allow you to create robust security policies for your application. First, you can pass via a string the name of either a class method or an instance method of the controller to use as the :meth:`~pecan.secure.SecureControllerBase.check_permissions` method. Instance methods are particularly useful if you wish to authorize access to attributes of a model instance. Consider the following example of a basic virtual filesystem. :: from pecan import expose from pecan.secure import secure from myapp.session import get_current_user from myapp.model import FileObject class FileController(object): def __init__(self, name): self.file_object = FileObject(name) def read_access(self): self.file_object.read_access(get_current_user()) def write_access(self): self.file_object.write_access(get_current_user()) @secure('write_access') @expose() def upload_file(self): pass @secure('read_access') @expose() def download_file(self): pass class RootController(object): @expose() def _lookup(self, name, *remainder): return FileController(name), remainder The :func:`~pecan.secure.secure` method also accepts a function argument. When passing a function, make sure that the function is imported from another file or defined in the same file before the class definition, otherwise you will likely get error during module import. :: from pecan import expose from pecan.secure import secure from myapp.auth import user_authenitcated class RootController(object): @secure(user_authenticated) @expose() def index(self): return 'Logged in' You can also use the :func:`~pecan.secure.secure` method to change the behavior of a :class:`~pecan.secure.SecureController`. Decorating a method or wrapping a subcontroller tells Pecan to use another security function other than the default controller method. This is useful for situations where you want a different level or type of security. :: from pecan import expose from pecan.secure import SecureController, secure from myapp.auth import user_authenticated, admin_user class ApiController(object): pass class RootController(SecureController): @classmethod def check_permissions(cls): return user_authenticated() @classmethod def check_api_permissions(cls): return admin_user() @expose() def index(self): return 'logged in user' api = secure(ApiController(), 'check_api_permissions') In the example above, pecan will *only* call :func:`admin_user` when a request is made for ``/api/``. Multiple Secure Controllers --------------------------- Secure controllers can be nested to provide increasing levels of security on subcontrollers. In the example below, when a request is made for ``/admin/index/``, Pecan first calls :func:`~pecan.secure.SecureControllerBase.check_permissions` on the :class:`RootController` and then calls :func:`~pecan.secure.SecureControllerBase.check_permissions` on the :class:`AdminController`. :: from pecan import expose from pecan.secure import SecureController from myapp.auth import user_logged_in, is_admin class AdminController(SecureController): @classmethod def check_permissions(cls): return is_admin() @expose() def index(self): return 'admin dashboard' class RootController(SecureController): @classmethod def check_permissions(cls): return user_logged_in @expose() def index(self): return 'user dashboard' pecan-1.5.1/docs/source/sessions.rst000066400000000000000000000023421445453044500174450ustar00rootroot00000000000000.. _session: Working with Sessions and User Authentication ============================================= Pecan provides no opinionated support for managing user sessions, but it's easy to hook into your session framework of choice with minimal effort. This article details best practices for integrating the popular session framework, `Beaker `_, into your Pecan project. Setting up Session Management ----------------------------- There are several approaches that can be taken to set up session management. One approach is WSGI middleware. Another is Pecan :ref:`hooks`. Here's an example of wrapping your WSGI application with Beaker's :class:`~beaker.middleware.SessionMiddleware` in your project's ``app.py``. :: from pecan import conf, make_app from beaker.middleware import SessionMiddleware from test_project import model app = make_app( ... ) app = SessionMiddleware(app, conf.beaker) And a corresponding dictionary in your configuration file. :: beaker = { 'session.key' : 'sessionkey', 'session.type' : 'cookie', 'session.validate_key' : '05d2175d1090e31f42fa36e63b8d2aad', '__force_dict__' : True } pecan-1.5.1/docs/source/simple_ajax.rst000066400000000000000000000226511445453044500201000ustar00rootroot00000000000000.. _simple_ajax: Example Application: Simple AJAX ================================ This guide will walk you through building a simple Pecan web application that uses AJAX to fetch JSON data from a server. Project Setup ------------- First, you'll need to install Pecan: :: $ pip install pecan Use Pecan's basic template support to start a new project: :: $ pecan create myajax $ cd myajax Install the new project in development mode: :: $ python setup.py develop Adding JavaScript AJAX Support ------------------------------ For this project we will need to add `jQuery `_ support. To add jQuery go into the ``templates`` folder and edit the ``layout.html`` file. Adding jQuery support is easy, we actually only need one line of code: .. code-block:: html The JavaScript to make the AJAX call is a little more in depth but shouldn't be unfamiliar if you've ever worked with jQuery before. The ``layout.html`` file will look like this: .. code-block:: html ${self.title()} ${self.style()} ${self.javascript()} ${self.body()} <%def name="title()"> Default Title <%def name="style()"> <%def name="javascript()"> **What did we just do?** #. In the ``head`` section we added jQuery support via the `Google CDN `_ #. Added JavaScript to make an AJAX call to the server via an HTTP ``GET`` passing in the ``id`` of the project to fetch more information on #. Once the ``onSuccess`` event is triggered by the returning data we take that and display it on the web page below the controls Adding Additional HTML ---------------------- Let's edit the ``index.html`` file next. We will add HTML to support the AJAX interaction between the web page and Pecan. Modify ``index.html`` to look like this: .. code-block:: html <%inherit file="layout.html" /> <%def name="title()"> Welcome to Pecan!

Select a project to get details:

**What did we just do?** #. Added a dropdown control and submit button for the user to interact with. Users can pick an open source project and get more details on it Building the Model with JSON Support ------------------------------------ The HTML and JavaScript work is now taken care of. At this point we can add a model to our project inside of the ``model`` folder. Create a file in there called ``projects.py`` and add the following to it: .. code-block:: python class Project(object): def __init__(self, name, licensing, repository, documentation): self.name = name self.licensing = licensing self.repository = repository self.documentation = documentation def __json__(self): return dict( name=self.name, licensing=self.licensing, repository=self.repository, documentation=self.documentation ) **What did we just do?** #. Created a model called ``Project`` that can hold project specific data #. Added a ``__json__`` method so an instance of the ``Project class`` can be easily represented as JSON. The controller we will soon build will make use of that JSON capability .. note:: There are other ways to return JSON with Pecan, check out :ref:`jsonify` for more information. Working with the Controllers ---------------------------- We don't need to do anything major to the ``root.py`` file in the ``controllers`` folder except to add support for a new controller we will call ``ProjectsController``. Modify the ``root.py`` like this: .. code-block:: python from pecan import expose from myajax.controllers.projects import ProjectsController class RootController(object): projects = ProjectsController() @expose(generic=True, template='index.html') def index(self): return dict() **What did we just do?** #. Removed some of the initial boilerplate code since we won't be using it #. Add support for the upcoming ``ProjectsController`` The final piece is to add a file called ``projects.py`` to the ``controllers`` folder. This new file will host the ``ProjectsController`` which will listen for incoming AJAX ``GET`` calls (in our case) and return the appropriate JSON response. Add the following code to the ``projects.py`` file: .. code-block:: python from pecan import expose, response from pecan.rest import RestController from myajax.model.projects import Project class ProjectsController(RestController): # Note: You would probably store this information in a database # This is just for simplicity and demonstration purposes def __init__(self): self.projects = [ Project(name='OpenStack', licensing='Apache 2', repository='http://github.com/openstack', documentation='http://docs.openstack.org'), Project(name='Pecan', licensing='BSD', repository='http://github.com/pecan/pecan', documentation='https://pecan.readthedocs.io'), Project(name='stevedore', licensing='Apache 2', repository='http://github.com/dreamhost/pecan', documentation='http://docs.openstack.org/developer/stevedore/') ] @expose('json', content_type='application/json') def get(self, id): response.status = 200 return self.projects[int(id)] **What did we just do?** #. Created a local class variable called ``projects`` that holds three open source projects and their details. Typically this kind of information would probably reside in a database #. Added code for the new controller that will listen on the ``projects`` endpoint and serve back JSON based on the ``id`` passed in from the web page Run the application: :: $ pecan serve config.py Open a web browser: `http://127.0.0.1:8080/ `_ There is something else we could add. What if an ``id`` is passed that is not found? A proper ``HTTP 404`` should be sent back. For this we will modify the ``ProjectsController``. Change the ``get`` function to look like this: .. code-block:: python @expose('json', content_type='application/json') def get(self, id): try: response.status = 200 return self.projects[int(id)] except (IndexError, ValueError) as ex: abort(404) To test this out we need to pass an invalid ``id`` to the ``ProjectsController``. This can be done by going into the ``index.html`` and adding an additional ``option`` tag with an ``id`` value that is outside of 0-2. .. code-block:: html

Select a project to get details:

You can see that we added ``WSME`` to the list and the value is 3. Run the application: :: $ pecan serve config.py Open a web browser: `http://127.0.0.1:8080/ `_ Select ``WSME`` from the list. You should see the error dialog box triggered. pecan-1.5.1/docs/source/simple_forms_processing.rst000066400000000000000000000156741445453044500225460ustar00rootroot00000000000000.. _simple_forms_processing: Example Application: Simple Forms Processing ============================================ This guide will walk you through building a simple Pecan web application that will do some simple forms processing. Project Setup ------------- First, you'll need to install Pecan: :: $ pip install pecan Use Pecan's basic template support to start a new project: :: $ pecan create mywebsite $ cd mywebsite Install the new project in development mode: :: $ python setup.py develop With the project ready, go into the ``templates`` folder and edit the ``index.html`` file. Modify it so that it resembles this: .. code-block:: html <%inherit file="layout.html" /> <%def name="title()"> Welcome to Pecan!

Enter a message:

Enter your first name:

% if not form_post_data is UNDEFINED:

${form_post_data['first_name']}, your message is: ${form_post_data['message']}

% endif
**What did we just do?** #. Modified the contents of the ``form`` tag to have two ``input`` tags. The first is named ``message`` and the second is named ``first_name`` #. Added a check if ``form_post_data`` has not been defined so we don't show the message or wording #. Added code to display the message from the user's ``POST`` action Go into the ``controllers`` folder now and edit the ``root.py`` file. There will be two functions inside of the ``RootController`` class which will display the ``index.html`` file when your web browser hits the ``'/'`` endpoint. If the user puts some data into the textbox and hits the submit button then they will see the personalized message displayed back at them. Modify the ``root.py`` to look like this: .. code-block:: python from pecan import expose class RootController(object): @expose(generic=True, template='index.html') def index(self): return dict() @index.when(method='POST', template='index.html') def index_post(self, **kwargs): return dict(form_post_data=kwargs) **What did we just do?** #. Modified the ``index`` function to render the initial ``index.html`` webpage #. Modified the ``index_post`` function to return the posted data via keyword arguments Run the application: :: $ pecan serve config.py Open a web browser: `http://127.0.0.1:8080/ `_ Adding Validation ----------------- Enter a message into the textbox along with a name in the second textbox and press the submit button. You should see a personalized message displayed below the form once the page posts back. One problem you might have noticed is if you don't enter a message or a first name then you simply see no value entered for that part of the message. Let's add a little validation to make sure a message and a first name was actually entered. For this, we will use `WTForms `_ but you can substitute anything else for your projects. Add support for the `WTForms `_ library: :: $ pip install wtforms .. note:: Keep in mind that Pecan is not opinionated when it comes to a particular library when working with form generation, validation, etc. Choose which libraries you prefer and integrate those with Pecan. This is one way of doing this, there are many more ways so feel free to handle this however you want in your own projects. Go back to the ``root.py`` files and modify it like this: .. code-block:: python from pecan import expose, request from wtforms import Form, TextField, validators class PersonalizedMessageForm(Form): message = TextField(u'Enter a message', validators=[validators.required()]) first_name = TextField(u'Enter your first name', validators=[validators.required()]) class RootController(object): @expose(generic=True, template='index.html') def index(self): return dict(form=PersonalizedMessageForm()) @index.when(method='POST', template='index.html') def index_post(self): form = PersonalizedMessageForm(request.POST) if form.validate(): return dict(message=form.message.data, first_name=form.first_name.data) else: return dict(form=form) **What did we just do?** #. Added the ``PersonalizedMessageForm`` with two textfields and a required field validator for each #. Modified the ``index`` function to create a new instance of the ``PersonalizedMessageForm`` class and return it #. In the ``index_post`` function modify it to gather the posted data and validate it. If its valid, then set the returned data to be displayed on the webpage. If not valid, send the form which will contain the data plus the error message(s) Modify the ``index.html`` like this: .. code-block:: html <%inherit file="layout.html" /> ## provide definitions for blocks we want to redefine <%def name="title()"> Welcome to Pecan!

% if not form:

${first_name}, your message is: ${message}

% else:
${form.message.label}: ${form.message} % if form.message.errors: ${form.message.errors[0]} % endif
${form.first_name.label}: ${form.first_name} % if form.first_name.errors: ${form.first_name.errors[0]} % endif
% endif
.. note:: Keep in mind when using the `WTForms `_ library you can customize the error messages and more. Also, you have multiple validation rules so make sure to catch all the errors which will mean you need a loop rather than the simple example above which grabs the first error item in the list. See the `documentation `_ for more information. Run the application: :: $ pecan serve config.py Open a web browser: `http://127.0.0.1:8080/ `_ Try the form with valid data and with no data entered. pecan-1.5.1/docs/source/static000066400000000000000000000000001445453044500162440ustar00rootroot00000000000000pecan-1.5.1/docs/source/templates.rst000066400000000000000000000100511445453044500175710ustar00rootroot00000000000000.. _templates: Templating in Pecan =================== Pecan includes support for a variety of templating engines and also makes it easy to add support for new template engines. Currently, Pecan supports: =============== ============= Template System Renderer Name =============== ============= Mako_ mako Genshi_ genshi Kajiki_ kajiki Jinja2_ jinja JSON json =============== ============= .. _Mako: http://www.makotemplates.org/ .. _Genshi: http://genshi.edgewall.org/ .. _Kajiki: http://kajiki.pythonisito.com/ .. _Jinja2: http://jinja.pocoo.org/ The default template system is ``mako``, but that can be changed by passing the ``default_renderer`` key in your application's configuration:: app = { 'default_renderer' : 'kajiki', # ... } Using Template Renderers ------------------------ :py:mod:`pecan.decorators` defines a decorator called :func:`~pecan.decorators.expose`, which is used to flag a method as a public controller. The :func:`~pecan.decorators.expose` decorator takes a ``template`` argument, which can be used to specify the path to the template file to use for the controller method being exposed. :: class MyController(object): @expose('path/to/mako/template.html') def index(self): return dict(message='I am a mako template') :func:`~pecan.decorators.expose` will use the default template engine unless the path is prefixed by another renderer name. :: @expose('kajiki:path/to/kajiki/template.html') def my_controller(self): return dict(message='I am a kajiki template') .. seealso:: * :ref:`pecan_decorators` * :ref:`pecan_core` * :ref:`routing` Overriding Templates -------------------- :func:`~pecan.core.override_template` allows you to override the template set for a controller method when it is exposed. When :func:`~pecan.core.override_template` is called within the body of the controller method, it changes the template that will be used for that invocation of the method. :: class MyController(object): @expose('template_one.html') def index(self): # ... override_template('template_two.html') return dict(message='I will now render with template_two.html') Manual Rendering ---------------- :func:`~pecan.core.render` allows you to manually render output using the Pecan templating framework. Pass the template path and values to go into the template, and :func:`~pecan.core.render` returns the rendered output as text. :: @expose() def controller(self): return render('my_template.html', dict(message='I am the namespace')) .. _expose_json: The JSON Renderer ----------------- Pecan also provides a ``JSON`` renderer, which you can use by exposing a controller method with ``@expose('json')``. .. seealso:: * :ref:`jsonify` * :ref:`pecan_jsonify` Defining Custom Renderers ------------------------- To define a custom renderer, you can create a class that follows the renderer protocol:: class MyRenderer(object): def __init__(self, path, extra_vars): ''' Your renderer is provided with a path to templates, as configured by your application, and any extra template variables, also as configured ''' pass def render(self, template_path, namespace): ''' Lookup the template based on the path, and render your output based upon the supplied namespace dictionary, as returned from the controller. ''' return str(namespace) To enable your custom renderer, define a ``custom_renderers`` key in your application's configuration:: app = { 'custom_renderers' : { 'my_renderer' : MyRenderer }, # ... } ...and specify the renderer in the :func:`~pecan.decorators.expose` method:: class RootController(object): @expose('my_renderer:template.html') def index(self): return dict(name='Bob') pecan-1.5.1/docs/source/testing.rst000066400000000000000000000106161445453044500172570ustar00rootroot00000000000000.. _testing: Testing Pecan Applications ========================== Tests can live anywhere in your Pecan project as long as the test runner can discover them. Traditionally, they exist in a package named ``myapp.tests``. The suggested mechanism for unit and integration testing of a Pecan application is the :mod:`unittest` module. Test Discovery and Other Tools ------------------------------ Tests for a Pecan project can be invoked as simply as ``python setup.py test``, though it's possible to run your tests with different discovery and automation tools. In particular, Pecan projects are known to work well with `nose `_, `pytest `_, and `tox `_. Writing Functional Tests with WebTest ------------------------------------- A **unit test** typically relies on "mock" or "fake" objects to give the code under test enough context to run. In this way, only an individual unit of source code is tested. A healthy suite of tests combines **unit tests** with **functional tests**. In the context of a Pecan application, functional tests can be written with the help of the :mod:`webtest` library. In this way, it is possible to write tests that verify the behavior of an HTTP request life cycle from the controller routing down to the HTTP response. The following is an example that is similar to the one included with Pecan's quickstart project. :: # myapp/myapp/tests/__init__.py import os from unittest import TestCase from pecan import set_config from pecan.testing import load_test_app class FunctionalTest(TestCase): """ Used for functional tests where you need to test your literal application and its integration with the framework. """ def setUp(self): self.app = load_test_app(os.path.join( os.path.dirname(__file__), 'config.py' )) def tearDown(self): set_config({}, overwrite=True) The testing utility included with Pecan, :func:`pecan.testing.load_test_app`, can be passed a file path representing a Pecan configuration file, and will return an instance of the application, wrapped in a :class:`~webtest.app.TestApp` environment. From here, it's possible to extend the :class:`FunctionalTest` base class and write tests that issue simulated HTTP requests. :: class TestIndex(FunctionalTest): def test_index(self): resp = self.app.get('/') assert resp.status_int == 200 assert 'Hello, World' in resp.body .. seealso:: See the :mod:`webtest` documentation for further information about the methods available to a :class:`~webtest.app.TestApp` instance. Special Testing Variables ------------------------- Sometimes it's not enough to make assertions about the response body of certain requests. To aid in inspection, Pecan applications provide a special set of "testing variables" to any :class:`~webtest.response.TestResponse` object. Let's suppose that your Pecan applicaton had some controller which took a ``name`` as an optional argument in the URL. :: # myapp/myapp/controllers/root.py from pecan import expose class RootController(object): @expose('index.html') def index(self, name='Joe'): """A request to / will access this controller""" return dict(name=name) and rendered that name in it's template (and thus, the response body). :: # myapp/myapp/templates/index.html Hello, ${name}! A functional test for this controller might look something like :: class TestIndex(FunctionalTest): def test_index(self): resp = self.app.get('/') assert resp.status_int == 200 assert 'Hello, Joe!' in resp.body In addition to :attr:`webtest.TestResponse.body`, Pecan also provides :attr:`webtest.TestResponse.namespace`, which represents the template namespace returned from the controller, and :attr:`webtest.TestResponse.template_name`, which contains the name of the template used. :: class TestIndex(FunctionalTest): def test_index(self): resp = self.app.get('/') assert resp.status_int == 200 assert resp.namespace == {'name': 'Joe'} assert resp.template_name == 'index.html' In this way, it's possible to test the return value and rendered template of individual controllers. pecan-1.5.1/pecan/000077500000000000000000000000001445453044500137025ustar00rootroot00000000000000pecan-1.5.1/pecan/__init__.py000066400000000000000000000116441445453044500160210ustar00rootroot00000000000000from .core import ( abort, override_template, Pecan, Request, Response, load_app, redirect, render, request, response ) from .decorators import expose from .hooks import RequestViewerHook from .middleware.debug import DebugMiddleware from .middleware.errordocument import ErrorDocumentMiddleware from .middleware.recursive import RecursiveMiddleware from .middleware.static import StaticFileMiddleware from .routing import route from .configuration import set_config, Config from .configuration import _runtime_conf as conf from . import middleware try: from logging.config import dictConfig as load_logging_config except ImportError: from logutils.dictconfig import dictConfig as load_logging_config # noqa import warnings __all__ = [ 'make_app', 'load_app', 'Pecan', 'Request', 'Response', 'request', 'response', 'override_template', 'expose', 'conf', 'set_config', 'render', 'abort', 'redirect', 'route' ] def make_app(root, **kw): ''' Utility for creating the Pecan application object. This function should generally be called from the ``setup_app`` function in your project's ``app.py`` file. :param root: A string representing a root controller object (e.g., "myapp.controller.root.RootController") :param static_root: The relative path to a directory containing static files. Serving static files is only enabled when debug mode is set. :param debug: A flag to enable debug mode. This enables the debug middleware and serving static files. :param wrap_app: A function or middleware class to wrap the Pecan app. This must either be a wsgi middleware class or a function that returns a wsgi application. This wrapper is applied first before wrapping the application in other middlewares such as Pecan's debug middleware. This should be used if you want to use middleware to perform authentication or intercept all requests before they are routed to the root controller. :param logging: A dictionary used to configure logging. This uses ``logging.config.dictConfig``. All other keyword arguments are passed in to the Pecan app constructor. :returns: a ``Pecan`` object. ''' # Pass logging configuration (if it exists) on to the Python logging module logging = kw.get('logging', {}) debug = kw.get('debug', False) if logging: if debug: try: # # By default, Python 2.7+ silences DeprecationWarnings. # However, if conf.app.debug is True, we should probably ensure # that users see these types of warnings. # from logging import captureWarnings captureWarnings(True) warnings.simplefilter("default", DeprecationWarning) except ImportError: # No captureWarnings on Python 2.6, DeprecationWarnings are on pass if isinstance(logging, Config): logging = logging.to_dict() if 'version' not in logging: logging['version'] = 1 load_logging_config(logging) # Instantiate the WSGI app by passing **kw onward app = Pecan(root, **kw) # Optionally wrap the app in another WSGI app wrap_app = kw.get('wrap_app', None) if wrap_app: app = wrap_app(app) # Configuration for serving custom error messages errors = kw.get('errors', getattr(conf.app, 'errors', {})) if errors: app = middleware.errordocument.ErrorDocumentMiddleware(app, errors) # Included for internal redirect support app = middleware.recursive.RecursiveMiddleware(app) # When in debug mode, load exception debugging middleware static_root = kw.get('static_root', None) if debug: debug_kwargs = getattr(conf, 'debug', {}) debug_kwargs.setdefault('context_injectors', []).append( lambda environ: { 'request': environ.get('pecan.locals', {}).get('request') } ) app = DebugMiddleware( app, **debug_kwargs ) # Support for serving static files (for development convenience) if static_root: app = middleware.static.StaticFileMiddleware(app, static_root) elif static_root: warnings.warn( "`static_root` is only used when `debug` is True, ignoring", RuntimeWarning ) if hasattr(conf, 'requestviewer'): warnings.warn(''.join([ "`pecan.conf.requestviewer` is deprecated. To apply the ", "`RequestViewerHook` to your application, add it to ", "`pecan.conf.app.hooks` or manually in your project's `app.py` ", "file."]), DeprecationWarning ) return app pecan-1.5.1/pecan/commands/000077500000000000000000000000001445453044500155035ustar00rootroot00000000000000pecan-1.5.1/pecan/commands/__init__.py000066400000000000000000000002571445453044500176200ustar00rootroot00000000000000from .base import CommandRunner, BaseCommand # noqa from .serve import ServeCommand # noqa from .shell import ShellCommand # noqa from .create import CreateCommand # noqa pecan-1.5.1/pecan/commands/base.py000066400000000000000000000112571445453044500167750ustar00rootroot00000000000000import pkg_resources import argparse import logging import sys from warnings import warn log = logging.getLogger(__name__) class HelpfulArgumentParser(argparse.ArgumentParser): def error(self, message): # pragma: nocover """error(message: string) Prints a usage message incorporating the message to stderr and exits. If you override this in a subclass, it should not return -- it should either exit or raise an exception. """ self.print_help(sys.stderr) self._print_message('\n') self.exit(2, '%s: %s\n' % (self.prog, message)) class CommandManager(object): """ Used to discover `pecan.command` entry points. """ def __init__(self): self.commands = {} self.load_commands() def load_commands(self): for ep in pkg_resources.iter_entry_points('pecan.command'): log.debug('%s loading plugin %s', self.__class__.__name__, ep) if ep.name in self.commands: warn( "Duplicate entry points found on `%s` - ignoring %s" % ( ep.name, ep ), RuntimeWarning ) continue try: cmd = ep.load() cmd.run # ensure existance; catch AttributeError otherwise except Exception as e: # pragma: nocover warn("Unable to load plugin %s: %s" % (ep, e), RuntimeWarning) continue self.add({ep.name: cmd}) def add(self, cmd): self.commands.update(cmd) class CommandRunner(object): """ Dispatches `pecan` command execution requests. """ def __init__(self): self.manager = CommandManager() self.parser = HelpfulArgumentParser(add_help=True) self.parser.add_argument( '--version', action='version', version='Pecan %s' % self.version ) self.parse_sub_commands() def parse_sub_commands(self): subparsers = self.parser.add_subparsers( dest='command_name', metavar='command' ) for name, cmd in self.commands.items(): sub = subparsers.add_parser( name, help=cmd.summary ) for arg in getattr(cmd, 'arguments', tuple()): arg = arg.copy() if isinstance(arg.get('name'), str): sub.add_argument(arg.pop('name'), **arg) elif isinstance(arg.get('name'), list): sub.add_argument(*arg.pop('name'), **arg) def run(self, args): ns = self.parser.parse_args(args) if ns.command_name is None: self.run(['--help']) return self.commands[ns.command_name]().run(ns) @classmethod def handle_command_line(cls): # pragma: nocover runner = CommandRunner() runner.run(sys.argv[1:]) @property def version(self): return pkg_resources.get_distribution('pecan').version @property def commands(self): return self.manager.commands class BaseCommandMeta(type): @property def summary(cls): """ This is used to populate the --help argument on the command line. This provides a default behavior which takes the first sentence of the command's docstring and uses it. """ return cls.__doc__.strip().splitlines()[0].rstrip('.') class BaseCommandParent(object): """ A base interface for Pecan commands. Can be extended to support ``pecan`` command extensions in individual Pecan projects, e.g., $ ``pecan my-custom-command config.py`` :: # myapp/myapp/custom_command.py class CustomCommand(pecan.commands.base.BaseCommand): ''' (First) line of the docstring is used to summarize the command. ''' arguments = ({ 'name': '--extra_arg', 'help': 'an extra command line argument', 'optional': True }) def run(self, args): super(SomeCommand, self).run(args) if args.extra_arg: pass """ arguments = ({ 'name': 'config_file', 'help': 'a Pecan configuration file', 'nargs': '?', 'default': None, },) def run(self, args): """To be implemented by subclasses.""" self.args = args def load_app(self): from pecan import load_app return load_app(self.args.config_file) BaseCommand = BaseCommandMeta('BaseCommand', (BaseCommandParent,), { '__doc__': BaseCommandParent.__doc__ }) pecan-1.5.1/pecan/commands/create.py000066400000000000000000000031701445453044500173210ustar00rootroot00000000000000""" Create command for Pecan """ import pkg_resources import logging from warnings import warn from pecan.commands import BaseCommand from pecan.scaffolds import DEFAULT_SCAFFOLD log = logging.getLogger(__name__) class ScaffoldManager(object): """ Used to discover `pecan.scaffold` entry points. """ def __init__(self): self.scaffolds = {} self.load_scaffolds() def load_scaffolds(self): for ep in pkg_resources.iter_entry_points('pecan.scaffold'): log.debug('%s loading scaffold %s', self.__class__.__name__, ep) try: cmd = ep.load() cmd.copy_to # ensure existance; catch AttributeError otherwise except Exception as e: # pragma: nocover warn( "Unable to load scaffold %s: %s" % (ep, e), RuntimeWarning ) continue self.add({ep.name: cmd}) def add(self, cmd): self.scaffolds.update(cmd) class CreateCommand(BaseCommand): """ Creates the file layout for a new Pecan scaffolded project. """ manager = ScaffoldManager() arguments = ({ 'name': 'project_name', 'help': 'the (package) name of the new project' }, { 'name': 'template_name', 'metavar': 'template_name', 'help': 'a registered Pecan template', 'nargs': '?', 'default': DEFAULT_SCAFFOLD, 'choices': manager.scaffolds.keys() }) def run(self, args): super(CreateCommand, self).run(args) self.manager.scaffolds[args.template_name]().copy_to( args.project_name ) pecan-1.5.1/pecan/commands/serve.py000066400000000000000000000152531445453044500172070ustar00rootroot00000000000000""" Serve command for Pecan. """ from __future__ import print_function import logging import os import sys import threading import time import subprocess from wsgiref.simple_server import WSGIRequestHandler from pecan.commands import BaseCommand from pecan import util logger = logging.getLogger(__name__) class ServeCommand(BaseCommand): """ Serves a Pecan web application. This command serves a Pecan web application using the provided configuration file for the server and application. """ arguments = BaseCommand.arguments + ({ 'name': '--reload', 'help': 'Watch for changes and automatically reload.', 'default': False, 'action': 'store_true' },) def run(self, args): super(ServeCommand, self).run(args) app = self.load_app() self.serve(app, app.config) def create_subprocess(self): self.server_process = subprocess.Popen( [arg for arg in sys.argv if arg != '--reload'], stdout=sys.stdout, stderr=sys.stderr ) def watch_and_spawn(self, conf): from watchdog.observers import Observer from watchdog.events import ( FileSystemEventHandler, FileSystemMovedEvent, FileModifiedEvent, DirModifiedEvent ) print('Monitoring for changes...') self.create_subprocess() parent = self class AggressiveEventHandler(FileSystemEventHandler): lock = threading.Lock() def should_reload(self, event): for t in ( FileSystemMovedEvent, FileModifiedEvent, DirModifiedEvent ): if isinstance(event, t): return True return False def on_modified(self, event): if self.should_reload(event) and self.lock.acquire(False): parent.server_process.kill() parent.create_subprocess() time.sleep(1) self.lock.release() # Determine a list of file paths to monitor paths = self.paths_to_monitor(conf) event_handler = AggressiveEventHandler() for path, recurse in paths: observer = Observer() observer.schedule( event_handler, path=path, recursive=recurse ) observer.start() try: while True: time.sleep(1) except KeyboardInterrupt: pass def paths_to_monitor(self, conf): paths = [] for package_name in getattr(conf.app, 'modules', []): module = __import__(package_name, fromlist=['app']) if hasattr(module, 'app') and hasattr(module.app, 'setup_app'): paths.append(( os.path.dirname(module.__file__), True )) break paths.append((os.path.dirname(conf.__file__), False)) return paths def _serve(self, app, conf): from wsgiref.simple_server import make_server host, port = conf.server.host, int(conf.server.port) srv = make_server( host, port, app, handler_class=PecanWSGIRequestHandler, ) print('Starting server in PID %s' % os.getpid()) if host == '0.0.0.0': print( 'serving on 0.0.0.0:%s, view at http://127.0.0.1:%s' % (port, port) ) else: print("serving on http://%s:%s" % (host, port)) try: srv.serve_forever() except KeyboardInterrupt: # allow CTRL+C to shutdown pass def serve(self, app, conf): """ A very simple approach for a WSGI server. """ if self.args.reload: try: self.watch_and_spawn(conf) except ImportError: print('The `--reload` option requires `watchdog` to be ' 'installed.') print(' $ pip install watchdog') else: self._serve(app, conf) def gunicorn_run(): """ The ``gunicorn_pecan`` command for launching ``pecan`` applications """ try: from gunicorn.app.wsgiapp import WSGIApplication except ImportError as exc: args = exc.args arg0 = args[0] if args else '' arg0 += ' (are you sure `gunicorn` is installed?)' exc.args = (arg0,) + args[1:] raise class PecanApplication(WSGIApplication): def init(self, parser, opts, args): if len(args) != 1: parser.error("No configuration file was specified.") self.cfgfname = os.path.normpath( os.path.join(os.getcwd(), args[0]) ) self.cfgfname = os.path.abspath(self.cfgfname) if not os.path.exists(self.cfgfname): parser.error("Config file not found: %s" % self.cfgfname) from pecan.configuration import _runtime_conf, set_config set_config(self.cfgfname, overwrite=True) # If available, use the host and port from the pecan config file cfg = {} if _runtime_conf.get('server'): server = _runtime_conf['server'] if hasattr(server, 'host') and hasattr(server, 'port'): cfg['bind'] = '%s:%s' % ( server.host, server.port ) return cfg def load(self): from pecan.deploy import deploy return deploy(self.cfgfname) PecanApplication("%(prog)s [OPTIONS] config.py").run() class PecanWSGIRequestHandler(WSGIRequestHandler, object): """ A wsgiref request handler class that allows actual log output depending on the application configuration. """ def __init__(self, *args, **kwargs): # We set self.path to avoid crashes in log_message() on unsupported # requests (like "OPTIONS"). self.path = '' super(PecanWSGIRequestHandler, self).__init__(*args, **kwargs) def log_message(self, format, *args): """ overrides the ``log_message`` method from the wsgiref server so that normal logging works with whatever configuration the application has been set to. Levels are inferred from the HTTP status code, 4XX codes are treated as warnings, 5XX as errors and everything else as INFO level. """ code = args[1][0] levels = { '4': 'warning', '5': 'error' } log_handler = getattr(logger, levels.get(code, 'info')) log_handler(format % args) pecan-1.5.1/pecan/commands/shell.py000066400000000000000000000126251445453044500171720ustar00rootroot00000000000000""" Shell command for Pecan. """ from pecan.commands import BaseCommand try: from webtest import TestApp except ImportError: TestApp = None from warnings import warn import sys class NativePythonShell(object): """ Open an interactive python shell with the Pecan app loaded. """ @classmethod def invoke(cls, ns, banner): # pragma: nocover """ :param ns: local namespace :param banner: interactive shell startup banner Embed an interactive native python shell. """ import code py_prefix = sys.platform.startswith('java') and 'J' or 'P' shell_banner = 'Pecan Interactive Shell\n%sython %s\n\n' % \ (py_prefix, sys.version) shell = code.InteractiveConsole(locals=ns) try: import readline # noqa except ImportError: pass shell.interact(shell_banner + banner) class IPythonShell(object): """ Open an interactive ipython shell with the Pecan app loaded. """ @classmethod def invoke(cls, ns, banner): # pragma: nocover """ :param ns: local namespace :param banner: interactive shell startup banner Embed an interactive ipython shell. Try the InteractiveShellEmbed API first, fall back on IPShellEmbed for older IPython versions. """ try: from IPython.frontend.terminal.embed import ( InteractiveShellEmbed ) # try and load their default profile from IPython.frontend.terminal.ipapp import ( load_default_config ) config = load_default_config() shell = InteractiveShellEmbed(config=config, banner2=banner) shell(local_ns=ns) except ImportError: # Support for the IPython <= 0.10 shell API from IPython.Shell import IPShellEmbed shell = IPShellEmbed(argv=[]) shell.set_banner(shell.IP.BANNER + '\n\n' + banner) shell(local_ns=ns, global_ns={}) class BPythonShell(object): """ Open an interactive bpython shell with the Pecan app loaded. """ @classmethod def invoke(cls, ns, banner): # pragma: nocover """ :param ns: local namespace :param banner: interactive shell startup banner Embed an interactive bpython shell. """ from bpython import embed embed(ns, ['-i'], banner) class ShellCommand(BaseCommand): """ Open an interactive shell with the Pecan app loaded. Attempt to invoke the specified python shell flavor (ipython, bpython, etc.). Fall back on the native python shell if the requested flavor variance is not installed. """ SHELLS = { 'python': NativePythonShell, 'ipython': IPythonShell, 'bpython': BPythonShell, } arguments = BaseCommand.arguments + ({ 'name': ['--shell', '-s'], 'help': 'which Python shell to use', 'choices': SHELLS.keys(), 'default': 'python' },) def run(self, args): """ Load the pecan app, prepare the locals, sets the banner, and invokes the python shell. """ super(ShellCommand, self).run(args) # load the application app = self.load_app() # prepare the locals locs = dict(__name__='pecan-admin') locs['wsgiapp'] = app if TestApp: locs['app'] = TestApp(app) model = self.load_model(app.config) if model: locs['model'] = model # insert the pecan locals from pecan import abort, conf, redirect, request, response locs['abort'] = abort locs['conf'] = conf locs['redirect'] = redirect locs['request'] = request locs['response'] = response # prepare the banner banner = ' The following objects are available:\n' banner += ' %-10s - This project\'s WSGI App instance\n' % 'wsgiapp' banner += ' %-10s - The current configuration\n' % 'conf' if TestApp: banner += ' %-10s - webtest.TestApp wrapped around wsgiapp\n' % 'app' # noqa if model: model_name = getattr( model, '__module__', getattr(model, '__name__', 'model') ) banner += ' %-10s - Models from %s\n' % ('model', model_name) self.invoke_shell(locs, banner) def invoke_shell(self, locs, banner): """ Invokes the appropriate flavor of the python shell. Falls back on the native python shell if the requested flavor (ipython, bpython,etc) is not installed. """ shell = self.SHELLS[self.args.shell] try: shell().invoke(locs, banner) except ImportError as e: warn(( "%s is not installed, `%s`, " "falling back to native shell") % (self.args.shell, e), RuntimeWarning ) if shell == NativePythonShell: raise NativePythonShell().invoke(locs, banner) def load_model(self, config): """ Load the model extension module """ for package_name in getattr(config.app, 'modules', []): module = __import__(package_name, fromlist=['model']) if hasattr(module, 'model'): return module.model return None pecan-1.5.1/pecan/compat/000077500000000000000000000000001445453044500151655ustar00rootroot00000000000000pecan-1.5.1/pecan/compat/__init__.py000066400000000000000000000012641445453044500173010ustar00rootroot00000000000000import inspect import urllib.parse as urlparse # noqa from urllib.parse import quote, unquote_plus # noqa from urllib.request import urlopen, URLError # noqa from html import escape # noqa izip = zip def is_bound_method(ob): return inspect.ismethod(ob) and ob.__self__ is not None def getargspec(func): import sys if sys.version_info < (3, 5): return inspect.getargspec(func) from collections import namedtuple ArgSpec = namedtuple('ArgSpec', 'args varargs keywords defaults') args, varargs, keywords, defaults = inspect.getfullargspec(func)[:4] return ArgSpec(args=args, varargs=varargs, keywords=keywords, defaults=defaults) pecan-1.5.1/pecan/configuration.py000066400000000000000000000160341445453044500171270ustar00rootroot00000000000000import re import inspect import os import sys from importlib.machinery import SourceFileLoader IDENTIFIER = re.compile(r'[a-z_](\w)*$', re.IGNORECASE) DEFAULT = { # Server Specific Configurations 'server': { 'port': '8080', 'host': '0.0.0.0' }, # Pecan Application Configurations 'app': { 'root': None, 'modules': [], 'static_root': 'public', 'template_path': '', 'force_canonical': True } } class ConfigDict(dict): pass class Config(object): ''' Base class for Pecan configurations. Create a Pecan configuration object from a dictionary or a filename. :param conf_dict: A python dictionary to use for the configuration. :param filename: A filename to use for the configuration. ''' def __init__(self, conf_dict={}, filename=''): self.__values__ = {} self.__file__ = filename self.update(conf_dict) def empty(self): self.__values__ = {} def update(self, conf_dict): ''' Updates this configuration with a dictionary. :param conf_dict: A python dictionary to update this configuration with. ''' if isinstance(conf_dict, dict): iterator = iter(conf_dict.items()) else: iterator = iter(conf_dict) for k, v in iterator: if not IDENTIFIER.match(k): msg = ("'%s' is not a valid Python identifier," "consider using the '__force_dict__' key if requiring " "a native dictionary") raise ValueError(msg % k) cur_val = self.__values__.get(k) if isinstance(cur_val, Config): cur_val.update(conf_dict[k]) else: self[k] = conf_dict[k] def get(self, attribute, default=None): try: return self[attribute] except KeyError: return default def __dictify__(self, obj, prefix): ''' Private helper method for to_dict. ''' for k, v in obj.copy().items(): if prefix: del obj[k] k = "%s%s" % (prefix, k) if isinstance(v, Config): v = self.__dictify__(dict(v), prefix) obj[k] = v return obj def to_dict(self, prefix=None): ''' Converts recursively the Config object into a valid dictionary. :param prefix: A string to optionally prefix all key elements in the returned dictonary. ''' conf_obj = dict(self) return self.__dictify__(conf_obj, prefix) def __getattr__(self, name): try: return self.__values__[name] except KeyError: msg = "'pecan.conf' object has no attribute '%s'" % name raise AttributeError(msg) def __setattr__(self, key, value): if key not in ('__values__', '__file__'): self.__setitem__(key, value) return super(Config, self).__setattr__(key, value) def __getitem__(self, key): return self.__values__[key] def __setitem__(self, key, value): if isinstance(value, dict) and not isinstance(value, ConfigDict): if value.get('__force_dict__'): del value['__force_dict__'] self.__values__[key] = ConfigDict(value) else: self.__values__[key] = Config(value, filename=self.__file__) elif isinstance(value, str) and '%(confdir)s' in value: confdir = os.path.dirname(self.__file__) or os.getcwd() self.__values__[key] = value.replace('%(confdir)s', confdir) else: self.__values__[key] = value def __iter__(self): return iter(self.__values__.items()) def __dir__(self): """ When using dir() returns a list of the values in the config. Note: This function only works in Python2.6 or later. """ return list(self.__values__.keys()) def __repr__(self): return 'Config(%s)' % str(self.__values__) def conf_from_file(filepath): ''' Creates a configuration dictionary from a file. :param filepath: The path to the file. ''' abspath = os.path.abspath(os.path.expanduser(filepath)) conf_dict = {} if not os.path.isfile(abspath): raise RuntimeError('`%s` is not a file.' % abspath) # First, make sure the code will actually compile (and has no SyntaxErrors) with open(abspath, 'rb') as f: compiled = compile(f.read(), abspath, 'exec') # Next, attempt to actually import the file as a module. # This provides more verbose import-related error reporting than exec() absname, _ = os.path.splitext(abspath) basepath, module_name = absname.rsplit(os.sep, 1) SourceFileLoader(module_name, abspath).load_module(module_name) # If we were able to import as a module, actually exec the compiled code exec(compiled, globals(), conf_dict) conf_dict['__file__'] = abspath return conf_from_dict(conf_dict) def get_conf_path_from_env(): ''' If the ``PECAN_CONFIG`` environment variable exists and it points to a valid path it will return that, otherwise it will raise a ``RuntimeError``. ''' config_path = os.environ.get('PECAN_CONFIG') if not config_path: error = "PECAN_CONFIG is not set and " \ "no config file was passed as an argument." elif not os.path.isfile(config_path): error = "PECAN_CONFIG was set to an invalid path: %s" % config_path else: return config_path raise RuntimeError(error) def conf_from_dict(conf_dict): ''' Creates a configuration dictionary from a dictionary. :param conf_dict: The configuration dictionary. ''' conf = Config(filename=conf_dict.get('__file__', '')) for k, v in iter(conf_dict.items()): if k.startswith('__'): continue elif inspect.ismodule(v): continue conf[k] = v return conf def initconf(): ''' Initializes the default configuration and exposes it at ``pecan.configuration.conf``, which is also exposed at ``pecan.conf``. ''' return conf_from_dict(DEFAULT) def set_config(config, overwrite=False): ''' Updates the global configuration. :param config: Can be a dictionary containing configuration, or a string which represents a (relative) configuration filename. ''' if config is None: config = get_conf_path_from_env() # must be after the fallback other a bad fallback will incorrectly clear if overwrite is True: _runtime_conf.empty() if isinstance(config, str): config = conf_from_file(config) _runtime_conf.update(config) if config.__file__: _runtime_conf.__file__ = config.__file__ elif isinstance(config, dict): _runtime_conf.update(conf_from_dict(config)) else: raise TypeError('%s is neither a dictionary or a string.' % config) _runtime_conf = initconf() pecan-1.5.1/pecan/core.py000066400000000000000000000765351445453044500152240ustar00rootroot00000000000000from inspect import Arguments from itertools import chain, tee from mimetypes import guess_type, add_type from os.path import splitext import logging import operator import sys import types from webob import (Request as WebObRequest, Response as WebObResponse, exc, acceptparse) from webob.multidict import NestedMultiDict from .compat import urlparse, izip, is_bound_method as ismethod from .jsonify import encode as dumps from .secure import handle_security from .templating import RendererFactory from .routing import lookup_controller, NonCanonicalPath from .util import _cfg, getargspec from .middleware.recursive import ForwardRequestException # make sure that json is defined in mimetypes add_type('application/json', '.json', True) state = None logger = logging.getLogger(__name__) class RoutingState(object): def __init__(self, request, response, app, hooks=[], controller=None, arguments=None): self.request = request self.response = response self.app = app self.hooks = hooks self.controller = controller self.arguments = arguments class Request(WebObRequest): def __getattribute__(self, name): try: return WebObRequest.__getattribute__(self, name) except UnicodeDecodeError as e: logger.exception(e) abort(400) class Response(WebObResponse): pass def proxy(key): class ObjectProxy(object): explanation_ = AttributeError( "`pecan.state` is not bound to a context-local context.\n" "Ensure that you're accessing `pecan.request` or `pecan.response` " "from within the context of a WSGI `__call__` and that " "`use_context_locals` = True." ) def __getattr__(self, attr): try: obj = getattr(state, key) except AttributeError: raise self.explanation_ return getattr(obj, attr) def __setattr__(self, attr, value): obj = getattr(state, key) return setattr(obj, attr, value) def __delattr__(self, attr): obj = getattr(state, key) return delattr(obj, attr) def __dir__(self): obj = getattr(state, key) return dir(obj) return ObjectProxy() request = proxy('request') response = proxy('response') def override_template(template, content_type=None): ''' Call within a controller to override the template that is used in your response. :param template: a valid path to a template file, just as you would specify in an ``@expose``. :param content_type: a valid MIME type to use for the response.func_closure ''' request.pecan['override_template'] = template if content_type: request.pecan['override_content_type'] = content_type def abort(status_code, detail='', headers=None, comment=None, **kw): ''' Raise an HTTP status code, as specified. Useful for returning status codes like 401 Unauthorized or 403 Forbidden. :param status_code: The HTTP status code as an integer. :param detail: The message to send along, as a string. :param headers: A dictionary of headers to send along with the response. :param comment: A comment to include in the response. ''' # If there is a traceback, we need to catch it for a re-raise try: _, _, traceback = sys.exc_info() webob_exception = exc.status_map[status_code]( detail=detail, headers=headers, comment=comment, **kw ) raise webob_exception.with_traceback(traceback) finally: # Per the suggestion of the Python docs, delete the traceback object del traceback def redirect(location=None, internal=False, code=None, headers={}, add_slash=False, request=None): ''' Perform a redirect, either internal or external. An internal redirect performs the redirect server-side, while the external redirect utilizes an HTTP 302 status code. :param location: The HTTP location to redirect to. :param internal: A boolean indicating whether the redirect should be internal. :param code: The HTTP status code to use for the redirect. Defaults to 302. :param headers: Any HTTP headers to send with the response, as a dictionary. :param request: The :class:`pecan.Request` instance to use. ''' request = request or state.request if add_slash: if location is None: split_url = list(urlparse.urlsplit(request.url)) new_proto = request.environ.get( 'HTTP_X_FORWARDED_PROTO', split_url[0] ) split_url[0] = new_proto else: split_url = urlparse.urlsplit(location) split_url[2] = split_url[2].rstrip('/') + '/' location = urlparse.urlunsplit(split_url) if not headers: headers = {} if internal: if code is not None: raise ValueError('Cannot specify a code for internal redirects') request.environ['pecan.recursive.context'] = request.context raise ForwardRequestException(location) if code is None: code = 302 raise exc.status_map[code](location=location, headers=headers) def render(template, namespace, app=None): ''' Render the specified template using the Pecan rendering framework with the specified template namespace as a dictionary. Useful in a controller where you have no template specified in the ``@expose``. :param template: The path to your template, as you would specify in ``@expose``. :param namespace: The namespace to use for rendering the template, as a dictionary. :param app: The instance of :class:`pecan.Pecan` to use ''' app = app or state.app return app.render(template, namespace) def load_app(config, **kwargs): ''' Used to load a ``Pecan`` application and its environment based on passed configuration. :param config: Can be a dictionary containing configuration, a string which represents a (relative) configuration filename returns a pecan.Pecan object ''' from .configuration import _runtime_conf, set_config set_config(config, overwrite=True) for package_name in getattr(_runtime_conf.app, 'modules', []): module = __import__(package_name, fromlist=['app']) if hasattr(module, 'app') and hasattr(module.app, 'setup_app'): app = module.app.setup_app(_runtime_conf, **kwargs) app.config = _runtime_conf return app raise RuntimeError( 'No app.setup_app found in any of the configured app.modules' ) class PecanBase(object): SIMPLEST_CONTENT_TYPES = ( ['text/html'], ['text/plain'] ) def __init__(self, root, default_renderer='mako', template_path='templates', hooks=lambda: [], custom_renderers={}, extra_template_vars={}, force_canonical=True, guess_content_type_from_ext=True, context_local_factory=None, request_cls=Request, response_cls=Response, **kw): if isinstance(root, str): root = self.__translate_root__(root) self.root = root self.request_cls = request_cls self.response_cls = response_cls self.renderers = RendererFactory(custom_renderers, extra_template_vars) self.default_renderer = default_renderer # pre-sort these so we don't have to do it per-request if callable(hooks): hooks = hooks() self.hooks = list(sorted( hooks, key=operator.attrgetter('priority') )) self.template_path = template_path self.force_canonical = force_canonical self.guess_content_type_from_ext = guess_content_type_from_ext def __translate_root__(self, item): ''' Creates a root controller instance from a string root, e.g., > __translate_root__("myproject.controllers.RootController") myproject.controllers.RootController() :param item: The string to the item ''' if '.' in item: parts = item.split('.') name = '.'.join(parts[:-1]) fromlist = parts[-1:] module = __import__(name, fromlist=fromlist) kallable = getattr(module, parts[-1]) msg = "%s does not represent a callable class or function." if not callable(kallable): raise TypeError(msg % item) return kallable() raise ImportError('No item named %s' % item) def route(self, req, node, path): ''' Looks up a controller from a node based upon the specified path. :param node: The node, such as a root controller object. :param path: The path to look up on this node. ''' path = path.split('/')[1:] try: node, remainder = lookup_controller(node, path, req) return node, remainder except NonCanonicalPath as e: if self.force_canonical and \ not _cfg(e.controller).get('accept_noncanonical', False): if req.method == 'POST': raise RuntimeError( "You have POSTed to a URL '%s' which " "requires a slash. Most browsers will not maintain " "POST data when redirected. Please update your code " "to POST to '%s/' or set force_canonical to False" % (req.pecan['routing_path'], req.pecan['routing_path']) ) redirect(code=302, add_slash=True, request=req) return e.controller, e.remainder def determine_hooks(self, controller=None): ''' Determines the hooks to be run, in which order. :param controller: If specified, includes hooks for a specific controller. ''' controller_hooks = [] if controller: controller_hooks = _cfg(controller).get('hooks', []) if controller_hooks: return list( sorted( chain(controller_hooks, self.hooks), key=operator.attrgetter('priority') ) ) return self.hooks def handle_hooks(self, hooks, hook_type, *args): ''' Processes hooks of the specified type. :param hook_type: The type of hook, including ``before``, ``after``, ``on_error``, and ``on_route``. :param \*args: Arguments to pass to the hooks. ''' if hook_type not in ['before', 'on_route']: hooks = reversed(hooks) for hook in hooks: result = getattr(hook, hook_type)(*args) # on_error hooks can choose to return a Response, which will # be used instead of the standard error pages. if hook_type == 'on_error' and isinstance(result, WebObResponse): return result def get_args(self, state, all_params, remainder, argspec, im_self): ''' Determines the arguments for a controller based upon parameters passed the argument specification for the controller. ''' args = [] varargs = [] kwargs = dict() valid_args = argspec.args[:] if ismethod(state.controller) or im_self: valid_args.pop(0) # pop off `self` pecan_state = state.request.pecan remainder = [x for x in remainder if x] if im_self is not None: args.append(im_self) # grab the routing args from nested REST controllers if 'routing_args' in pecan_state: remainder = pecan_state['routing_args'] + list(remainder) del pecan_state['routing_args'] # handle positional arguments if valid_args and remainder: args.extend(remainder[:len(valid_args)]) remainder = remainder[len(valid_args):] valid_args = valid_args[len(args):] # handle wildcard arguments if [i for i in remainder if i]: if not argspec[1]: abort(404) varargs.extend(remainder) # get the default positional arguments if argspec[3]: defaults = dict(izip(argspec[0][-len(argspec[3]):], argspec[3])) else: defaults = dict() # handle positional GET/POST params for name in valid_args: if name in all_params: args.append(all_params.pop(name)) elif name in defaults: args.append(defaults[name]) else: break # handle wildcard GET/POST params if argspec[2]: for name, value in iter(all_params.items()): if name not in argspec[0]: kwargs[name] = value return args, varargs, kwargs def render(self, template, namespace): if template == 'json': renderer = self.renderers.get('json', self.template_path) elif ':' in template: renderer_name, template = template.split(':', 1) renderer = self.renderers.get( renderer_name, self.template_path ) if renderer is None: raise RuntimeError( 'support for "%s" was not found; ' % renderer_name + 'supported template engines are %s' % self.renderers.keys() ) else: renderer = self.renderers.get( self.default_renderer, self.template_path ) return renderer.render(template, namespace) def find_controller(self, state): ''' The main request handler for Pecan applications. ''' # get a sorted list of hooks, by priority (no controller hooks yet) req = state.request pecan_state = req.pecan # store the routing path for the current application to allow hooks to # modify it pecan_state['routing_path'] = path = req.path_info # handle "on_route" hooks self.handle_hooks(self.hooks, 'on_route', state) # lookup the controller, respecting content-type as requested # by the file extension on the URI pecan_state['extension'] = None # attempt to guess the content type based on the file extension if self.guess_content_type_from_ext \ and not pecan_state['content_type'] \ and '.' in path: _, extension = splitext(path.rstrip('/')) # preface with a letter to ensure compat for 2.5 potential_type = guess_type('x' + extension)[0] if extension and potential_type is not None: path = ''.join(path.rsplit(extension, 1)) pecan_state['extension'] = extension pecan_state['content_type'] = potential_type controller, remainder = self.route(req, self.root, path) cfg = _cfg(controller) if cfg.get('generic_handler'): raise exc.HTTPNotFound # handle generic controllers im_self = None if cfg.get('generic'): im_self = controller.__self__ handlers = cfg['generic_handlers'] controller = handlers.get(req.method, handlers['DEFAULT']) handle_security(controller, im_self) cfg = _cfg(controller) # add the controller to the state so that hooks can use it state.controller = controller # if unsure ask the controller for the default content type content_types = cfg.get('content_types', {}) if not pecan_state['content_type']: # attempt to find a best match based on accept headers (if they # exist) accept = getattr(req.accept, 'header_value', '*/*') or '*/*' if accept == '*/*' or ( accept.startswith('text/html,') and list(content_types.keys()) in self.SIMPLEST_CONTENT_TYPES): pecan_state['content_type'] = cfg.get( 'content_type', 'text/html' ) else: best_default = None accept_header = acceptparse.create_accept_header(accept) offers = accept_header.acceptable_offers( list(content_types.keys()) ) if offers: # If content type matches exactly use matched type best_default = offers[0][0] else: # If content type doesn't match exactly see if something # matches when not using parameters for k in content_types.keys(): if accept.startswith(k): best_default = k break if best_default is None: msg = "Controller '%s' defined does not support " + \ "content_type '%s'. Supported type(s): %s" logger.error( msg % ( controller.__name__, pecan_state['content_type'], content_types.keys() ) ) raise exc.HTTPNotAcceptable() pecan_state['content_type'] = best_default elif cfg.get('content_type') is not None and \ pecan_state['content_type'] not in content_types: msg = "Controller '%s' defined does not support content_type " + \ "'%s'. Supported type(s): %s" logger.error( msg % ( controller.__name__, pecan_state['content_type'], content_types.keys() ) ) raise exc.HTTPNotFound # fetch any parameters if req.method == 'GET': params = req.GET elif req.content_type in ('application/json', 'application/javascript'): try: if not isinstance(req.json, dict): raise TypeError('%s is not a dict' % req.json) params = NestedMultiDict(req.GET, req.json) except (TypeError, ValueError): params = req.params else: params = req.params # fetch the arguments for the controller args, varargs, kwargs = self.get_args( state, params.mixed(), remainder, cfg['argspec'], im_self ) state.arguments = Arguments(args, varargs, kwargs) # handle "before" hooks self.handle_hooks(self.determine_hooks(controller), 'before', state) return controller, args + varargs, kwargs def invoke_controller(self, controller, args, kwargs, state): ''' The main request handler for Pecan applications. ''' cfg = _cfg(controller) content_types = cfg.get('content_types', {}) req = state.request resp = state.response pecan_state = req.pecan # If a keyword is supplied via HTTP GET or POST arguments, but the # function signature does not allow it, just drop it (rather than # generating a TypeError). argspec = getargspec(controller) keys = kwargs.keys() for key in keys: if key not in argspec.args and not argspec.keywords: kwargs.pop(key) # get the result from the controller result = controller(*args, **kwargs) # a controller can return the response object which means they've taken # care of filling it out if result is response: return elif isinstance(result, WebObResponse): state.response = result return raw_namespace = result # pull the template out based upon content type and handle overrides template = content_types.get(pecan_state['content_type']) # check if for controller override of template template = pecan_state.get('override_template', template) if template is None and cfg['explicit_content_type'] is False: if self.default_renderer == 'json': template = 'json' pecan_state['content_type'] = pecan_state.get( 'override_content_type', pecan_state['content_type'] ) # if there is a template, render it if template: if template == 'json': pecan_state['content_type'] = 'application/json' result = self.render(template, result) # If we are in a test request put the namespace where it can be # accessed directly if req.environ.get('paste.testing'): testing_variables = req.environ['paste.testing_variables'] testing_variables['namespace'] = raw_namespace testing_variables['template_name'] = template testing_variables['controller_output'] = result # set the body content if result and isinstance(result, str): resp.text = result elif result: resp.body = result if pecan_state['content_type']: # set the content type resp.content_type = pecan_state['content_type'] def _handle_empty_response_body(self, state): # Enforce HTTP 204 for responses which contain no body if state.response.status_int == 200: # If the response is a generator... if isinstance(state.response.app_iter, types.GeneratorType): # Split the generator into two so we can peek at one of them # and determine if there is any response body content a, b = tee(state.response.app_iter) try: next(a) except StopIteration: # If we hit StopIteration, the body is empty state.response.status = 204 finally: state.response.app_iter = b else: text = None if state.response.charset: # `response.text` cannot be accessed without a valid # charset (because we don't know which encoding to use) try: text = state.response.text except UnicodeDecodeError: # If a valid charset is not specified, don't bother # trying to guess it (because there's obviously # content, so we know this shouldn't be a 204) pass if not any((state.response.body, text)): state.response.status = 204 if state.response.status_int in (204, 304): state.response.content_type = None def __call__(self, environ, start_response): ''' Implements the WSGI specification for Pecan applications, utilizing ``WebOb``. ''' # create the request and response object req = self.request_cls(environ) resp = self.response_cls() state = RoutingState(req, resp, self) environ['pecan.locals'] = { 'request': req, 'response': resp } controller = None # track internal redirects internal_redirect = False # handle the request try: # add context and environment to the request req.context = environ.get('pecan.recursive.context', {}) req.pecan = dict(content_type=None) controller, args, kwargs = self.find_controller(state) self.invoke_controller(controller, args, kwargs, state) except Exception as e: # if this is an HTTP Exception, set it as the response if isinstance(e, exc.HTTPException): # if the client asked for JSON, do our best to provide it accept_header = acceptparse.create_accept_header( getattr(req.accept, 'header_value', '*/*') or '*/*') offers = accept_header.acceptable_offers( ('text/plain', 'text/html', 'application/json')) best_match = offers[0][0] if offers else None state.response = e if best_match == 'application/json': json_body = dumps({ 'code': e.status_int, 'title': e.title, 'description': e.detail }) if isinstance(json_body, str): e.text = json_body else: e.body = json_body state.response.content_type = best_match environ['pecan.original_exception'] = e # note if this is an internal redirect internal_redirect = isinstance(e, ForwardRequestException) # if this is not an internal redirect, run error hooks on_error_result = None if not internal_redirect: on_error_result = self.handle_hooks( self.determine_hooks(state.controller), 'on_error', state, e ) # if the on_error handler returned a Response, use it. if isinstance(on_error_result, WebObResponse): state.response = on_error_result else: if not isinstance(e, exc.HTTPException): raise # if this is an HTTP 405, attempt to specify an Allow header if isinstance(e, exc.HTTPMethodNotAllowed) and controller: allowed_methods = _cfg(controller).get('allowed_methods', []) if allowed_methods: state.response.allow = sorted(allowed_methods) finally: # if this is not an internal redirect, run "after" hooks if not internal_redirect: self.handle_hooks( self.determine_hooks(state.controller), 'after', state ) self._handle_empty_response_body(state) # get the response return state.response(environ, start_response) class ExplicitPecan(PecanBase): def get_args(self, state, all_params, remainder, argspec, im_self): # When comparing the argspec of the method to GET/POST params, # ignore the implicit (req, resp) at the beginning of the function # signature if hasattr(state.controller, '__self__'): _repr = '.'.join(( state.controller.__self__.__class__.__module__, state.controller.__self__.__class__.__name__, state.controller.__name__ )) else: _repr = '.'.join(( state.controller.__module__, state.controller.__name__ )) signature_error = TypeError( 'When `use_context_locals` is `False`, pecan passes an explicit ' 'reference to the request and response as the first two arguments ' 'to the controller.\nChange the `%s` signature to accept exactly ' '2 initial arguments (req, resp)' % _repr ) try: positional = argspec.args[:] positional.pop(1) # req positional.pop(1) # resp argspec = argspec._replace(args=positional) except IndexError: raise signature_error args, varargs, kwargs = super(ExplicitPecan, self).get_args( state, all_params, remainder, argspec, im_self ) if ismethod(state.controller): args = [state.request, state.response] + args else: # generic controllers have an explicit self *first* # (because they're decorated functions, not instance methods) args[1:1] = [state.request, state.response] return args, varargs, kwargs class Pecan(PecanBase): ''' Pecan application object. Generally created using ``pecan.make_app``, rather than being created manually. Creates a Pecan application instance, which is a WSGI application. :param root: A string representing a root controller object (e.g., "myapp.controller.root.RootController") :param default_renderer: The default template rendering engine to use. Defaults to mako. :param template_path: A relative file system path (from the project root) where template files live. Defaults to 'templates'. :param hooks: A callable which returns a list of :class:`pecan.hooks.PecanHook` :param custom_renderers: Custom renderer objects, as a dictionary keyed by engine name. :param extra_template_vars: Any variables to inject into the template namespace automatically. :param force_canonical: A boolean indicating if this project should require canonical URLs. :param guess_content_type_from_ext: A boolean indicating if this project should use the extension in the URL for guessing the content type to return. :param use_context_locals: When `True`, `pecan.request` and `pecan.response` will be available as thread-local references. :param request_cls: Can be used to specify a custom `pecan.request` object. Defaults to `pecan.Request`. :param response_cls: Can be used to specify a custom `pecan.response` object. Defaults to `pecan.Response`. ''' def __new__(cls, *args, **kw): if kw.get('use_context_locals') is False: self = super(Pecan, cls).__new__(ExplicitPecan, *args, **kw) self.__init__(*args, **kw) return self return super(Pecan, cls).__new__(cls) def __init__(self, *args, **kw): self.init_context_local(kw.get('context_local_factory')) super(Pecan, self).__init__(*args, **kw) def __call__(self, environ, start_response): try: state.hooks = [] state.app = self state.controller = None state.arguments = None return super(Pecan, self).__call__(environ, start_response) finally: del state.hooks del state.request del state.response del state.controller del state.arguments del state.app def init_context_local(self, local_factory): global state if local_factory is None: from threading import local as local_factory state = local_factory() def find_controller(self, _state): state.request = _state.request state.response = _state.response controller, args, kw = super(Pecan, self).find_controller(_state) state.controller = controller state.arguments = _state.arguments return controller, args, kw def handle_hooks(self, hooks, *args, **kw): state.hooks = hooks return super(Pecan, self).handle_hooks(hooks, *args, **kw) pecan-1.5.1/pecan/decorators.py000066400000000000000000000133021445453044500164200ustar00rootroot00000000000000from inspect import getmembers, isclass, isfunction from .util import _cfg, getargspec __all__ = [ 'expose', 'transactional', 'accept_noncanonical', 'after_commit', 'after_rollback' ] def when_for(controller): def when(method, **kw): def decorate(f): _cfg(f)['generic_handler'] = True controller._pecan['generic_handlers'][method.upper()] = f controller._pecan['allowed_methods'].append(method.upper()) expose(**kw)(f) return f return decorate return when def expose(template=None, generic=False, route=None, **kw): ''' Decorator used to flag controller methods as being "exposed" for access via HTTP, and to configure that access. :param template: The path to a template, relative to the base template directory. Can also be passed a string representing a special or custom renderer, such as ``'json'`` for :ref:`expose_json`. :param content_type: The content-type to use for this template. :param generic: A boolean which flags this as a "generic" controller, which uses generic functions based upon ``functools.singledispatch`` generic functions. Allows you to split a single controller into multiple paths based upon HTTP method. :param route: The name of the path segment to match (excluding separator characters, like `/`). Defaults to the name of the function itself, but this can be used to resolve paths which are not valid Python function names, e.g., if you wanted to route a function to `some-special-path'. ''' content_type = kw.get('content_type', 'text/html') if template == 'json': content_type = 'application/json' def decorate(f): # flag the method as exposed f.exposed = True cfg = _cfg(f) cfg['explicit_content_type'] = 'content_type' in kw if route: # This import is here to avoid a circular import issue from pecan import routing if cfg.get('generic_handler'): raise ValueError( 'Path segments cannot be overridden for generic ' 'controllers.' ) routing.route(route, f) # set a "pecan" attribute, where we will store details cfg['content_type'] = content_type cfg.setdefault('template', []).append(template) cfg.setdefault('content_types', {})[content_type] = template # handle generic controllers if generic: if f.__name__ in ('_default', '_lookup', '_route'): raise ValueError( 'The special method %s cannot be used as a generic ' 'controller' % f.__name__ ) cfg['generic'] = True cfg['generic_handlers'] = dict(DEFAULT=f) cfg['allowed_methods'] = [] f.when = when_for(f) # store the arguments for this controller method cfg['argspec'] = getargspec(f) return f return decorate def transactional(ignore_redirects=True): ''' If utilizing the :mod:`pecan.hooks` ``TransactionHook``, allows you to flag a controller method or class as being wrapped in a transaction, regardless of HTTP method. :param ignore_redirects: Indicates if the hook should ignore redirects for this controller or not. ''' def deco(f): if isclass(f): for meth in [ m[1] for m in getmembers(f) if isfunction(m[1]) ]: if getattr(meth, 'exposed', False): _cfg(meth)['transactional'] = True _cfg(meth)['transactional_ignore_redirects'] = _cfg( meth ).get( 'transactional_ignore_redirects', ignore_redirects ) else: _cfg(f)['transactional'] = True _cfg(f)['transactional_ignore_redirects'] = ignore_redirects return f return deco def after_action(action_type, action): ''' If utilizing the :mod:`pecan.hooks` ``TransactionHook``, allows you to flag a controller method to perform a callable action after the action_type is successfully issued. :param action: The callable to call after the commit is successfully issued. ''' if action_type not in ('commit', 'rollback'): raise Exception('action_type (%s) is not valid' % action_type) def deco(func): _cfg(func).setdefault('after_%s' % action_type, []).append(action) return func return deco def after_commit(action): ''' If utilizing the :mod:`pecan.hooks` ``TransactionHook``, allows you to flag a controller method to perform a callable action after the commit is successfully issued. :param action: The callable to call after the commit is successfully issued. ''' return after_action('commit', action) def after_rollback(action): ''' If utilizing the :mod:`pecan.hooks` ``TransactionHook``, allows you to flag a controller method to perform a callable action after the rollback is successfully issued. :param action: The callable to call after the rollback is successfully issued. ''' return after_action('rollback', action) def accept_noncanonical(func): ''' Flags a controller method as accepting non-canoncial URLs. ''' _cfg(func)['accept_noncanonical'] = True return func pecan-1.5.1/pecan/deploy.py000066400000000000000000000002661445453044500155540ustar00rootroot00000000000000from .core import load_app def deploy(config): """ Given a config (dictionary of relative filename), returns a configured WSGI app. """ return load_app(config) pecan-1.5.1/pecan/ext/000077500000000000000000000000001445453044500145025ustar00rootroot00000000000000pecan-1.5.1/pecan/ext/__init__.py000066400000000000000000000002051445453044500166100ustar00rootroot00000000000000def install(): from pecan.extensions import PecanExtensionImporter PecanExtensionImporter().install() install() del install pecan-1.5.1/pecan/extensions.py000066400000000000000000000050151445453044500164540ustar00rootroot00000000000000import sys import pkg_resources import inspect import logging log = logging.getLogger(__name__) class PecanExtensionMissing(ImportError): pass class PecanExtensionImporter(object): """ Short circuits imports for extensions. This is used in combination with ``pecan.ext`` so that when a user does ``from pecan.ext import foo``, it will attempt to map ``foo`` to a registered setuptools entry point in some other (Pecan extension) project. Conversely, an extension developer may define an entry point in his ``setup.py``, e.g., setup( ... entry_points=''' [pecan.extension] celery = pecancelery.lib.core ''' ) This is mostly for convenience and consistency. In this way, Pecan can maintain an ecosystem of extensions that share a common namespace, ``pecan.ext``, while still maintaining backwards compatibility for simple package names (e.g., ``pecancelery``). """ extension_module = 'pecan.ext' prefix = extension_module + '.' def install(self): if self not in sys.meta_path: sys.meta_path.append(self) def __eq__(self, b): return self.__class__.__module__ == b.__class__.__module__ and \ self.__class__.__name__ == b.__class__.__name__ def __ne__(self, b): return not self.__eq__(b) def find_module(self, fullname, path=None): if fullname.startswith(self.prefix): return self def load_module(self, fullname): if fullname in sys.modules: return self extname = fullname.split(self.prefix)[1] module = self.find_module_for_extension(extname) realname = module.__name__ try: __import__(realname) except ImportError: raise sys.exc_info() module = sys.modules[fullname] = sys.modules[realname] if '.' not in extname: setattr(sys.modules[self.extension_module], extname, module) return module def find_module_for_extension(self, name): for ep in pkg_resources.iter_entry_points('pecan.extension'): if ep.name != name: continue log.debug('%s loading extension %s', self.__class__.__name__, ep) module = ep.load() if not inspect.ismodule(module): log.debug('%s is not a module, skipping...' % module) continue return module raise PecanExtensionMissing( 'The `pecan.ext.%s` extension is not installed.' % name ) pecan-1.5.1/pecan/hooks.py000066400000000000000000000310111445453044500153730ustar00rootroot00000000000000import builtins import types import sys from inspect import getmembers from webob.exc import HTTPFound from .util import iscontroller, _cfg __all__ = [ 'PecanHook', 'TransactionHook', 'HookController', 'RequestViewerHook' ] def walk_controller(root_class, controller, hooks, seen=None): seen = seen or set() if type(controller) not in vars(builtins).values(): # Avoid recursion loops try: if controller in seen: return seen.add(controller) except TypeError: # If we discover an unhashable item (like a list), it's not # something that we want to traverse because it's not the sort of # thing we would add a hook to return for hook in getattr(controller, '__hooks__', []): # Append hooks from controller class definition hooks.add(hook) for name, value in getmembers(controller): if name == 'controller': continue if name.startswith('__') and name.endswith('__'): continue if iscontroller(value): for hook in hooks: value._pecan.setdefault('hooks', set()).add(hook) elif hasattr(value, '__class__'): # Skip non-exposed methods that are defined in parent classes; # they're internal implementation details of that class, and # not actual routable controllers, so we shouldn't bother # assigning hooks to them. if ( isinstance(value, types.MethodType) and any(filter(lambda c: value.__func__ in c.__dict__.values(), value.__self__.__class__.mro()[1:])) ): continue walk_controller(root_class, value, hooks, seen) class HookControllerMeta(type): ''' A base class for controllers that would like to specify hooks on their controller methods. Simply create a list of hook objects called ``__hooks__`` as a member of the controller's namespace. ''' def __init__(cls, name, bases, dict_): hooks = set(dict_.get('__hooks__', [])) for base in bases: # Add hooks from parent class and mixins for hook in getattr(base, '__hooks__', []): hooks.add(hook) walk_controller(cls, cls, hooks) HookController = HookControllerMeta( 'HookController', (object,), {'__doc__': ("A base class for controllers that would like to specify " "hooks on their controller methods. Simply create a list " "of hook objects called ``__hooks__`` as a class attribute " "of your controller.")} ) class PecanHook(object): ''' A base class for Pecan hooks. Inherit from this class to create your own hooks. Set a priority on a hook by setting the ``priority`` attribute for the hook, which defaults to 100. ''' priority = 100 def on_route(self, state): ''' Override this method to create a hook that gets called upon the start of routing. :param state: The Pecan ``state`` object for the current request. ''' return def before(self, state): ''' Override this method to create a hook that gets called after routing, but before the request gets passed to your controller. :param state: The Pecan ``state`` object for the current request. ''' return def after(self, state): ''' Override this method to create a hook that gets called after the request has been handled by the controller. :param state: The Pecan ``state`` object for the current request. ''' return def on_error(self, state, e): ''' Override this method to create a hook that gets called upon an exception being raised in your controller. :param state: The Pecan ``state`` object for the current request. :param e: The ``Exception`` object that was raised. ''' return class TransactionHook(PecanHook): ''' :param start: A callable that will bind to a writable database and start a transaction. :param start_ro: A callable that will bind to a readable database. :param commit: A callable that will commit the active transaction. :param rollback: A callable that will roll back the active transaction. :param clear: A callable that will clear your current context. A basic framework hook for supporting wrapping requests in transactions. By default, it will wrap all but ``GET`` and ``HEAD`` requests in a transaction. Override the ``is_transactional`` method to define your own rules for what requests should be transactional. ''' def __init__(self, start, start_ro, commit, rollback, clear): self.start = start self.start_ro = start_ro self.commit = commit self.rollback = rollback self.clear = clear def is_transactional(self, state): ''' Decide if a request should be wrapped in a transaction, based upon the state of the request. By default, wraps all but ``GET`` and ``HEAD`` requests in a transaction, along with respecting the ``transactional`` decorator from :mod:pecan.decorators. :param state: The Pecan state object for the current request. ''' controller = getattr(state, 'controller', None) if controller: force_transactional = _cfg(controller).get('transactional', False) else: force_transactional = False if state.request.method not in ('GET', 'HEAD') or force_transactional: return True return False def on_route(self, state): state.request.error = False if self.is_transactional(state): state.request.transactional = True self.start() else: state.request.transactional = False self.start_ro() def before(self, state): if self.is_transactional(state) \ and not getattr(state.request, 'transactional', False): self.clear() state.request.transactional = True self.start() def on_error(self, state, e): # # If we should ignore redirects, # (e.g., shouldn't consider them rollback-worthy) # don't set `state.request.error = True`. # trans_ignore_redirects = ( state.request.method not in ('GET', 'HEAD') ) if state.controller is not None: trans_ignore_redirects = ( _cfg(state.controller).get( 'transactional_ignore_redirects', trans_ignore_redirects ) ) if type(e) is HTTPFound and trans_ignore_redirects is True: return state.request.error = True def after(self, state): if getattr(state.request, 'transactional', False): action_name = None if state.request.error: action_name = 'after_rollback' self.rollback() else: action_name = 'after_commit' self.commit() # # If a controller was routed to, find any # after_* actions it may have registered, and perform # them. # if action_name: controller = getattr(state, 'controller', None) if controller is not None: actions = _cfg(controller).get(action_name, []) for action in actions: action() self.clear() class RequestViewerHook(PecanHook): ''' :param config: A (optional) dictionary that can hold ``items`` and/or ``blacklist`` keys. :param writer: The stream writer to use. Can redirect output to other streams as long as the passed in stream has a ``write`` callable method. :param terminal: Outputs to the chosen stream writer (usually the terminal) :param headers: Sets values to the X-HTTP headers Returns some information about what is going on in a single request. It accepts specific items to report on but uses a default list of items when none are passed in. Based on the requested ``url``, items can also be blacklisted. Configuration is flexible, can be passed in (or not) and can contain some or all the keys supported. **items** This key holds the items that this hook will display. When this key is passed only the items in the list will be used. Valid items are *any* item that the ``request`` object holds, by default it uses the following: * path * status * method * controller * params * hooks .. note:: This key should always use a ``list`` of items to use. **blacklist** This key holds items that will be blacklisted based on ``url``. If there is a need to omit urls that start with `/javascript`, then this key would look like:: 'blacklist': ['/javascript'] As many blacklisting items as needed can be contained in the list. The hook will verify that the url is not starting with items in this list to display results, otherwise it will get omitted. .. note:: This key should always use a ``list`` of items to use. For more detailed documentation about this hook, please see :ref:`requestviewerhook` ''' available = ['path', 'status', 'method', 'controller', 'params', 'hooks'] def __init__(self, config=None, writer=sys.stdout, terminal=True, headers=True): if not config: self.config = {'items': self.available} else: if config.__class__.__name__ == 'Config': self.config = config.to_dict() else: self.config = config self.writer = writer self.items = self.config.get('items', self.available) self.blacklist = self.config.get('blacklist', []) self.terminal = terminal self.headers = headers def after(self, state): # Default and/or custom response information responses = { 'controller': lambda self, state: self.get_controller(state), 'method': lambda self, state: state.request.method, 'path': lambda self, state: state.request.path, 'params': lambda self, state: [ (p[0].encode('utf-8'), p[1].encode('utf-8')) for p in state.request.params.items() ], 'status': lambda self, state: state.response.status, 'hooks': lambda self, state: self.format_hooks(state.app.hooks), } is_available = [ i for i in self.items if i in self.available or hasattr(state.request, i) ] terminal = [] headers = [] will_skip = [ i for i in self.blacklist if state.request.path.startswith(i) ] if will_skip: return for request_info in is_available: try: value = responses.get(request_info) if not value: value = getattr(state.request, request_info) else: value = value(self, state) except Exception as e: value = e terminal.append('%-12s - %s\n' % (request_info, value)) headers.append((request_info, value)) if self.terminal: self.writer.write(''.join(terminal)) self.writer.write('\n\n') if self.headers: for h in headers: key = str(h[0]) value = str(h[1]) name = 'X-Pecan-%s' % key state.response.headers[name] = value def get_controller(self, state): ''' Retrieves the actual controller name from the application Specific to Pecan (not available in the request object) ''' path = state.request.pecan['routing_path'].split('/')[1:] return state.controller.__str__().split()[2] def format_hooks(self, hooks): ''' Tries to format the hook objects to be more readable Specific to Pecan (not available in the request object) ''' str_hooks = [str(i).split()[0].strip('<') for i in hooks] return [i.split('.')[-1] for i in str_hooks if '.' in i] pecan-1.5.1/pecan/jsonify.py000066400000000000000000000114551445453044500157430ustar00rootroot00000000000000from datetime import datetime, date from decimal import Decimal from json import JSONEncoder # depending on the version WebOb might have 2 types of dicts try: # WebOb <= 1.1.1 from webob.multidict import MultiDict, UnicodeMultiDict webob_dicts = (MultiDict, UnicodeMultiDict) # pragma: no cover except ImportError: # pragma no cover # WebOb >= 1.2 from webob.multidict import MultiDict webob_dicts = (MultiDict,) try: from functools import singledispatch except ImportError: # pragma: no cover from singledispatch import singledispatch try: import sqlalchemy # noqa try: # SQLAlchemy 2.0 support from sqlalchemy.engine import CursorResult as ResultProxy from sqlalchemy.engine import Row as RowProxy except ImportError: from sqlalchemy.engine.result import ResultProxy, RowProxy except ImportError: # dummy classes since we don't have SQLAlchemy installed class ResultProxy(object): # noqa pass class RowProxy(object): # noqa pass try: from sqlalchemy.engine.cursor import LegacyCursorResult, LegacyRow except ImportError: # pragma no cover # dummy classes since we don't have SQLAlchemy installed # or we're using SQLAlchemy < 1.4 class LegacyCursorResult(object): # noqa pass class LegacyRow(object): # noqa pass # # encoders # def is_saobject(obj): return hasattr(obj, '_sa_class_manager') class GenericJSON(JSONEncoder): ''' Generic JSON encoder. Makes several attempts to correctly JSONify requested response objects. ''' def default(self, obj): ''' Converts an object and returns a ``JSON``-friendly structure. :param obj: object or structure to be converted into a ``JSON``-ifiable structure Considers the following special cases in order: * object has a callable __json__() attribute defined returns the result of the call to __json__() * date and datetime objects returns the object cast to str * Decimal objects returns the object cast to float * SQLAlchemy objects returns a copy of the object.__dict__ with internal SQLAlchemy parameters removed * SQLAlchemy ResultProxy objects Casts the iterable ResultProxy into a list of tuples containing the entire resultset data, returns the list in a dictionary along with the resultset "row" count. .. note:: {'count': 5, 'rows': [('Ed Jones',), ('Pete Jones',), ('Wendy Williams',), ('Mary Contrary',), ('Fred Smith',)]} * SQLAlchemy RowProxy objects Casts the RowProxy cursor object into a dictionary, probably losing its ordered dictionary behavior in the process but making it JSON-friendly. * webob_dicts objects returns webob_dicts.mixed() dictionary, which is guaranteed to be JSON-friendly. ''' if hasattr(obj, '__json__') and callable(obj.__json__): return obj.__json__() elif isinstance(obj, (date, datetime)): return str(obj) elif isinstance(obj, Decimal): # XXX What to do about JSONEncoder crappy handling of Decimals? # SimpleJSON has better Decimal encoding than the std lib # but only in recent versions return float(obj) elif is_saobject(obj): props = {} for key in obj.__dict__: if not key.startswith('_sa_'): props[key] = getattr(obj, key) return props elif isinstance(obj, ResultProxy): props = dict(rows=list(obj), count=obj.rowcount) if props['count'] < 0: props['count'] = len(props['rows']) return props elif isinstance(obj, LegacyCursorResult): rows = [dict(row._mapping) for row in obj.fetchall()] return {'count': len(rows), 'rows': rows} elif isinstance(obj, LegacyRow): return dict(obj._mapping) elif isinstance(obj, RowProxy): if obj.__class__.__name__ == 'Row': # SQLAlchemy 2.0 support obj = obj._mapping return dict(obj) elif isinstance(obj, webob_dicts): return obj.mixed() else: return JSONEncoder.default(self, obj) _default = GenericJSON() def with_when_type(f): # Add some backwards support for simplegeneric's API f.when_type = f.register return f @with_when_type @singledispatch def jsonify(obj): return _default.default(obj) class GenericFunctionJSON(GenericJSON): def default(self, obj): return jsonify(obj) _instance = GenericFunctionJSON() def encode(obj): return _instance.encode(obj) pecan-1.5.1/pecan/log.py000066400000000000000000000032061445453044500150360ustar00rootroot00000000000000import logging from logutils.colorize import ColorizingStreamHandler class DefaultColorizer(ColorizingStreamHandler): level_map = { logging.DEBUG: (None, 'blue', True), logging.INFO: (None, None, True), logging.WARNING: (None, 'yellow', True), logging.ERROR: (None, 'red', True), logging.CRITICAL: (None, 'red', True), } class ColorFormatter(logging.Formatter): """ A very basic logging formatter that not only applies color to the levels of the ouput but can also add padding to the the level names so that they do not alter the visuals of logging when presented on the terminal. The padding is provided by a convenient keyword that adds padding to the ``levelname`` so that log output is easier to follow:: %(padded_color_levelname)s Which would result in log level output that looks like:: [INFO ] [WARNING ] [ERROR ] [DEBUG ] [CRITICAL] If colored output is not supported, it falls back to non-colored output without any extra settings. """ def __init__(self, _logging=None, colorizer=None, *a, **kw): self.logging = _logging or logging self.color = colorizer or DefaultColorizer() logging.Formatter.__init__(self, *a, **kw) def format(self, record): levelname = record.levelname padded_level = '%-8s' % levelname record.color_levelname = self.color.colorize(levelname, record) record.padded_color_levelname = self.color.colorize( padded_level, record ) return self.logging.Formatter.format(self, record) pecan-1.5.1/pecan/middleware/000077500000000000000000000000001445453044500160175ustar00rootroot00000000000000pecan-1.5.1/pecan/middleware/__init__.py000066400000000000000000000001111445453044500201210ustar00rootroot00000000000000from . import errordocument from . import recursive from . import static pecan-1.5.1/pecan/middleware/debug.py000066400000000000000000000056421445453044500174660ustar00rootroot00000000000000__CONFIG_HELP__ = b'''
To disable this interface, set
conf.app.debug = False
''' # noqa try: import re from backlash.debug import DebuggedApplication class DebugMiddleware(DebuggedApplication): body_re = re.compile(b'(]*>)', re.I) def debug_application(self, environ, start_response): for part in super(DebugMiddleware, self).debug_application( environ, start_response ): yield self.body_re.sub(b'<1>%s' % __CONFIG_HELP__, part) except ImportError: import logging from traceback import print_exc from pprint import pformat from mako.template import Template from io import StringIO from webob import Response from webob.exc import HTTPException LOG = logging.getLogger(__file__) debug_template_raw = ''' Pecan - Application Error

An error occurred!

%(config_help)s Pecan offers support for interactive debugging by installing the backlash package:

pip install backlash
...and reloading this page.

Traceback

${traceback}

WSGI Environment

${environment}
''' % {'config_help': __CONFIG_HELP__} # noqa debug_template = Template(debug_template_raw) class DebugMiddleware(object): def __init__(self, app, *args, **kwargs): self.app = app def __call__(self, environ, start_response): try: return self.app(environ, start_response) except Exception as exc: # get a formatted exception out = StringIO() print_exc(file=out) LOG.exception(exc) # get formatted WSGI environment formatted_environ = pformat(environ) # render our template result = debug_template.render( traceback=out.getvalue(), environment=formatted_environ ) # construct and return our response response = Response() if isinstance(exc, HTTPException): response.status_int = exc.status else: response.status_int = 500 response.unicode_body = result return response(environ, start_response) pecan-1.5.1/pecan/middleware/errordocument.py000066400000000000000000000050001445453044500212540ustar00rootroot00000000000000import sys from .recursive import ForwardRequestException, RecursionLoop class StatusPersist(object): def __init__(self, app, status, url): self.app = app self.status = status self.url = url def __call__(self, environ, start_response): def keep_status_start_response(status, headers, exc_info=None): return start_response(self.status, headers, exc_info) parts = self.url.split('?') environ['PATH_INFO'] = parts[0] if len(parts) > 1: environ['QUERY_STRING'] = parts[1] else: environ['QUERY_STRING'] = '' try: return self.app(environ, keep_status_start_response) except RecursionLoop as e: environ['wsgi.errors'].write( 'Recursion error getting error page: %s\n' % e ) keep_status_start_response( '500 Server Error', [('Content-type', 'text/plain')], sys.exc_info() ) return [ b'Error: %s. (Error page could not be fetched)' % ( self.status.encode('utf-8') ) ] class ErrorDocumentMiddleware(object): ''' Intersects HTTP response status code, looks it up in the error map defined in the Pecan app config.py, and routes to the controller assigned to that status. ''' def __init__(self, app, error_map): self.app = app self.error_map = error_map def __call__(self, environ, start_response): def replacement_start_response(status, headers, exc_info=None): ''' Overrides the default response if the status is defined in the Pecan app error map configuration. ''' try: status_code = int(status.split(' ')[0]) except (ValueError, TypeError): # pragma: nocover raise Exception(( 'ErrorDocumentMiddleware received an invalid ' 'status %s' % status )) if status_code in self.error_map: def factory(app): return StatusPersist( app, status, self.error_map[status_code] ) raise ForwardRequestException(factory=factory) return start_response(status, headers, exc_info) app_iter = self.app(environ, replacement_start_response) return app_iter pecan-1.5.1/pecan/middleware/recursive.py000066400000000000000000000154441445453044500204100ustar00rootroot00000000000000# (c) 2005 Ian Bicking and contributors; written for Paste # Licensed under the MIT license: # http://www.opensource.org/licenses/mit-license.php """ Middleware to make internal requests and forward requests internally. Raise ``ForwardRequestException(new_path_info)`` to do a forward (aborting the current request). """ __all__ = ['RecursiveMiddleware'] class RecursionLoop(AssertionError): # Subclasses AssertionError for legacy reasons """Raised when a recursion enters into a loop""" class CheckForRecursionMiddleware(object): def __init__(self, app, env): self.app = app self.env = env def __call__(self, environ, start_response): path_info = environ.get('PATH_INFO', '') if path_info in self.env.get('pecan.recursive.old_path_info', []): raise RecursionLoop( "Forwarding loop detected; %r visited twice (internal " "redirect path: %s)" % (path_info, self.env['pecan.recursive.old_path_info']) ) old_path_info = self.env.setdefault( 'pecan.recursive.old_path_info', [] ) old_path_info.append(self.env.get('PATH_INFO', '')) return self.app(environ, start_response) class RecursiveMiddleware(object): """ A WSGI middleware that allows for recursive and forwarded calls. All these calls go to the same 'application', but presumably that application acts differently with different URLs. The forwarded URLs must be relative to this container. """ def __init__(self, application, global_conf=None): self.application = application def __call__(self, environ, start_response): my_script_name = environ.get('SCRIPT_NAME', '') environ['pecan.recursive.script_name'] = my_script_name try: return self.application(environ, start_response) except ForwardRequestException as e: middleware = CheckForRecursionMiddleware( e.factory(self), environ) return middleware(environ, start_response) class ForwardRequestException(Exception): """ Used to signal that a request should be forwarded to a different location. ``url`` The URL to forward to starting with a ``/`` and relative to ``RecursiveMiddleware``. URL fragments can also contain query strings so ``/error?code=404`` would be a valid URL fragment. ``environ`` An altertative WSGI environment dictionary to use for the forwarded request. If specified is used *instead* of the ``url_fragment`` ``factory`` If specifed ``factory`` is used instead of ``url`` or ``environ``. ``factory`` is a callable that takes a WSGI application object as the first argument and returns an initialised WSGI middleware which can alter the forwarded response. Basic usage (must have ``RecursiveMiddleware`` present) : .. code-block:: python from pecan.middleware.recursive import ForwardRequestException def app(environ, start_response): if environ['PATH_INFO'] == '/hello': start_response("200 OK", [('Content-type', 'text/plain')]) return ['Hello World!'] elif environ['PATH_INFO'] == '/error': start_response("404 Not Found", [('Content-type', 'text/plain')] ) return ['Page not found'] else: raise ForwardRequestException('/error') from pecan.middleware.recursive import RecursiveMiddleware app = RecursiveMiddleware(app) If you ran this application and visited ``/hello`` you would get a ``Hello World!`` message. If you ran the application and visited ``/not_found`` a ``ForwardRequestException`` would be raised and the caught by the ``RecursiveMiddleware``. The ``RecursiveMiddleware`` would then return the headers and response from the ``/error`` URL but would display a ``404 Not found`` status message. You could also specify an ``environ`` dictionary instead of a url. Using the same example as before: .. code-block:: python def app(environ, start_response): ... same as previous example ... else: new_environ = environ.copy() new_environ['PATH_INFO'] = '/error' raise ForwardRequestException(environ=new_environ) """ def __init__(self, url=None, environ={}, factory=None, path_info=None): # Check no incompatible options have been chosen if factory and url: raise TypeError( 'You cannot specify factory and a url in ' 'ForwardRequestException' ) # pragma: nocover elif factory and environ: raise TypeError( 'You cannot specify factory and environ in ' 'ForwardRequestException' ) # pragma: nocover if url and environ: raise TypeError( 'You cannot specify environ and url in ' 'ForwardRequestException' ) # pragma: nocover # set the path_info or warn about its use. if path_info: self.path_info = path_info # If the url can be treated as a path_info do that if url and '?' not in str(url): self.path_info = url # Base middleware class ForwardRequestExceptionMiddleware(object): def __init__(self, app): self.app = app # Otherwise construct the appropriate middleware factory if hasattr(self, 'path_info'): p = self.path_info def factory_pi(app): class PathInfoForward(ForwardRequestExceptionMiddleware): def __call__(self, environ, start_response): environ['PATH_INFO'] = p return self.app(environ, start_response) return PathInfoForward(app) self.factory = factory_pi elif url: def factory_url(app): class URLForward(ForwardRequestExceptionMiddleware): def __call__(self, environ, start_response): environ['PATH_INFO'] = url.split('?')[0] environ['QUERY_STRING'] = url.split('?')[1] return self.app(environ, start_response) return URLForward(app) self.factory = factory_url elif environ: def factory_env(app): class EnvironForward(ForwardRequestExceptionMiddleware): def __call__(self, environ_, start_response): return self.app(environ, start_response) return EnvironForward(app) self.factory = factory_env else: self.factory = factory pecan-1.5.1/pecan/middleware/static.py000066400000000000000000000127241445453044500176660ustar00rootroot00000000000000""" This code is adapted from the Werkzeug project, under the BSD license. :copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details. :license: BSD, see LICENSE for more details. """ import os import mimetypes from datetime import datetime from time import gmtime class FileWrapper(object): """This class can be used to convert a :class:`file`-like object into an iterable. It yields `buffer_size` blocks until the file is fully read. You should not use this class directly but rather use the :func:`wrap_file` function that uses the WSGI server's file wrapper support if it's available. :param file: a :class:`file`-like object with a :meth:`~file.read` method. :param buffer_size: number of bytes for one iteration. """ def __init__(self, file, buffer_size=8192): self.file = file self.buffer_size = buffer_size def close(self): if hasattr(self.file, 'close'): self.file.close() def __iter__(self): return self def __next__(self): data = self.file.read(self.buffer_size) if data: return data raise StopIteration() def wrap_file(environ, file, buffer_size=8192): """Wraps a file. This uses the WSGI server's file wrapper if available or otherwise the generic :class:`FileWrapper`. If the file wrapper from the WSGI server is used it's important to not iterate over it from inside the application but to pass it through unchanged. More information about file wrappers are available in :pep:`333`. :param file: a :class:`file`-like object with a :meth:`~file.read` method. :param buffer_size: number of bytes for one iteration. """ return environ.get('wsgi.file_wrapper', FileWrapper)(file, buffer_size) def _dump_date(d, delim): """Used for `http_date` and `cookie_date`.""" if d is None: d = gmtime() elif isinstance(d, datetime): d = d.utctimetuple() elif isinstance(d, (int, float)): d = gmtime(d) return '%s, %02d%s%s%s%s %02d:%02d:%02d GMT' % ( ('Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun')[d.tm_wday], d.tm_mday, delim, ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec')[d.tm_mon - 1], delim, str(d.tm_year), d.tm_hour, d.tm_min, d.tm_sec ) def http_date(timestamp=None): """Formats the time to match the RFC1123 date format. Accepts a floating point number expressed in seconds since the epoch in, a datetime object or a timetuple. All times in UTC. Outputs a string in the format ``Wdy, DD Mon YYYY HH:MM:SS GMT``. :param timestamp: If provided that date is used, otherwise the current. """ return _dump_date(timestamp, ' ') class StaticFileMiddleware(object): """A WSGI middleware that provides static content for development environments. Currently the middleware does not support non ASCII filenames. If the encoding on the file system happens to be the encoding of the URI it may work but this could also be by accident. We strongly suggest using ASCII only file names for static files. The middleware will guess the mimetype using the Python `mimetype` module. If it's unable to figure out the charset it will fall back to `fallback_mimetype`. :param app: the application to wrap. If you don't want to wrap an application you can pass it :exc:`NotFound`. :param directory: the directory to serve up. :param fallback_mimetype: the fallback mimetype for unknown files. """ def __init__(self, app, directory, fallback_mimetype='text/plain'): self.app = app self.loader = self.get_directory_loader(directory) self.fallback_mimetype = fallback_mimetype def _opener(self, filename): return lambda: ( open(filename, 'rb'), datetime.utcfromtimestamp(os.path.getmtime(filename)), int(os.path.getsize(filename)) ) def get_directory_loader(self, directory): def loader(path): path = path or directory if path is not None: path = os.path.join(directory, path) if os.path.isfile(path): return os.path.basename(path), self._opener(path) return None, None return loader def __call__(self, environ, start_response): # sanitize the path for non unix systems cleaned_path = environ.get('PATH_INFO', '').strip('/') for sep in os.sep, os.altsep: if sep and sep != '/': cleaned_path = cleaned_path.replace(sep, '/') path = '/'.join([''] + [x for x in cleaned_path.split('/') if x and x != '..']) # attempt to find a loader for the file real_filename, file_loader = self.loader(path[1:]) if file_loader is None: return self.app(environ, start_response) # serve the file with the appropriate name if we found it guessed_type = mimetypes.guess_type(real_filename) mime_type = guessed_type[0] or self.fallback_mimetype f, mtime, file_size = file_loader() headers = [('Date', http_date())] headers.append(('Cache-Control', 'public')) headers.extend(( ('Content-Type', mime_type), ('Content-Length', str(file_size)), ('Last-Modified', http_date(mtime)) )) start_response('200 OK', headers) return wrap_file(environ, f) pecan-1.5.1/pecan/rest.py000066400000000000000000000360011445453044500152310ustar00rootroot00000000000000from inspect import ismethod, getmembers import warnings from webob import exc from .core import abort from .decorators import expose from .routing import lookup_controller, handle_lookup_traversal from .util import iscontroller, getargspec class RestController(object): ''' A base class for ``REST`` based controllers. Inherit from this class to implement a REST controller. ``RestController`` implements a set of routing functions which override the default pecan routing with behavior consistent with RESTful routing. This functionality covers navigation to the requested resource controllers, and the appropriate handling of both the common (``GET``, ``POST``, ``PUT``, ``DELETE``) as well as custom-defined REST action methods. For more on developing **RESTful** web applications with Pecan, see :ref:`rest`. ''' _custom_actions = {} def __new__(cls, *args, **kwargs): """ RestController does not support the `route` argument to :func:`~pecan.decorators.expose` Implement this with __new__ rather than a metaclass, because it's very common for pecan users to mixin RestController (with other bases that have their own metaclasses). """ for name, value in getmembers(cls): if iscontroller(value) and getattr(value, 'custom_route', None): raise ValueError( 'Path segments cannot be used in combination with ' 'pecan.rest.RestController. Remove the `route` argument ' 'to @pecan.expose on %s.%s.%s' % ( cls.__module__, cls.__name__, value.__name__ ) ) # object.__new__ will error if called with extra arguments, and either # __new__ is overridden or __init__ is not overridden; # https://hg.python.org/cpython/file/78d36d54391c/Objects/typeobject.c#l3034 # In PY3, this is a TypeError new = super(RestController, cls).__new__ if new is object.__new__: return new(cls) return new(cls, *args, **kwargs) def _get_args_for_controller(self, controller): """ Retrieve the arguments we actually care about. For Pecan applications that utilize thread locals, we should truncate the first argument, `self`. For applications that explicitly pass request/response references as the first controller arguments, we should truncate the first three arguments, `self, req, resp`. """ argspec = getargspec(controller) from pecan import request try: request.path except AttributeError: return argspec.args[3:] return argspec.args[1:] def _handle_bad_rest_arguments(self, controller, remainder, request): """ Ensure that the argspec for a discovered controller actually matched the positional arguments in the request path. If not, raise a webob.exc.HTTPBadRequest. """ argspec = self._get_args_for_controller(controller) fixed_args = len(argspec) - len( request.pecan.get('routing_args', []) ) if len(remainder) < fixed_args: # For controllers that are missing intermediate IDs # (e.g., /authors/books vs /authors/1/books), return a 404 for an # invalid path. abort(404) def _lookup_child(self, remainder): """ Lookup a child controller with a named path (handling Unicode paths properly for Python 2). """ try: controller = getattr(self, remainder, None) except UnicodeEncodeError: return None return controller @expose() def _route(self, args, request=None): ''' Routes a request to the appropriate controller and returns its result. Performs a bit of validation - refuses to route delete and put actions via a GET request). ''' if request is None: from pecan import request # convention uses "_method" to handle browser-unsupported methods method = request.params.get('_method', request.method).lower() # make sure DELETE/PUT requests don't use GET if request.method == 'GET' and method in ('delete', 'put'): abort(405) # check for nested controllers result = self._find_sub_controllers(args, request) if result: return result # handle the request handler = getattr( self, '_handle_%s' % method, self._handle_unknown_method ) try: if len(getargspec(handler).args) == 3: result = handler(method, args) else: result = handler(method, args, request) # # If the signature of the handler does not match the number # of remaining positional arguments, attempt to handle # a _lookup method (if it exists) # argspec = self._get_args_for_controller(result[0]) num_args = len(argspec) if num_args < len(args): _lookup_result = self._handle_lookup(args, request) if _lookup_result: return _lookup_result except (exc.HTTPClientError, exc.HTTPNotFound, exc.HTTPMethodNotAllowed) as e: # # If the matching handler results in a 400, 404, or 405, attempt to # handle a _lookup method (if it exists) # _lookup_result = self._handle_lookup(args, request) if _lookup_result: return _lookup_result # Build a correct Allow: header if isinstance(e, exc.HTTPMethodNotAllowed): def method_iter(): for func in ('get', 'get_one', 'get_all', 'new', 'edit', 'get_delete'): if self._find_controller(func): yield 'GET' break for method in ('HEAD', 'POST', 'PUT', 'DELETE', 'TRACE', 'PATCH'): func = method.lower() if self._find_controller(func): yield method e.allow = sorted(method_iter()) raise # return the result return result def _handle_lookup(self, args, request=None): if request is None: self._raise_method_deprecation_warning(self.handle_lookup) # filter empty strings from the arg list args = list(filter(bool, args)) # check for lookup controllers lookup = getattr(self, '_lookup', None) if args and iscontroller(lookup): result = handle_lookup_traversal(lookup, args) if result: obj, remainder = result return lookup_controller(obj, remainder, request) def _find_controller(self, *args): ''' Returns the appropriate controller for routing a custom action. ''' for name in args: obj = self._lookup_child(name) if obj and iscontroller(obj): return obj return None def _find_sub_controllers(self, remainder, request): ''' Identifies the correct controller to route to by analyzing the request URI. ''' # need either a get_one or get to parse args method = None for name in ('get_one', 'get'): if hasattr(self, name): method = name break if not method: return # get the args to figure out how much to chop off args = self._get_args_for_controller(getattr(self, method)) fixed_args = len(args) - len( request.pecan.get('routing_args', []) ) var_args = getargspec(getattr(self, method)).varargs # attempt to locate a sub-controller if var_args: for i, item in enumerate(remainder): controller = self._lookup_child(item) if controller and not ismethod(controller): self._set_routing_args(request, remainder[:i]) return lookup_controller(controller, remainder[i + 1:], request) elif fixed_args < len(remainder) and hasattr( self, remainder[fixed_args] ): controller = self._lookup_child(remainder[fixed_args]) if not ismethod(controller): self._set_routing_args(request, remainder[:fixed_args]) return lookup_controller( controller, remainder[fixed_args + 1:], request ) def _handle_unknown_method(self, method, remainder, request=None): ''' Routes undefined actions (like TRACE) to the appropriate controller. ''' if request is None: self._raise_method_deprecation_warning(self._handle_unknown_method) # try finding a post_{custom} or {custom} method first controller = self._find_controller('post_%s' % method, method) if controller: return controller, remainder # if no controller exists, try routing to a sub-controller; note that # since this isn't a safe GET verb, any local exposes are 405'd if remainder: if self._find_controller(remainder[0]): abort(405) sub_controller = self._lookup_child(remainder[0]) if sub_controller: return lookup_controller(sub_controller, remainder[1:], request) abort(405) def _handle_get(self, method, remainder, request=None): ''' Routes ``GET`` actions to the appropriate controller. ''' if request is None: self._raise_method_deprecation_warning(self._handle_get) # route to a get_all or get if no additional parts are available if not remainder or remainder == ['']: remainder = list(filter(bool, remainder)) controller = self._find_controller('get_all', 'get') if controller: self._handle_bad_rest_arguments(controller, remainder, request) return controller, [] abort(405) method_name = remainder[-1] # check for new/edit/delete GET requests if method_name in ('new', 'edit', 'delete'): if method_name == 'delete': method_name = 'get_delete' controller = self._find_controller(method_name) if controller: return controller, remainder[:-1] match = self._handle_custom_action(method, remainder, request) if match: return match controller = self._lookup_child(remainder[0]) if controller and not ismethod(controller): return lookup_controller(controller, remainder[1:], request) # finally, check for the regular get_one/get requests controller = self._find_controller('get_one', 'get') if controller: self._handle_bad_rest_arguments(controller, remainder, request) return controller, remainder abort(405) def _handle_delete(self, method, remainder, request=None): ''' Routes ``DELETE`` actions to the appropriate controller. ''' if request is None: self._raise_method_deprecation_warning(self._handle_delete) if remainder: match = self._handle_custom_action(method, remainder, request) if match: return match controller = self._lookup_child(remainder[0]) if controller and not ismethod(controller): return lookup_controller(controller, remainder[1:], request) # check for post_delete/delete requests first controller = self._find_controller('post_delete', 'delete') if controller: return controller, remainder # if no controller exists, try routing to a sub-controller; note that # since this is a DELETE verb, any local exposes are 405'd if remainder: if self._find_controller(remainder[0]): abort(405) sub_controller = self._lookup_child(remainder[0]) if sub_controller: return lookup_controller(sub_controller, remainder[1:], request) abort(405) def _handle_post(self, method, remainder, request=None): ''' Routes ``POST`` requests. ''' if request is None: self._raise_method_deprecation_warning(self._handle_post) # check for custom POST/PUT requests if remainder: match = self._handle_custom_action(method, remainder, request) if match: return match controller = self._lookup_child(remainder[0]) if controller and not ismethod(controller): return lookup_controller(controller, remainder[1:], request) # check for regular POST/PUT requests controller = self._find_controller(method) if controller: return controller, remainder abort(405) def _handle_put(self, method, remainder, request=None): return self._handle_post(method, remainder, request) def _handle_custom_action(self, method, remainder, request=None): if request is None: self._raise_method_deprecation_warning(self._handle_custom_action) remainder = [r for r in remainder if r] if remainder: if method in ('put', 'delete'): # For PUT and DELETE, additional arguments are supplied, e.g., # DELETE /foo/XYZ method_name = remainder[0] remainder = remainder[1:] else: method_name = remainder[-1] remainder = remainder[:-1] if method.upper() in self._custom_actions.get(method_name, []): controller = self._find_controller( '%s_%s' % (method, method_name), method_name ) if controller: return controller, remainder def _set_routing_args(self, request, args): ''' Sets default routing arguments. ''' request.pecan.setdefault('routing_args', []).extend(args) def _raise_method_deprecation_warning(self, handler): warnings.warn( ( "The function signature for %s.%s.%s is changing " "in the next version of pecan.\nPlease update to: " "`%s(self, method, remainder, request)`." % ( self.__class__.__module__, self.__class__.__name__, handler.__name__, handler.__name__ ) ), DeprecationWarning ) pecan-1.5.1/pecan/routing.py000066400000000000000000000265151445453044500157540ustar00rootroot00000000000000import logging import re import warnings from inspect import getmembers, ismethod from webob import exc from .secure import handle_security, cross_boundary from .util import iscontroller, getargspec, _cfg __all__ = ['lookup_controller', 'find_object', 'route'] __observed_controllers__ = set() __custom_routes__ = {} logger = logging.getLogger(__name__) def route(*args): """ This function is used to define an explicit route for a path segment. You generally only want to use this in situations where your desired path segment is not a valid Python variable/function name. For example, if you wanted to be able to route to: /path/with-dashes/ ...the following is invalid Python syntax:: class Controller(object): with-dashes = SubController() ...so you would instead define the route explicitly:: class Controller(object): pass pecan.route(Controller, 'with-dashes', SubController()) """ def _validate_route(route): if not isinstance(route, str): raise TypeError('%s must be a string' % route) if route in ('.', '..') or not re.match( '^[0-9a-zA-Z-_$\(\)\.~!,;:*+@=]+$', route ): raise ValueError( '%s must be a valid path segment. Keep in mind ' 'that path segments should not contain path separators ' '(e.g., /) ' % route ) if len(args) == 2: # The handler in this situation is a @pecan.expose'd callable, # and is generally only used by the @expose() decorator itself. # # This sets a special attribute, `custom_route` on the callable, which # pecan's routing logic knows how to make use of (as a special case) route, handler = args if ismethod(handler): handler = handler.__func__ if not iscontroller(handler): raise TypeError( '%s must be a callable decorated with @pecan.expose' % handler ) obj, attr, value = handler, 'custom_route', route if handler.__name__ in ('_lookup', '_default', '_route'): raise ValueError( '%s is a special method in pecan and cannot be used in ' 'combination with custom path segments.' % handler.__name__ ) elif len(args) == 3: # This is really just a setattr on the parent controller (with some # additional validation for the path segment itself) _, route, handler = args obj, attr, value = args if hasattr(obj, attr): raise RuntimeError( ( "%(module)s.%(class)s already has an " "existing attribute named \"%(route)s\"." % { 'module': obj.__module__, 'class': obj.__name__, 'route': attr } ), ) else: raise TypeError( 'pecan.route should be called in the format ' 'route(ParentController, "path-segment", SubController())' ) _validate_route(route) setattr(obj, attr, value) class PecanNotFound(Exception): pass class NonCanonicalPath(Exception): ''' Exception Raised when a non-canonical path is encountered when 'walking' the URI. This is typically a ``POST`` request which requires a trailing slash. ''' def __init__(self, controller, remainder): self.controller = controller self.remainder = remainder def lookup_controller(obj, remainder, request=None): ''' Traverses the requested url path and returns the appropriate controller object, including default routes. Handles common errors gracefully. ''' if request is None: warnings.warn( ( "The function signature for %s.lookup_controller is changing " "in the next version of pecan.\nPlease update to: " "`lookup_controller(self, obj, remainder, request)`." % ( __name__, ) ), DeprecationWarning ) notfound_handlers = [] while True: try: obj, remainder = find_object(obj, remainder, notfound_handlers, request) handle_security(obj) return obj, remainder except (exc.HTTPNotFound, exc.HTTPMethodNotAllowed, PecanNotFound) as e: if isinstance(e, PecanNotFound): e = exc.HTTPNotFound() while notfound_handlers: name, obj, remainder = notfound_handlers.pop() if name == '_default': # Notfound handler is, in fact, a controller, so stop # traversal return obj, remainder else: # Notfound handler is an internal redirect, so continue # traversal result = handle_lookup_traversal(obj, remainder) if result: # If no arguments are passed to the _lookup, yet the # argspec requires at least one, raise a 404 if ( remainder == [''] and len(obj._pecan['argspec'].args) > 1 ): raise e obj_, remainder_ = result return lookup_controller(obj_, remainder_, request) else: raise e def handle_lookup_traversal(obj, args): try: result = obj(*args) except TypeError as te: logger.debug('Got exception calling lookup(): %s (%s)', te, te.args) else: if result: prev_obj = obj obj, remainder = result # crossing controller boundary cross_boundary(prev_obj, obj) return result def find_object(obj, remainder, notfound_handlers, request): ''' 'Walks' the url path in search of an action for which a controller is implemented and returns that controller object along with what's left of the remainder. ''' prev_obj = None while True: if obj is None: raise PecanNotFound if iscontroller(obj): if getattr(obj, 'custom_route', None) is None: return obj, remainder _detect_custom_path_segments(obj) if remainder: custom_route = __custom_routes__.get((obj.__class__, remainder[0])) if custom_route: return getattr(obj, custom_route), remainder[1:] # are we traversing to another controller cross_boundary(prev_obj, obj) try: next_obj, rest = remainder[0], remainder[1:] if next_obj == '': index = getattr(obj, 'index', None) if iscontroller(index): return index, rest except IndexError: # the URL has hit an index method without a trailing slash index = getattr(obj, 'index', None) if iscontroller(index): raise NonCanonicalPath(index, []) default = getattr(obj, '_default', None) if iscontroller(default): notfound_handlers.append(('_default', default, remainder)) lookup = getattr(obj, '_lookup', None) if iscontroller(lookup): notfound_handlers.append(('_lookup', lookup, remainder)) route = getattr(obj, '_route', None) if iscontroller(route): if len(getargspec(route).args) == 2: warnings.warn( ( "The function signature for %s.%s._route is changing " "in the next version of pecan.\nPlease update to: " "`def _route(self, args, request)`." % ( obj.__class__.__module__, obj.__class__.__name__ ) ), DeprecationWarning ) next_obj, next_remainder = route(remainder) else: next_obj, next_remainder = route(remainder, request) cross_boundary(route, next_obj) return next_obj, next_remainder if not remainder: raise PecanNotFound prev_remainder = remainder prev_obj = obj remainder = rest try: obj = getattr(obj, next_obj, None) except UnicodeEncodeError: obj = None # Last-ditch effort: if there's not a matching subcontroller, no # `_default`, no `_lookup`, and no `_route`, look to see if there's # an `index` that has a generic method defined for the current request # method. if not obj and not notfound_handlers and hasattr(prev_obj, 'index'): if request.method in _cfg(prev_obj.index).get('generic_handlers', {}): return prev_obj.index, prev_remainder def _detect_custom_path_segments(obj): # Detect custom controller routes (on the initial traversal) if obj.__class__.__module__ == '__builtin__': return attrs = set(dir(obj)) if obj.__class__ not in __observed_controllers__: for key, val in getmembers(obj): if iscontroller(val) and isinstance( getattr(val, 'custom_route', None), str, ): route = val.custom_route # Detect class attribute name conflicts for conflict in attrs.intersection(set((route,))): raise RuntimeError( ( "%(module)s.%(class)s.%(function)s has " "a custom path segment, \"%(route)s\", " "but %(module)s.%(class)s already has an " "existing attribute named \"%(route)s\"." % { 'module': obj.__class__.__module__, 'class': obj.__class__.__name__, 'function': val.__name__, 'route': conflict } ), ) existing = __custom_routes__.get( (obj.__class__, route) ) if existing: # Detect custom path conflicts between functions raise RuntimeError( ( "%(module)s.%(class)s.%(function)s and " "%(module)s.%(class)s.%(other)s have a " "conflicting custom path segment, " "\"%(route)s\"." % { 'module': obj.__class__.__module__, 'class': obj.__class__.__name__, 'function': val.__name__, 'other': existing, 'route': route } ), ) __custom_routes__[ (obj.__class__, route) ] = key __observed_controllers__.add(obj.__class__) pecan-1.5.1/pecan/scaffolds/000077500000000000000000000000001445453044500156465ustar00rootroot00000000000000pecan-1.5.1/pecan/scaffolds/__init__.py000066400000000000000000000076161445453044500177710ustar00rootroot00000000000000import sys import os import re import pkg_resources from string import Template DEFAULT_SCAFFOLD = 'base' _bad_chars_re = re.compile('[^a-zA-Z0-9_]') class PecanScaffold(object): """ A base Pecan scaffold. New scaffolded implementations should extend this class and define a ``_scaffold_dir`` attribute, e.g., class CoolAddOnScaffold(PecanScaffold): _scaffold_dir = ('package', os.path.join('scaffolds', 'scaffold_name')) ...where... pkg_resources.resource_listdir(_scaffold_dir[0], _scaffold_dir[1])) ...points to some scaffold directory root. """ def normalize_output_dir(self, dest): return os.path.abspath(os.path.normpath(dest)) def normalize_pkg_name(self, dest): return _bad_chars_re.sub('', dest.lower()) def copy_to(self, dest, **kw): output_dir = self.normalize_output_dir(dest) pkg_name = self.normalize_pkg_name(dest) copy_dir(self._scaffold_dir, output_dir, {'package': pkg_name}, **kw) class BaseScaffold(PecanScaffold): _scaffold_dir = ('pecan', os.path.join('scaffolds', 'base')) class RestAPIScaffold(PecanScaffold): _scaffold_dir = ('pecan', os.path.join('scaffolds', 'rest-api')) def copy_dir(source, dest, variables, out_=sys.stdout, i=0): """ Copies the ``source`` directory to the ``dest`` directory, where ``source`` is some tuple representing an installed package and a subdirectory in the package, e.g., ('pecan', os.path.join('scaffolds', 'base')) ('pecan_extension', os.path.join('scaffolds', 'scaffold_name')) ``variables``: A dictionary of variables to use in any substitutions. Substitution is performed via ``string.Template``. ``out_``: File object to write to (default is sys.stdout). """ def out(msg): out_.write('%s%s' % (' ' * (i * 2), msg)) out_.write('\n') out_.flush() names = sorted(pkg_resources.resource_listdir(source[0], source[1])) if not os.path.exists(dest): out('Creating %s' % dest) makedirs(dest) else: out('%s already exists' % dest) return for name in names: full = '/'.join([source[1], name]) dest_full = os.path.join(dest, substitute_filename(name, variables)) sub_file = False if dest_full.endswith('_tmpl'): dest_full = dest_full[:-5] sub_file = True if pkg_resources.resource_isdir(source[0], full): out('Recursing into %s' % os.path.basename(full)) copy_dir((source[0], full), dest_full, variables, out_, i + 1) continue else: content = pkg_resources.resource_string(source[0], full) if sub_file: content = render_template(content, variables) if content is None: continue # pragma: no cover out('Copying %s to %s' % (full, dest_full)) f = open(dest_full, 'wb') f.write(content) f.close() def makedirs(directory): """ Resursively create a named directory. """ parent = os.path.dirname(os.path.abspath(directory)) if not os.path.exists(parent): makedirs(parent) os.mkdir(directory) def substitute_filename(fn, variables): """ Substitute +variables+ in file directory names. """ for var, value in variables.items(): fn = fn.replace('+%s+' % var, str(value)) return fn def render_template(content, variables): """ Return a bytestring representing a templated file based on the input (content) and the variable names defined (vars). """ fsenc = sys.getfilesystemencoding() def to_native(s, encoding='latin-1', errors='strict'): if isinstance(s, str): return s return str(s, encoding, errors) output = Template( to_native(content, fsenc) ).substitute(variables) if isinstance(output, str): output = output.encode(fsenc, 'strict') return output pecan-1.5.1/pecan/scaffolds/base/000077500000000000000000000000001445453044500165605ustar00rootroot00000000000000pecan-1.5.1/pecan/scaffolds/base/+package+/000077500000000000000000000000001445453044500203015ustar00rootroot00000000000000pecan-1.5.1/pecan/scaffolds/base/+package+/__init__.py000066400000000000000000000000001445453044500224000ustar00rootroot00000000000000pecan-1.5.1/pecan/scaffolds/base/+package+/app.py_tmpl000066400000000000000000000004061445453044500224670ustar00rootroot00000000000000from pecan import make_app from ${package} import model def setup_app(config): model.init_model() app_conf = dict(config.app) return make_app( app_conf.pop('root'), logging=getattr(config, 'logging', {}), **app_conf ) pecan-1.5.1/pecan/scaffolds/base/+package+/controllers/000077500000000000000000000000001445453044500226475ustar00rootroot00000000000000pecan-1.5.1/pecan/scaffolds/base/+package+/controllers/__init__.py000066400000000000000000000000001445453044500247460ustar00rootroot00000000000000pecan-1.5.1/pecan/scaffolds/base/+package+/controllers/root.py000066400000000000000000000011651445453044500242070ustar00rootroot00000000000000from pecan import expose, redirect from webob.exc import status_map class RootController(object): @expose(generic=True, template='index.html') def index(self): return dict() @index.when(method='POST') def index_post(self, q): redirect('https://pecan.readthedocs.io/en/latest/search.html?q=%s' % q) @expose('error.html') def error(self, status): try: status = int(status) except ValueError: # pragma: no cover status = 500 message = getattr(status_map.get(status), 'explanation', '') return dict(status=status, message=message) pecan-1.5.1/pecan/scaffolds/base/+package+/model/000077500000000000000000000000001445453044500214015ustar00rootroot00000000000000pecan-1.5.1/pecan/scaffolds/base/+package+/model/__init__.py000066400000000000000000000007171445453044500235170ustar00rootroot00000000000000from pecan import conf # noqa def init_model(): """ This is a stub method which is called at application startup time. If you need to bind to a parsed database configuration, set up tables or ORM classes, or perform any database initialization, this is the recommended place to do it. For more information working with databases, and some common recipes, see https://pecan.readthedocs.io/en/latest/databases.html """ pass pecan-1.5.1/pecan/scaffolds/base/+package+/templates/000077500000000000000000000000001445453044500222775ustar00rootroot00000000000000pecan-1.5.1/pecan/scaffolds/base/+package+/templates/error.html000066400000000000000000000004211445453044500243130ustar00rootroot00000000000000<%inherit file="layout.html" /> ## provide definitions for blocks we want to redefine <%def name="title()"> Server Error ${status} ## now define the body of the template

Server Error ${status}

${message}

pecan-1.5.1/pecan/scaffolds/base/+package+/templates/index.html000066400000000000000000000015411445453044500242750ustar00rootroot00000000000000<%inherit file="layout.html" /> ## provide definitions for blocks we want to redefine <%def name="title()"> Welcome to Pecan! ## now define the body of the template

This is a sample Pecan project.

Instructions for getting started can be found online at pecanpy.org

...or you can search the documentation here:

Enter search terms or a module, class or function name.
pecan-1.5.1/pecan/scaffolds/base/+package+/templates/layout.html000066400000000000000000000007001445453044500244770ustar00rootroot00000000000000 ${self.title()} ${self.style()} ${self.javascript()} ${self.body()} <%def name="title()"> Default Title <%def name="style()"> <%def name="javascript()"> pecan-1.5.1/pecan/scaffolds/base/+package+/tests/000077500000000000000000000000001445453044500214435ustar00rootroot00000000000000pecan-1.5.1/pecan/scaffolds/base/+package+/tests/__init__.py_tmpl000066400000000000000000000010061445453044500246050ustar00rootroot00000000000000import os from unittest import TestCase from pecan import set_config from pecan.testing import load_test_app __all__ = ['FunctionalTest'] class FunctionalTest(TestCase): """ Used for functional tests where you need to test your literal application and its integration with the framework. """ def setUp(self): self.app = load_test_app(os.path.join( os.path.dirname(__file__), 'config.py' )) def tearDown(self): set_config({}, overwrite=True) pecan-1.5.1/pecan/scaffolds/base/+package+/tests/config.py_tmpl000066400000000000000000000010631445453044500243160ustar00rootroot00000000000000# Server Specific Configurations server = { 'port': '8080', 'host': '0.0.0.0' } # Pecan Application Configurations app = { 'root': '${package}.controllers.root.RootController', 'modules': ['${package}'], 'static_root': '%(confdir)s/../../public', 'template_path': '%(confdir)s/../templates', 'debug': True, 'errors': { '404': '/error/404', '__force_dict__': True } } # Custom Configurations must be in Python dictionary format:: # # foo = {'bar':'baz'} # # All configurations are accessible at:: # pecan.conf pecan-1.5.1/pecan/scaffolds/base/+package+/tests/test_functional.py_tmpl000066400000000000000000000012631445453044500262540ustar00rootroot00000000000000from unittest import TestCase from webtest import TestApp from ${package}.tests import FunctionalTest class TestRootController(FunctionalTest): def test_get(self): response = self.app.get('/') assert response.status_int == 200 def test_search(self): response = self.app.post('/', params={'q': 'RestController'}) assert response.status_int == 302 assert response.headers['Location'] == ( 'https://pecan.readthedocs.io/en/latest/search.html' '?q=RestController' ) def test_get_not_found(self): response = self.app.get('/a/bogus/url', expect_errors=True) assert response.status_int == 404 pecan-1.5.1/pecan/scaffolds/base/+package+/tests/test_units.py000066400000000000000000000001611445453044500242140ustar00rootroot00000000000000from unittest import TestCase class TestUnits(TestCase): def test_units(self): assert 5 * 5 == 25 pecan-1.5.1/pecan/scaffolds/base/MANIFEST.in000066400000000000000000000000331445453044500203120ustar00rootroot00000000000000recursive-include public * pecan-1.5.1/pecan/scaffolds/base/config.py_tmpl000066400000000000000000000027221445453044500214360ustar00rootroot00000000000000# Server Specific Configurations server = { 'port': '8080', 'host': '0.0.0.0' } # Pecan Application Configurations app = { 'root': '${package}.controllers.root.RootController', 'modules': ['${package}'], 'static_root': '%(confdir)s/public', 'template_path': '%(confdir)s/${package}/templates', 'debug': True, 'errors': { 404: '/error/404', '__force_dict__': True } } logging = { 'root': {'level': 'INFO', 'handlers': ['console']}, 'loggers': { '${package}': {'level': 'DEBUG', 'handlers': ['console'], 'propagate': False}, 'pecan': {'level': 'DEBUG', 'handlers': ['console'], 'propagate': False}, 'py.warnings': {'handlers': ['console']}, '__force_dict__': True }, 'handlers': { 'console': { 'level': 'DEBUG', 'class': 'logging.StreamHandler', 'formatter': 'color' } }, 'formatters': { 'simple': { 'format': ('%(asctime)s %(levelname)-5.5s [%(name)s]' '[%(threadName)s] %(message)s') }, 'color': { '()': 'pecan.log.ColorFormatter', 'format': ('%(asctime)s [%(padded_color_levelname)s] [%(name)s]' '[%(threadName)s] %(message)s'), '__force_dict__': True } } } # Custom Configurations must be in Python dictionary format:: # # foo = {'bar':'baz'} # # All configurations are accessible at:: # pecan.conf pecan-1.5.1/pecan/scaffolds/base/public/000077500000000000000000000000001445453044500200365ustar00rootroot00000000000000pecan-1.5.1/pecan/scaffolds/base/public/css/000077500000000000000000000000001445453044500206265ustar00rootroot00000000000000pecan-1.5.1/pecan/scaffolds/base/public/css/style.css000066400000000000000000000010711445453044500224770ustar00rootroot00000000000000body { background: #311F00; color: white; font-family: 'Helvetica Neue', 'Helvetica', 'Verdana', sans-serif; padding: 1em 2em; } a { color: #FAFF78; text-decoration: none; } a:hover { text-decoration: underline; } div#content { width: 800px; margin: 0 auto; } form { margin: 0; padding: 0; border: 0; } fieldset { border: 0; } input.error { background: #FAFF78; } header { text-align: center; } h1, h2, h3, h4, h5, h6 { font-family: 'Futura-CondensedExtraBold', 'Futura', 'Helvetica', sans-serif; text-transform: uppercase; } pecan-1.5.1/pecan/scaffolds/base/public/images/000077500000000000000000000000001445453044500213035ustar00rootroot00000000000000pecan-1.5.1/pecan/scaffolds/base/public/images/logo.png000066400000000000000000000501641445453044500227570ustar00rootroot00000000000000PNG  IHDR^D AiCCPICC ProfileH wTSϽ7" %z ;HQIP&vDF)VdTG"cE b PQDE݌k 5ޚYg}׺PtX4X\XffGD=HƳ.d,P&s"7C$ E6<~&S2)212 "įl+ɘ&Y4Pޚ%ᣌ\%g|eTI(L0_&l2E9r9hxgIbטifSb1+MxL 0oE%YmhYh~S=zU&ϞAYl/$ZUm@O ޜl^ ' lsk.+7oʿ9V;?#I3eE妧KD d9i,UQ h A1vjpԁzN6p\W p G@ K0ށiABZyCAP8C@&*CP=#t] 4}a ٰ;GDxJ>,_“@FXDBX$!k"EHqaYbVabJ0՘cVL6f3bձX'?v 6-V``[a;p~\2n5׌ &x*sb|! ߏƿ' Zk! $l$T4QOt"y\b)AI&NI$R$)TIj"]&=&!:dGrY@^O$ _%?P(&OJEBN9J@y@yCR nXZOD}J}/G3ɭk{%Oחw_.'_!JQ@SVF=IEbbbb5Q%O@%!BӥyҸM:e0G7ӓ e%e[(R0`3R46i^)*n*|"fLUo՝mO0j&jajj.ϧwϝ_4갺zj=U45nɚ4ǴhZ ZZ^0Tf%9->ݫ=cXgN].[7A\SwBOK/X/_Q>QG[ `Aaac#*Z;8cq>[&IIMST`ϴ kh&45ǢYYF֠9<|y+ =X_,,S-,Y)YXmĚk]c}džjcΦ浭-v};]N"&1=xtv(}'{'IߝY) Σ -rqr.d._xpUەZM׍vm=+KGǔ ^WWbj>:>>>v}/avO8 FV> 2 u/_$\BCv< 5 ]s.,4&yUx~xw-bEDCĻHGKwFGEGME{EEKX,YFZ ={$vrK .3\rϮ_Yq*©L_wד+]eD]cIIIOAu_䩔)3ѩiB%a+]3='/40CiU@ёL(sYfLH$%Y jgGeQn~5f5wugv5k֮\۹Nw]m mHFˍenQQ`hBBQ-[lllfjۗ"^bO%ܒY}WwvwXbY^Ю]WVa[q`id2JjGէ{׿m>PkAma꺿g_DHGGu;776ƱqoC{P38!9 ҝˁ^r۽Ug9];}}_~imp㭎}]/}.{^=}^?z8hc' O*?f`ϳgC/Oϩ+FFGGόzˌㅿ)ѫ~wgbk?Jި9mdwi獵ޫ?cǑOO?w| x&mf2:Y~ pHYs  @IDATx E,@B %1aEvAy 72>.ฎt8# >Dy(KL@@ ,IHU4}}'7|Vwuթs~uu9#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#84w^{m+1bD… 73fפU 8H4Aq3,$\~(vK"dJ׿ۂ^mb{ٖ,, GUr4ghH\aҤI=[noi?|T#SO'y<ISG]PL2uE2flzL=I nU{EmԮN;mrۇ޴CUzhi\7c4ՠYx. ڵ;www/27o޼vƍ+QecƌYk•1ZFz<+Cvy] B.bŊ^} ,xލzEip j'N8ਣڥg P;tCQ%#<#J=Mީ(ڽk֬{ذaKEhŕ)HdXz ? Z )B3?F 6MÛ4j1"o+t"i\6ve]ǛBuȣrБ:>/Fe} ?p±s-%h*mr!t=0ٌp5/l.EcvaK(O:"i 5zI|IeRsYvؓwO# >Ԟ%T»-Ztf*OYԀM9|֬95(ѴDzx_bOj""(ekeX Q ϒ ˥eZX$xUH࿎"bD,Pc׫WҲL$Hp'|(g2|rǚM([zQk EiJ)@D#ANʖ4,[o MΪ6nٔ  +8jM]m€/B>,-|~H\nBh[-m.ۢ(s U|63B͆Q`u0uNϺ/4eІO&ΌV#ʗ v:]k*P\:p.T2O6~ΰ`gMp=HJr>ݭ2$u3=+&v :=)}әO-:lǃQ[מXZm 9m!t]x$1^o5%SC5+1޷SB)wG*)< >Bm=[*]4DQYl:j/-lt'JS%{֭)`4aSeJ2XUN+-m:q}+/ҏMEdQ9w=:FM\V+uʅ]0:*iG(p֨~BYce>7#/RF7pZ HFXʘ/`A8Z#4gᡏdPHϦ !:IF RS:} #%vN\GA0?.,ye e<gشڈrSYJɚϼ鶰~Q^/XqtF(֎2/ҊO8S:(VTVUQAٖ~[\2lF?^S[UcxE4o 4a5녤- KthI{ZcbxyAރȵ>0xp)>3eF QrY]e̺tcifRlV3@4m]7r-ttؔNZд_~ygadq指vF45ȧ̣1e ѮKCMP&9s11G# :k MEjlQ𹂩>>| >ܗo,ҝg:@#[B zVS i>7|xX* ?4Dɠ.jWL )`l(תk9w] ;:{yC'FƢXn7wQ\?1p ;|sM w #%EO puKhotc"9Ea-˕ީlMM#m8ޠoϓ{ŔFmh@xцWB鲗M*~YiHNypR< 49]0(N~>˻@7Ȝda墘_ 7A隆cDlII6> 7+׹y[23 ^QfL҇v_LtHG_&= C_Hk q`6'z0i ёd.ֽdWU tZV׏F::e#EԻD ? or=1c9a #>8&mT<'ӑ ZK:R)kbr1yZ2`:h=W9PIKߦt4BLS4qI )!b  UB8^"!*P3D! 3R``ej|F?ӔѳzN )mR:;AP&pjlf[*zNc8G`Na]Za'3H3+A䱰Km)#dU# |︾ #{s.#Tg4/Ct#đ /![tK֊^j6$%lotTH׿#-Luϣ_ Bf5 Qu{<#:評[Ncgk042r? ~τ#uiϑ`DzjKAgA;JAѡ\7ٔ?U~:joћ͛7i%V }n!&C2Cti{p׏th %WrqiU)E+SyEв ]/h"WI N|Ս_az2hr? I[TGs~S50!۠**NC$-1D<гQO#1 \7k͒ \QJ"ű71dO?_2׊lDBxYtEmuկ~)ӳX,ˏ@<Ǒה=>-]cV-S0Mz |SV~Ao֍c)8>^S7|vm7 g:k@Cd :.'س<ZC?^%!UjIid ʃ$~z2XHJ N׶'hK~JCC(+S&t~3 Pc]j*y5})=~>XQ/Ӭ+o&w F70Z^ϖԚ nBUt!9 [NlRpV]uhdkT{T OFجUN`zFyC:;#"aWU%y()z݁01bKP[a" 9k/Rg0 όDZK; ¹܆FnJǥwvzycb"e5z>G4fYhgQ)<{,y#O)B|FӅJzy5q<J95ixUYLU:3؉iKx?za@ #?אvX%3 ^41a-Yղ\lDK5zuC|Qj;$g.TwkXnֈF@qmu(KyzK|nz%럫Ye(_';1-JRڜ>Gߒj&kiKaTexe1qW˨ѳ0|/(%>TUG!a0&mo#GjdWyW$5tZ׆I"qp H9[w.~ӐHfhK;1|~e~4#=)_#]8,5m^ffuxU ֢CM"ځ Va53SZ#^k+*X+C?I镨wD2"DY"6sc{cCIyJHsH2Ʌ^qw$z@k' '|r!E ml7!wc.䒝Jof<B8C; v^gh^ lzELq4̈́zN/+0 9A)ҺVB@A:IF͎^&jA[]_r "jTPsڊP 6e`yyZ| _OMaG0aرz += d/Kȫ޺\Ôj 咗 Qu&y| Pֱe=ĩc{[`(o"3 kZZa&m]}h^^ɉ?n Q`Dϟt̘1`f!;(aÆiltzMchF [;pk[&~.6qSF6mLu ljS:U*J%h=}t}.w40ZpKEooq(ae0,G)ycxt ۹ز֭̚= hOj]ղ@l#<@v3qQ30eGydYY"q:H+Ls(`F@ olneXuz -ëE]/;!v"ز2ϗ#]V^F)ځfl>w L; ȫx6-C* ;SmFd<2D^Z*\J\6c1$긭.IHKCZQ oFŋBm #\٬#"Z8qzqV" ^F_28խi!KRũi I"N 1j|8^ns!,BGdN0ӵpа2 HQçpBtN ڞ$4MF%'# 9a==^]J'NnAP축<6Zi?Coqw@;ףuGij gȩ䲩h/! ^/Q#0°=ƫ)>FpВ -Sg>gr gfRB_')t={[|mJc2Z!Jo q YpѢBQaMSZkn2ֈ"\Y7WR672Յr"ڦV=iepϯ鄋:7@q/Xj? {y3&J-+I^s=G<3Y*QzNWZ0 > p~0< Aԑ%Zj-+BK4նvsf|_QVQǥ:øk׈=ZbNQvGnE/\<?3K.~yzÓB #U`,o''Vr` <ՏW/ GsU<"ˀw-7Q>kX'0B<%/FL#יrTPNP{>OSv:1؟AȮNr^hQB.?bA#_(sP#ij`.!CqDhi'26*+>7F*No ҉J`hdl-Y}`g ˁa*1E:f\ZH6NΜ9s' ?;xyFl!kQV҉uE G]o# ~wBPJԻkh\ISf`P^3fxBҙ4<2vmFA.C&_V82x.,hҷ:Nxct('@GRׅuf4bJ^@KKRoz_Ӎ2䉝Ox0re,.~ ya"MQLT%nmԳX*e/?ۙo1K[ۣGި̦iv<XjĠѿAȣС, T=M6ա=c[[W  +V8WQ,nʥǔ/T ynM :tGWe|D7Y> $^ſAp& }V$oy YW{vz_ X9zW$Fid[OO FԸԨщ~ofU>-F3KBFШןmxv uF i^+ ʽIcUh焑!s26|/Eh)h+hWݘ$dǍ!\K=*_[n/Yw`FB,42 l@i: KtԏtK)Lч,t!BJ >--]e zMGhVǔKhP_8NQaLwWjʲe]GeUTp4 Y>c(e/"Z<$i*R˨׌zֻ$je#o㫯't E0 ` y\B;B0Wyk^OGSuFx!\I^sZz3cHt4DEw2>|cƌY[3cn`yjyL͌tC)9HwdM`L/l_[+UR:)TJ\mHᦫK;zC0Bm#;bWV|"ry<_zka^43}d Y+×a>k n`u/#auBNS܀a#;^^"j&xs9vF5JiDQ/<_`ԴPGtCpG'!q`3/_'p8n_:&l3Ti].'9rwc-\ҭ&>ODI@^ &*K4oҧivr-FL`6sidt0F#qH7n/ #/B[VGygXU79WMo d:+C:Tgi֬Y$p/1M.Gq8=t CZ=NIjvou_e(Yqcp 4\=}ue,>p8c'CUc뀗ݧC==yϔrOa(/+:tz@_=nK=BWGvN&ߍGk7K?G+ o PݚXT=- Nhh:ӔVq^*t <*:)VZ4!1ʙ iLTܺ7#FhOYt w]]fnREy}G?w zZRnЭ2+ɨCPiIV筺PHCy+fR/Tj4b1w^GQZO.<='5g_AOd%FCZXD%k%4dOB<zaeNjB=ϔwɏrZ/,CFp=#wr  L^@K'ˋ_ಜk}\CNa;铎*Sp&Fz.IxPhF_rD3B;˟RϷ~3byIY|Oҿ$o[ _@r֨LիIeB2N|I|ZT\/jF`†^i{G|C#Ƨ?L/,#Hi0J-EK}u\X&6wZ92fy M b_a+#A奓N򈖼C;K]KZQv`V¿Bx!}CH+=jtOε%JQh_ EW4ݚϑ:l#\n"fnE9R X9#ӵ/ ¤)ee rSL p-ŗҫ,>nNUi41H&39"cGMv įoVГ3%w׳ j`Tߩ慎)죺OYVs/9H'fICЎSGco0/.ht2e5J2+vO<.]վh>Q>lEj,R^0V'̵Z$՘UbXtJZ 1B!>j-3(/|NP/3yOiK_dlm;_ѩ0XSq#x⭓v10+6/G~ՑLglO![RkV^3P gߖ2pBK afD}wOkdϝg2 сfxʒj FogG7 E͋|fdɠil?E:L] 5n{CS8MԈ}l5gy4%i8?!Eઃ>+2EclG_)3{'kR7^e7xjxP \,~ k= 8tVAekʖ <2mbxmf鯞,[YgƬvG&5Pl^Q |j-uЖ ]_d_getO_ty,= ~~X,(r*>ۑIgkQlr :q_or-"aFa,pݤaK&-ƛBSv6ԕ%,9'h)Г3<ҝF)#~}\Xؗf]|tXߡ£llWxj'T^=&2p\K /@/C#8e(\^`JLfG4B9n2|OI"*x's 3M#uq(E%1.1:d%b):+NG_ǐ#9mlJгZ.74ˤ }nmTf%pekdSXS y丗:}Xez%S@M_Xa乮lK֬u(VWZw k}l Χzz^XCց2RNZ 0_zYz'QVLW atzO'z{~~9dfLmiՖt0q_rpᇟ -fd8=KkI ~pl)mjw#,|F!>s#O_`o?agYi愃am N[ᄅ;{zp*7̑Fw"\o 78ficV^}uPaԵHLiM>' Pjxm 'aEyz?Q^!)9^r!NJ_@L/b^KREzu!ckBb朄(i0t~8\p A㬳d$<WX.j6шC#0IA1ZsG[\ >5dF!ϐ^y[Κ\aG uVp,E~m䋤+LQivpW\myCoOgYP-2tcg bHUj2edGb A;ArGt O0LuȄt63I#.[X*fqj;EO qjQJexhd"_hBbXT4 F8bїⰚ^'`1(tCxBG9WOgu;\a.hlEy[62V޼eZX^.EƓAx/5$5"L72\J/"EH 78@~ʈ7WI=K: ndS#t&ۊBόm3sGc4+38@!juG oW 8N1p:7_.#tnx;Ɯ_Gxv|#is~GpU8@!juG oW 8N1p:7_.#tnx;Ɯ_Gxv|#is~GpU8@!juG oW 8N1p:7_.#tnx;Ɯ_Gxv|#is~GpU8@!juG oW 8N1p:7_.#tnx;Ɯ_Gxv|#i h|-AM$ݞF #8Gj$P0cү_V18@-ûٴiӳ0+%xOOOC|ի7lo<#4@]Czwƌ䐫<&q|{"GpGpGuGf(MfyhGx#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#8#Z?Q/udIENDB`pecan-1.5.1/pecan/scaffolds/base/setup.cfg_tmpl000066400000000000000000000001341445453044500214330ustar00rootroot00000000000000[nosetests] match=^test where=${package} nocapture=1 cover-package=${package} cover-erase=1 pecan-1.5.1/pecan/scaffolds/base/setup.py_tmpl000066400000000000000000000007521445453044500213320ustar00rootroot00000000000000# -*- coding: utf-8 -*- try: from setuptools import setup, find_packages except ImportError: from ez_setup import use_setuptools use_setuptools() from setuptools import setup, find_packages setup( name='${package}', version='0.1', description='', author='', author_email='', install_requires=[ "pecan", ], test_suite='${package}', zip_safe=False, include_package_data=True, packages=find_packages(exclude=['ez_setup']) ) pecan-1.5.1/pecan/scaffolds/rest-api/000077500000000000000000000000001445453044500173725ustar00rootroot00000000000000pecan-1.5.1/pecan/scaffolds/rest-api/+package+/000077500000000000000000000000001445453044500211135ustar00rootroot00000000000000pecan-1.5.1/pecan/scaffolds/rest-api/+package+/__init__.py000066400000000000000000000000001445453044500232120ustar00rootroot00000000000000pecan-1.5.1/pecan/scaffolds/rest-api/+package+/app.py_tmpl000066400000000000000000000005231445453044500233010ustar00rootroot00000000000000from pecan import make_app from ${package} import model from ${package}.errors import JSONErrorHook def setup_app(config): model.init_model() app_conf = dict(config.app) return make_app( app_conf.pop('root'), logging=getattr(config, 'logging', {}), hooks=[JSONErrorHook()], **app_conf ) pecan-1.5.1/pecan/scaffolds/rest-api/+package+/controllers/000077500000000000000000000000001445453044500234615ustar00rootroot00000000000000pecan-1.5.1/pecan/scaffolds/rest-api/+package+/controllers/__init__.py000066400000000000000000000000001445453044500255600ustar00rootroot00000000000000pecan-1.5.1/pecan/scaffolds/rest-api/+package+/controllers/root.py000066400000000000000000000020751445453044500250220ustar00rootroot00000000000000from pecan import expose, response, abort people = { 1: 'Luke', 2: 'Leia', 3: 'Han', 4: 'Anakin' } class PersonController(object): def __init__(self, person_id): self.person_id = person_id @expose(generic=True) def index(self): return people.get(self.person_id) or abort(404) @index.when(method='PUT') def put(self): # TODO: Idempotent PUT (returns 200 or 204) response.status = 204 @index.when(method='DELETE') def delete(self): # TODO: Idempotent DELETE response.status = 204 class PeopleController(object): @expose() def _lookup(self, person_id, *remainder): return PersonController(int(person_id)), remainder @expose(generic=True, template='json') def index(self): return people @index.when(method='POST', template='json') def post(self): # TODO: Create a new person response.status = 201 class RootController(object): people = PeopleController() @expose() def index(self): return "Hello, World!" pecan-1.5.1/pecan/scaffolds/rest-api/+package+/errors.py000066400000000000000000000007601445453044500230040ustar00rootroot00000000000000import json import webob from pecan.hooks import PecanHook class JSONErrorHook(PecanHook): """ A pecan hook that translates webob HTTP errors into a JSON format. """ def on_error(self, state, exc): if isinstance(exc, webob.exc.HTTPError): return webob.Response( body=json.dumps({'reason': str(exc)}), status=exc.status, headerlist=exc.headerlist, content_type='application/json' ) pecan-1.5.1/pecan/scaffolds/rest-api/+package+/model/000077500000000000000000000000001445453044500222135ustar00rootroot00000000000000pecan-1.5.1/pecan/scaffolds/rest-api/+package+/model/__init__.py000066400000000000000000000007171445453044500243310ustar00rootroot00000000000000from pecan import conf # noqa def init_model(): """ This is a stub method which is called at application startup time. If you need to bind to a parsed database configuration, set up tables or ORM classes, or perform any database initialization, this is the recommended place to do it. For more information working with databases, and some common recipes, see https://pecan.readthedocs.io/en/latest/databases.html """ pass pecan-1.5.1/pecan/scaffolds/rest-api/+package+/tests/000077500000000000000000000000001445453044500222555ustar00rootroot00000000000000pecan-1.5.1/pecan/scaffolds/rest-api/+package+/tests/__init__.py_tmpl000066400000000000000000000010061445453044500254170ustar00rootroot00000000000000import os from unittest import TestCase from pecan import set_config from pecan.testing import load_test_app __all__ = ['FunctionalTest'] class FunctionalTest(TestCase): """ Used for functional tests where you need to test your literal application and its integration with the framework. """ def setUp(self): self.app = load_test_app(os.path.join( os.path.dirname(__file__), 'config.py' )) def tearDown(self): set_config({}, overwrite=True) pecan-1.5.1/pecan/scaffolds/rest-api/+package+/tests/config.py_tmpl000066400000000000000000000006001445453044500251240ustar00rootroot00000000000000# Server Specific Configurations server = { 'port': '8080', 'host': '0.0.0.0' } # Pecan Application Configurations app = { 'root': '${package}.controllers.root.RootController', 'modules': ['${package}'], 'debug': True } # Custom Configurations must be in Python dictionary format:: # # foo = {'bar':'baz'} # # All configurations are accessible at:: # pecan.conf pecan-1.5.1/pecan/scaffolds/rest-api/+package+/tests/test_functional.py_tmpl000066400000000000000000000022261445453044500270660ustar00rootroot00000000000000import json from ${package}.tests import FunctionalTest class TestRootController(FunctionalTest): def test_get_all(self): response = self.app.get('/people/') assert response.status_int == 200 assert response.namespace[1] == 'Luke' assert response.namespace[2] == 'Leia' assert response.namespace[3] == 'Han' assert response.namespace[4] == 'Anakin' def test_get_one(self): response = self.app.get('/people/1/') assert response.status_int == 200 assert response.body.decode() == 'Luke' def test_post(self): response = self.app.post('/people/') assert response.status_int == 201 def test_put(self): response = self.app.put('/people/1/') assert response.status_int == 204 def test_delete(self): response = self.app.delete('/people/1/') assert response.status_int == 204 def test_not_found(self): response = self.app.get('/missing/', expect_errors=True) assert response.status_int == 404 assert json.loads(response.body.decode()) == { 'reason': 'The resource could not be found.' } pecan-1.5.1/pecan/scaffolds/rest-api/+package+/tests/test_units.py000066400000000000000000000001611445453044500250260ustar00rootroot00000000000000from unittest import TestCase class TestUnits(TestCase): def test_units(self): assert 5 * 5 == 25 pecan-1.5.1/pecan/scaffolds/rest-api/config.py_tmpl000066400000000000000000000024371445453044500222530ustar00rootroot00000000000000# Server Specific Configurations server = { 'port': '8080', 'host': '0.0.0.0' } # Pecan Application Configurations app = { 'root': '${package}.controllers.root.RootController', 'modules': ['${package}'], 'debug': True } logging = { 'root': {'level': 'INFO', 'handlers': ['console']}, 'loggers': { '${package}': {'level': 'DEBUG', 'handlers': ['console'], 'propagate': False}, 'pecan': {'level': 'DEBUG', 'handlers': ['console'], 'propagate': False}, 'py.warnings': {'handlers': ['console']}, '__force_dict__': True }, 'handlers': { 'console': { 'level': 'DEBUG', 'class': 'logging.StreamHandler', 'formatter': 'color' } }, 'formatters': { 'simple': { 'format': ('%(asctime)s %(levelname)-5.5s [%(name)s]' '[%(threadName)s] %(message)s') }, 'color': { '()': 'pecan.log.ColorFormatter', 'format': ('%(asctime)s [%(padded_color_levelname)s] [%(name)s]' '[%(threadName)s] %(message)s'), '__force_dict__': True } } } # Custom Configurations must be in Python dictionary format:: # # foo = {'bar':'baz'} # # All configurations are accessible at:: # pecan.conf pecan-1.5.1/pecan/scaffolds/rest-api/setup.cfg_tmpl000066400000000000000000000001341445453044500222450ustar00rootroot00000000000000[nosetests] match=^test where=${package} nocapture=1 cover-package=${package} cover-erase=1 pecan-1.5.1/pecan/scaffolds/rest-api/setup.py_tmpl000066400000000000000000000007521445453044500221440ustar00rootroot00000000000000# -*- coding: utf-8 -*- try: from setuptools import setup, find_packages except ImportError: from ez_setup import use_setuptools use_setuptools() from setuptools import setup, find_packages setup( name='${package}', version='0.1', description='', author='', author_email='', install_requires=[ "pecan", ], test_suite='${package}', zip_safe=False, include_package_data=True, packages=find_packages(exclude=['ez_setup']) ) pecan-1.5.1/pecan/secure.py000066400000000000000000000156541445453044500155550ustar00rootroot00000000000000from functools import wraps from inspect import getmembers, isfunction from webob import exc from .compat import is_bound_method as ismethod from .decorators import expose from .util import _cfg, iscontroller __all__ = ['unlocked', 'secure', 'SecureController'] class _SecureState(object): def __init__(self, desc, boolean_value): self.description = desc self.boolean_value = boolean_value def __repr__(self): return '' % self.description def __nonzero__(self): return self.boolean_value def __bool__(self): return self.__nonzero__() Any = _SecureState('Any', False) Protected = _SecureState('Protected', True) # security method decorators def _unlocked_method(func): _cfg(func)['secured'] = Any return func def _secure_method(check_permissions_func): def wrap(func): cfg = _cfg(func) cfg['secured'] = Protected cfg['check_permissions'] = check_permissions_func return func return wrap # classes to assist with wrapping attributes class _UnlockedAttribute(object): def __init__(self, obj): self.obj = obj @_unlocked_method @expose() def _lookup(self, *remainder): return self.obj, remainder class _SecuredAttribute(object): def __init__(self, obj, check_permissions): self.obj = obj self.check_permissions = check_permissions self._parent = None def _check_permissions(self): if isinstance(self.check_permissions, str): return getattr(self.parent, self.check_permissions)() else: return self.check_permissions() def __get_parent(self): return self._parent def __set_parent(self, parent): if ismethod(parent): self._parent = parent.__self__ else: self._parent = parent parent = property(__get_parent, __set_parent) @_secure_method('_check_permissions') @expose() def _lookup(self, *remainder): return self.obj, list(remainder) # helper for secure decorator def _allowed_check_permissions_types(x): return ( ismethod(x) or isfunction(x) or isinstance(x, str) ) # methods that can either decorate functions or wrap classes # these should be the main methods used for securing or unlocking def unlocked(func_or_obj): """ This method unlocks method or class attribute on a SecureController. Can be used to decorate or wrap an attribute """ if ismethod(func_or_obj) or isfunction(func_or_obj): return _unlocked_method(func_or_obj) else: return _UnlockedAttribute(func_or_obj) def secure(func_or_obj, check_permissions_for_obj=None): """ This method secures a method or class depending on invocation. To decorate a method use one argument: @secure() To secure a class, invoke with two arguments: secure(, ) """ if _allowed_check_permissions_types(func_or_obj): return _secure_method(func_or_obj) else: if not _allowed_check_permissions_types(check_permissions_for_obj): msg = "When securing an object, secure() requires the " + \ "second argument to be method" raise TypeError(msg) return _SecuredAttribute(func_or_obj, check_permissions_for_obj) class SecureControllerMeta(type): """ Used to apply security to a controller. Implementations of SecureController should extend the `check_permissions` method to return a True or False value (depending on whether or not the user has permissions to the controller). """ def __init__(cls, name, bases, dict_): cls._pecan = dict( secured=Protected, check_permissions=cls.check_permissions, unlocked=[] ) for name, value in getmembers(cls)[:]: if isfunction(value): if iscontroller(value) and value._pecan.get( 'secured' ) is None: # Wrap the function so that the security context is # local to this class definition. This works around # the fact that unbound method attributes are shared # across classes with the same bases. wrapped = _make_wrapper(value) wrapped._pecan['secured'] = Protected wrapped._pecan['check_permissions'] = \ cls.check_permissions setattr(cls, name, wrapped) elif hasattr(value, '__class__'): if name.startswith('__') and name.endswith('__'): continue if isinstance(value, _UnlockedAttribute): # mark it as unlocked and remove wrapper cls._pecan['unlocked'].append(value.obj) setattr(cls, name, value.obj) elif isinstance(value, _SecuredAttribute): # The user has specified a different check_permissions # than the class level version. As far as the class # is concerned, this method is unlocked because # it is using a check_permissions function embedded in # the _SecuredAttribute wrapper cls._pecan['unlocked'].append(value) class SecureControllerBase(object): @classmethod def check_permissions(cls): """ Returns `True` or `False` to grant access. Implemented in subclasses of :class:`SecureController`. """ return False SecureController = SecureControllerMeta( 'SecureController', (SecureControllerBase,), {'__doc__': SecureControllerMeta.__doc__} ) def _make_wrapper(f): """return a wrapped function with a copy of the _pecan context""" @wraps(f) def wrapper(*args, **kwargs): return f(*args, **kwargs) wrapper._pecan = f._pecan.copy() return wrapper # methods to evaluate security during routing def handle_security(controller, im_self=None): """ Checks the security of a controller. """ if controller._pecan.get('secured', False): check_permissions = controller._pecan['check_permissions'] if isinstance(check_permissions, str): check_permissions = getattr( im_self or controller.__self__, check_permissions ) if not check_permissions(): raise exc.HTTPUnauthorized def cross_boundary(prev_obj, obj): """ Check permissions as we move between object instances. """ if prev_obj is None: return if isinstance(obj, _SecuredAttribute): # a secure attribute can live in unsecure class so we have to set # while we walk the route obj.parent = prev_obj if hasattr(prev_obj, '_pecan'): if obj not in prev_obj._pecan.get('unlocked', []): handle_security(prev_obj) pecan-1.5.1/pecan/templating.py000066400000000000000000000203071445453044500164220ustar00rootroot00000000000000from .compat import escape from .jsonify import encode _builtin_renderers = {} error_formatters = [] # # JSON rendering engine # class JsonRenderer(object): ''' Defines the builtin ``JSON`` renderer. ''' def __init__(self, path, extra_vars): pass def render(self, template_path, namespace): ''' Implements ``JSON`` rendering. ''' return encode(namespace) # TODO: add error formatter for json (pass it through json lint?) _builtin_renderers['json'] = JsonRenderer # # Genshi rendering engine # try: from genshi.template import (TemplateLoader, TemplateError as gTemplateError) class GenshiRenderer(object): ''' Defines the builtin ``Genshi`` renderer. ''' def __init__(self, path, extra_vars): self.loader = TemplateLoader([path], auto_reload=True) self.extra_vars = extra_vars def render(self, template_path, namespace): ''' Implements ``Genshi`` rendering. ''' tmpl = self.loader.load(template_path) stream = tmpl.generate(**self.extra_vars.make_ns(namespace)) return stream.render('html') _builtin_renderers['genshi'] = GenshiRenderer def format_genshi_error(exc_value): ''' Implements ``Genshi`` renderer error formatting. ''' if isinstance(exc_value, (gTemplateError)): retval = '

Genshi error %s

' % escape( exc_value.args[0], True ) retval += format_line_context(exc_value.filename, exc_value.lineno) return retval error_formatters.append(format_genshi_error) except ImportError: # pragma no cover pass # # Mako rendering engine # try: from mako.lookup import TemplateLookup from mako.exceptions import (CompileException, SyntaxException, html_error_template) class MakoRenderer(object): ''' Defines the builtin ``Mako`` renderer. ''' def __init__(self, path, extra_vars): self.loader = TemplateLookup( directories=[path], output_encoding='utf-8' ) self.extra_vars = extra_vars def render(self, template_path, namespace): ''' Implements ``Mako`` rendering. ''' tmpl = self.loader.get_template(template_path) return tmpl.render(**self.extra_vars.make_ns(namespace)) _builtin_renderers['mako'] = MakoRenderer def format_mako_error(exc_value): ''' Implements ``Mako`` renderer error formatting. ''' if isinstance(exc_value, (CompileException, SyntaxException)): return html_error_template().render(full=False, css=False) error_formatters.append(format_mako_error) except ImportError: # pragma no cover pass # # Kajiki rendering engine # try: from kajiki.loader import FileLoader class KajikiRenderer(object): ''' Defines the builtin ``Kajiki`` renderer. ''' def __init__(self, path, extra_vars): self.loader = FileLoader(path, reload=True) self.extra_vars = extra_vars def render(self, template_path, namespace): ''' Implements ``Kajiki`` rendering. ''' Template = self.loader.import_(template_path) stream = Template(self.extra_vars.make_ns(namespace)) return stream.render() _builtin_renderers['kajiki'] = KajikiRenderer # TODO: add error formatter for kajiki except ImportError: # pragma no cover pass # # Jinja2 rendering engine # try: from jinja2 import Environment, FileSystemLoader from jinja2.exceptions import TemplateSyntaxError as jTemplateSyntaxError class JinjaRenderer(object): ''' Defines the builtin ``Jinja`` renderer. ''' def __init__(self, path, extra_vars): self.env = Environment(loader=FileSystemLoader(path)) self.extra_vars = extra_vars def render(self, template_path, namespace): ''' Implements ``Jinja`` rendering. ''' template = self.env.get_template(template_path) return template.render(self.extra_vars.make_ns(namespace)) _builtin_renderers['jinja'] = JinjaRenderer def format_jinja_error(exc_value): ''' Implements ``Jinja`` renderer error formatting. ''' retval = '

Jinja2 error in \'%s\' on line %d

%s
' if isinstance(exc_value, (jTemplateSyntaxError)): retval = retval % ( exc_value.name, exc_value.lineno, exc_value.message ) retval += format_line_context(exc_value.filename, exc_value.lineno) return retval error_formatters.append(format_jinja_error) except ImportError: # pragma no cover pass # # format helper function # def format_line_context(filename, lineno, context=10): ''' Formats the the line context for error rendering. :param filename: the location of the file, within which the error occurred :param lineno: the offending line number :param context: number of lines of code to display before and after the offending line. ''' with open(filename) as f: lines = f.readlines() lineno = lineno - 1 # files are indexed by 1 not 0 if lineno > 0: start_lineno = max(lineno - context, 0) end_lineno = lineno + context lines = [escape(l, True) for l in lines[start_lineno:end_lineno]] i = lineno - start_lineno lines[i] = '%s' % lines[i] else: lines = [escape(l, True) for l in lines[:context]] msg = '
%s
' return msg % ''.join(lines) # # Extra Vars Rendering # class ExtraNamespace(object): ''' Extra variables for the template namespace to pass to the renderer as named parameters. :param extras: dictionary of extra parameters. Defaults to an empty dict. ''' def __init__(self, extras={}): self.namespace = dict(extras) def update(self, d): ''' Updates the extra variable dictionary for the namespace. ''' self.namespace.update(d) def make_ns(self, ns): ''' Returns the `lazily` created template namespace. ''' if self.namespace: val = {} val.update(self.namespace) val.update(ns) return val else: return ns # # Rendering Factory # class RendererFactory(object): ''' Manufactures known Renderer objects. :param custom_renderers: custom-defined renderers to manufacture :param extra_vars: extra vars for the template namespace ''' def __init__(self, custom_renderers={}, extra_vars={}): self._renderers = {} self._renderer_classes = dict(_builtin_renderers) self.add_renderers(custom_renderers) self.extra_vars = ExtraNamespace(extra_vars) def add_renderers(self, custom_dict): ''' Adds a custom renderer. :param custom_dict: a dictionary of custom renderers to add ''' self._renderer_classes.update(custom_dict) def available(self, name): ''' Returns true if queried renderer class is available. :param name: renderer name ''' return name in self._renderer_classes def get(self, name, template_path): ''' Returns the renderer object. :param name: name of the requested renderer :param template_path: path to the template ''' if name not in self._renderers: cls = self._renderer_classes.get(name) if cls is None: return None else: self._renderers[name] = cls(template_path, self.extra_vars) return self._renderers[name] def keys(self, *args, **kwargs): return self._renderer_classes.keys(*args, **kwargs) pecan-1.5.1/pecan/testing.py000066400000000000000000000032571445453044500157400ustar00rootroot00000000000000import os from pecan import load_app def load_test_app(config=None, **kwargs): """ Used for functional tests where you need to test your literal application and its integration with the framework. :param config: Can be a dictionary containing configuration, a string which represents a (relative) configuration filename or ``None`` which will fallback to get the ``PECAN_CONFIG`` env variable. returns a pecan.Pecan WSGI application wrapped in a webtest.TestApp instance. :: app = load_test_app('path/to/some/config.py') resp = app.get('/path/to/some/resource').status_int assert resp.status_int == 200 resp = app.post('/path/to/some/resource', params={'param': 'value'}) assert resp.status_int == 302 Alternatively you could call ``load_test_app`` with no parameters if the environment variable is set :: app = load_test_app() resp = app.get('/path/to/some/resource').status_int assert resp.status_int == 200 """ # Only import at runtime, since this is a relatively heavy-weight # dependency: from webtest import TestApp return TestApp(load_app(config, **kwargs)) def reset_global_config(): """ When tests alter application configurations they can get sticky and pollute other tests that might rely on a pristine configuration. This helper will reset the config by overwriting it with ``pecan.configuration.DEFAULT``. """ from pecan import configuration configuration.set_config( dict(configuration.initconf()), overwrite=True ) os.environ.pop('PECAN_CONFIG', None) pecan-1.5.1/pecan/tests/000077500000000000000000000000001445453044500150445ustar00rootroot00000000000000pecan-1.5.1/pecan/tests/__init__.py000066400000000000000000000003051445453044500171530ustar00rootroot00000000000000import sys import os from unittest import TestCase from pecan.testing import reset_global_config class PecanTestCase(TestCase): def setUp(self): self.addCleanup(reset_global_config) pecan-1.5.1/pecan/tests/config_fixtures/000077500000000000000000000000001445453044500202425ustar00rootroot00000000000000pecan-1.5.1/pecan/tests/config_fixtures/bad/000077500000000000000000000000001445453044500207705ustar00rootroot00000000000000pecan-1.5.1/pecan/tests/config_fixtures/bad/importerror.py000066400000000000000000000000441445453044500237240ustar00rootroot00000000000000import pecan.thismoduledoesnotexist pecan-1.5.1/pecan/tests/config_fixtures/bad/module_and_underscore.py000066400000000000000000000000601445453044500256760ustar00rootroot00000000000000import sys __badattr__ = True moduleattr = sys pecan-1.5.1/pecan/tests/config_fixtures/config.py000066400000000000000000000006671445453044500220720ustar00rootroot00000000000000 # Server Specific Configurations server = { 'port': '8081', 'host': '1.1.1.1', 'hostport': '{pecan.conf.server.host}:{pecan.conf.server.port}' } # Pecan Application Configurations app = { 'static_root': 'public', 'template_path': 'myproject/templates', 'debug': True } # Custom Configurations must be in Python dictionary format:: # # foo = {'bar':'baz'} # # All configurations are accessible at:: # pecan.conf pecan-1.5.1/pecan/tests/config_fixtures/empty.py000066400000000000000000000000251445453044500217470ustar00rootroot00000000000000app = {} server = {} pecan-1.5.1/pecan/tests/config_fixtures/foobar.py000066400000000000000000000000141445453044500220570ustar00rootroot00000000000000foo = "bar" pecan-1.5.1/pecan/tests/config_fixtures/forcedict.py000066400000000000000000000005221445453044500225550ustar00rootroot00000000000000# Pecan Application Configurations beaker = { 'session.key': 'key', 'session.type': 'cookie', 'session.validate_key': '1a971a7df182df3e1dec0af7c6913ec7', '__force_dict__': True } # Custom Configurations must be in Python dictionary format:: # # foo = {'bar':'baz'} # # All configurations are accessible at:: # pecan.conf pecan-1.5.1/pecan/tests/middleware/000077500000000000000000000000001445453044500171615ustar00rootroot00000000000000pecan-1.5.1/pecan/tests/middleware/__init__.py000066400000000000000000000000001445453044500212600ustar00rootroot00000000000000pecan-1.5.1/pecan/tests/middleware/static_fixtures/000077500000000000000000000000001445453044500224015ustar00rootroot00000000000000pecan-1.5.1/pecan/tests/middleware/static_fixtures/self.png000066400000000000000000000155001445453044500240410ustar00rootroot00000000000000PNG  IHDR$AiCCPICC ProfileXY8?N{T^{k+UH!DIIVF)R@V]߹s{?}>~^nCCaI6:pv{wylt<H$@XD<>*Jt~+5<|!?H>{N`fWo?1c>Vk5RQPݨǨTAQ{;_F! iwȈ^ bW(/o*(@aWkTwD?XZZ1 $ŵњJ][F%CpPWtN놄Ɛ||yK8CRGVZF콿7=bʲfm>&3VnxD"; өgEv> ) X;  AQ8R@( \7AhC LY,A"@+ @b, i@d9Cn E@G$(: .B =h (f9aAX VaSX8>eU?G)( %RF,Q.(o JGPf$P54MAK 4Bۣ=ЇL9t !`80bU1 ド¤` 0;.d=bVX,VD3{-a۱i2cʼnq8".;k fq?xn,' W!~LL̒̓,,ll\\܎ܟy!y-y+o{(T()(( )Ssrp(X4 <ɹĥϕʵMϭǝG'/oE>ލ=B{$3GΧ͗o\L@YW@ y!!cXWaMCe"XeQXTAWH_ S+LjIPJhKDJHd4Ll$/"#-SZA:PB LLWYQY"a9QF/b^+++PPRTR$)*.(+)+)3([)g*?RUiQYSUT WYMB-@^^{+NQ'_TpӸ1ɫI,|ŧU5-}UIΪnnJP/]ON^%CÆF#S1cNcj%%8Lߚ̚as\W 2rJ]ku;#6ݶmخeٽpvpuvXus<8$ęϹR鲼O_YWB?>v oaݮm-eewcb%]3<<<ԽN{y{QY-];ȿ52rvc`]>-).8 AWHt`XhJ!CH0(lXc8r8&R#(GCԭhјXKч=w=rț8P{|QGg  84Q:t$ǤdkRRH)c'NSRΦLLɐ(9)s)S}YY糱٣99UiOǞ5ϭK0q|A3g >yHNQ]1GqZjgy󵥜.nQixWSwTiQccS=Ͽ|eZ~OU)aikXQFirJ꛽ue 74گtzنF&)Em?l-O::?qyouuAFNdu:CL]N :L Ç}hʍ u>l|D=N)^J1D$SNTJkO|}rǬ9+s g (:Z\XR{ɅeS +QDuD]ͻvv ڛrloɮnhn~n˝{um%;;<}ҽhq' =+QŌU=|5FKDΤSjSޔFMϬ6g373|Aja}ՇEu<>~dˑ_~L\FhfkKr{{|u%G$`px 2!r!=U*kjxRVF&ef"K*mI N9} <y[R6q!+9/+"XԤTXuv Ms@,z F&fIVǭSlRm2c|\iw9u0:ýǣWaG_i?JEꠢଐP!WVwFHĵȔ(hXÜGXib$=֓x#)?9zfic2<)k9{5gܥ8KsN(t…ً^VsIDS>tx́A!g#֣cϓ_{Jwmbq)7ooMwΔz5G97<_pEߏAB?.~!}=jopzO\>lRn_ !чɆYEum}7oO_ QP_GpHh8, 9&HYl\"V_)_IUj^YuFf.?g0jxרDdʴgidilb޺&N͞`?p񈓉3k}!ށnxAb{^OJqއ|}6}@@{ `tpWPjHș]n=0rodTB4gX5GWv&IKMf9NRN}VA̔?;9~zVzv@is.kI=?^4Sz^TE2r KɫȪHRvDg7o r}`CsSqX Ha{;Z;_?~ۭqfύ'[}" N>>9iy8 Tof2c>Z|]Yod;@.g:D9 HyfrةD- kp!'97Hi\|;!U xD -$? N"`4 C0/{',o^GQ*VE/0tS$#BX-lq@|̑lܜ"*E%ҝ @H#|jA2@sf֙΀2}=*C' 4S3EՂu-]}ӝKC|^=|XLD"K#bwKKJH3H/<"&`(ĨQeRuHgon>qy=E9ޘ̄Ҕ\2*Ϻf֎`/t˜+~Ux{zi] 82tȀ.Q%}#vၸ yfWN䥉wezbz4w"Jb.T_pUv(j[upw<*\~13o޹-}|+.޸1ywg~s@M`\A94~0 &H 2$ Ѱlsp'Ł2EAաflht a11w1XMl1猻ߑ- >ߤpd$a U43uM;m.]-3f12=a>Rj-^1ÙeM=s?>~uDD $4 ^fMv^nLG}*_ըJ[kik}ut T Ǜ\46[`Է @δ<3vyۜ(Kp}z`[;g> @s!Ia#ǣibc?;:}'&'%<v"z=3'1)_Y"R貇U0^sLÑ'-ԭz :oL }2:|z|iY9E*W67;\?~Z$  1"AP;fpGѢQɨvZ nFob11v+!a2rܟ|B. }U"5/u;+ m6S`C+f.>Sl: \ܧyxHc~&%J%/%& U"(L
o bW1 @ 茊<)"!hf2|BsB.5,m8>sTjd΋|/\.UpL^eOUpՁZ7߉lLk;p/M}CtaOzg8 1<3y=~눩39x>=O >,|95޷W-_F.pѿVNX]P(iYeu'ϫжyoNQU$hYC@#:5̨)_O pHYs  IDAT8T]HSa~sb3u\)Rf֔n +A&H20 P4  i;f4773p*Ss<9/38(R}:I.5413L ͇)21qx|jyN*"B6Ucf~juQSIVR`Y`92RM}PRZ/򉳆%u%Ei8~4|l Zt%poo] D&*HYuo~u9 4c6MPRK[Is:2YI赭щya%5U~:Z'8G˖9g򜌸0 -7욮7%NSeK C&zp'[QV|J-C(nNM?]HJX,ފ/,.2YD#Hg:{"F=اӍfUGqn=sv{'$:O:HUM ږίEu4\e|T;uV7x>X-vCDxfh21:Y#blэN26϶dGfMWL]O@[7S :j#AU ZqHڒg2U8pAeзY8N'+)"J@n*%"J f8&KZycGGd`y_qHdIENDB`pecan-1.5.1/pecan/tests/middleware/static_fixtures/text.txt000066400000000000000000000007311445453044500241270ustar00rootroot00000000000000This is a test text file. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.pecan-1.5.1/pecan/tests/middleware/test_errordocument.py000066400000000000000000000056721445453044500234740ustar00rootroot00000000000000import json from webtest import TestApp import pecan from pecan.middleware.errordocument import ErrorDocumentMiddleware from pecan.middleware.recursive import RecursiveMiddleware from pecan.tests import PecanTestCase def four_oh_four_app(environ, start_response): if environ['PATH_INFO'].startswith('/error'): code = environ['PATH_INFO'].split('/')[2].encode('utf-8') start_response("200 OK", [('Content-type', 'text/plain')]) body = b"Error: %s" % code if environ['QUERY_STRING']: body += b"\nQS: %s" % environ['QUERY_STRING'].encode('utf-8') return [body] start_response("404 Not Found", [('Content-type', 'text/plain')]) return [] class TestErrorDocumentMiddleware(PecanTestCase): def setUp(self): super(TestErrorDocumentMiddleware, self).setUp() self.app = TestApp(RecursiveMiddleware(ErrorDocumentMiddleware( four_oh_four_app, {404: '/error/404'} ))) def test_hit_error_page(self): r = self.app.get('/error/404') assert r.status_int == 200 assert r.body == b'Error: 404' def test_middleware_routes_to_404_message(self): r = self.app.get('/', expect_errors=True) assert r.status_int == 404 assert r.body == b'Error: 404' def test_error_endpoint_with_query_string(self): app = TestApp(RecursiveMiddleware(ErrorDocumentMiddleware( four_oh_four_app, {404: '/error/404?foo=bar'} ))) r = app.get('/', expect_errors=True) assert r.status_int == 404 assert r.body == b'Error: 404\nQS: foo=bar' def test_error_with_recursion_loop(self): app = TestApp(RecursiveMiddleware(ErrorDocumentMiddleware( four_oh_four_app, {404: '/'} ))) r = app.get('/', expect_errors=True) assert r.status_int == 404 assert r.body == ( b'Error: 404 Not Found. (Error page could not be fetched)' ) def test_original_exception(self): class RootController(object): @pecan.expose() def index(self): if pecan.request.method != 'POST': pecan.abort(405, 'You have to POST, dummy!') return 'Hello, World!' @pecan.expose('json') def error(self, status): return dict( status=int(status), reason=pecan.request.environ[ 'pecan.original_exception' ].detail ) app = pecan.Pecan(RootController()) app = RecursiveMiddleware(ErrorDocumentMiddleware(app, { 405: '/error/405' })) app = TestApp(app) assert app.post('/').status_int == 200 r = app.get('/', expect_errors=405) assert r.status_int == 405 resp = json.loads(r.body.decode()) assert resp['status'] == 405 assert resp['reason'] == 'You have to POST, dummy!' pecan-1.5.1/pecan/tests/middleware/test_recursive.py000066400000000000000000000125731445453044500226110ustar00rootroot00000000000000from webtest import TestApp from pecan.middleware.recursive import (RecursiveMiddleware, ForwardRequestException) from pecan.tests import PecanTestCase def simple_app(environ, start_response): start_response("200 OK", [('Content-type', 'text/plain')]) return [b'requested page returned'] def error_docs_app(environ, start_response): if environ['PATH_INFO'] == '/not_found': start_response("404 Not found", [('Content-type', 'text/plain')]) return [b'Not found'] elif environ['PATH_INFO'] == '/error': start_response("200 OK", [('Content-type', 'text/plain')]) return [b'Page not found'] elif environ['PATH_INFO'] == '/recurse': raise ForwardRequestException('/recurse') else: return simple_app(environ, start_response) class Middleware(object): def __init__(self, app, url='/error'): self.app = app self.url = url def __call__(self, environ, start_response): raise ForwardRequestException(self.url) def forward(app): app = TestApp(RecursiveMiddleware(app)) res = app.get('') assert res.headers['content-type'] == 'text/plain' assert res.status == '200 OK' assert 'requested page returned' in res res = app.get('/error') assert res.headers['content-type'] == 'text/plain' assert res.status == '200 OK' assert 'Page not found' in res res = app.get('/not_found') assert res.headers['content-type'] == 'text/plain' assert res.status == '200 OK' assert 'Page not found' in res try: res = app.get('/recurse') except AssertionError as e: if str(e).startswith('Forwarding loop detected'): pass else: raise AssertionError('Failed to detect forwarding loop') class TestRecursiveMiddleware(PecanTestCase): def test_ForwardRequest_url(self): class TestForwardRequestMiddleware(Middleware): def __call__(self, environ, start_response): if environ['PATH_INFO'] != '/not_found': return self.app(environ, start_response) raise ForwardRequestException(self.url) forward(TestForwardRequestMiddleware(error_docs_app)) def test_ForwardRequest_url_with_params(self): class TestForwardRequestMiddleware(Middleware): def __call__(self, environ, start_response): if environ['PATH_INFO'] != '/not_found': return self.app(environ, start_response) raise ForwardRequestException(self.url + '?q=1') forward(TestForwardRequestMiddleware(error_docs_app)) def test_ForwardRequest_environ(self): class TestForwardRequestMiddleware(Middleware): def __call__(self, environ, start_response): if environ['PATH_INFO'] != '/not_found': return self.app(environ, start_response) environ['PATH_INFO'] = self.url raise ForwardRequestException(environ=environ) forward(TestForwardRequestMiddleware(error_docs_app)) def test_ForwardRequest_factory(self): class TestForwardRequestMiddleware(Middleware): def __call__(self, environ, start_response): if environ['PATH_INFO'] != '/not_found': return self.app(environ, start_response) environ['PATH_INFO'] = self.url def factory(app): class WSGIApp(object): def __init__(self, app): self.app = app def __call__(self, e, start_response): def keep_status_start_response(status, headers, exc_info=None): return start_response( '404 Not Found', headers, exc_info ) return self.app(e, keep_status_start_response) return WSGIApp(app) raise ForwardRequestException(factory=factory) app = TestForwardRequestMiddleware(error_docs_app) app = TestApp(RecursiveMiddleware(app)) res = app.get('') assert res.headers['content-type'] == 'text/plain' assert res.status == '200 OK' assert 'requested page returned' in res res = app.get('/error') assert res.headers['content-type'] == 'text/plain' assert res.status == '200 OK' assert 'Page not found' in res res = app.get('/not_found', status=404) assert res.headers['content-type'] == 'text/plain' assert res.status == '404 Not Found' # Different status assert 'Page not found' in res try: res = app.get('/recurse') except AssertionError as e: if str(e).startswith('Forwarding loop detected'): pass else: raise AssertionError('Failed to detect forwarding loop') def test_ForwardRequestException(self): class TestForwardRequestExceptionMiddleware(Middleware): def __call__(self, environ, start_response): if environ['PATH_INFO'] != '/not_found': return self.app(environ, start_response) raise ForwardRequestException(path_info=self.url) forward(TestForwardRequestExceptionMiddleware(error_docs_app)) pecan-1.5.1/pecan/tests/middleware/test_static.py000066400000000000000000000045261445453044500220700ustar00rootroot00000000000000from pecan.middleware.static import (StaticFileMiddleware, FileWrapper, _dump_date) from pecan.tests import PecanTestCase import os class TestStaticFileMiddleware(PecanTestCase): def setUp(self): super(TestStaticFileMiddleware, self).setUp() def app(environ, start_response): response_headers = [('Content-type', 'text/plain')] start_response('200 OK', response_headers) return ['Hello world!\n'] self.app = StaticFileMiddleware( app, os.path.dirname(__file__) ) self._status = None self._response_headers = None def _request(self, path): def start_response(status, response_headers, exc_info=None): self._status = status self._response_headers = response_headers return self.app( dict(PATH_INFO=path), start_response ) def _get_response_header(self, header): for k, v in self._response_headers: if k.upper() == header.upper(): return v return None def test_file_can_be_found(self): result = self._request('/static_fixtures/text.txt') assert isinstance(result, FileWrapper) result.close() def test_no_file_found_causes_passthrough(self): result = self._request('/static_fixtures/nosuchfile.txt') assert not isinstance(result, FileWrapper) assert result == ['Hello world!\n'] def test_mime_type_works_for_png_files(self): result = self._request('/static_fixtures/self.png') assert self._get_response_header('Content-Type') == 'image/png' result.close() def test_file_can_be_closed(self): result = self._request('/static_fixtures/text.txt') assert result.close() is None def test_file_can_be_iterated_over(self): result = self._request('/static_fixtures/text.txt') assert len([x for x in result]) result.close() def test_date_dumping_on_unix_timestamps(self): result = _dump_date(1331755274.59, ' ') assert result == 'Wed, 14 Mar 2012 20:01:14 GMT' def test_separator_sanitization_still_finds_file(self): os.altsep = ':' result = self._request(':static_fixtures:text.txt') assert isinstance(result, FileWrapper) result.close() pecan-1.5.1/pecan/tests/scaffold_builder.py000066400000000000000000000105631445453044500207120ustar00rootroot00000000000000import os import sys import subprocess import time from pecan.compat import urlopen, URLError from pecan.tests import PecanTestCase import unittest if __name__ == '__main__': class TestTemplateBuilds(PecanTestCase): """ Used to test the templated quickstart project(s). """ @property def bin(self): return os.path.dirname(sys.executable) def poll(self, proc): limit = 30 for i in range(limit): proc.poll() # Make sure it's running if proc.returncode is None: break elif i == limit: # pragma: no cover raise RuntimeError("Server process didn't start.") time.sleep(.1) def test_project_pecan_serve_command(self): # Start the server proc = subprocess.Popen([ os.path.join(self.bin, 'pecan'), 'serve', 'testing123/config.py' ]) try: self.poll(proc) retries = 30 while True: retries -= 1 if retries < 0: # pragma: nocover raise RuntimeError( "The HTTP server has not replied within 3 seconds." ) try: # ...and that it's serving (valid) content... resp = urlopen('http://localhost:8080/') assert resp.getcode() assert len(resp.read().decode()) except URLError: pass else: break time.sleep(.1) finally: proc.terminate() def test_project_pecan_shell_command(self): # Start the server proc = subprocess.Popen([ os.path.join(self.bin, 'pecan'), 'shell', 'testing123/config.py' ], stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE ) self.poll(proc) out, _ = proc.communicate( b'{"model" : model, "conf" : conf, "app" : app}' ) assert 'testing123.model' in out.decode(), out assert 'Config(' in out.decode(), out assert 'webtest.app.TestApp' in out.decode(), out try: # just in case stdin doesn't close proc.terminate() except: pass class TestThirdPartyServe(TestTemplateBuilds): def poll_http(self, name, proc, port): try: self.poll(proc) retries = 30 while True: retries -= 1 if retries < 0: # pragma: nocover raise RuntimeError( "The %s server has not replied within" " 3 seconds." % name ) try: # ...and that it's serving (valid) content... resp = urlopen('http://localhost:%d/' % port) assert resp.getcode() assert len(resp.read().decode()) except URLError: pass else: break time.sleep(.1) finally: proc.terminate() class TestUWSGIServiceCommand(TestThirdPartyServe): def test_serve_from_config(self): # Start the server proc = subprocess.Popen([ os.path.join(self.bin, 'uwsgi'), '--http-socket', ':8080', '--venv', sys.prefix, '--pecan', 'testing123/config.py' ]) self.poll_http('uwsgi', proc, 8080) # First, ensure that the `testing123` package has been installed args = [ os.path.join(os.path.dirname(sys.executable), 'pip'), 'install', '-U', '-e', './testing123' ] process = subprocess.Popen(args) _, unused_err = process.communicate() assert not process.poll() unittest.main() pecan-1.5.1/pecan/tests/scaffold_fixtures/000077500000000000000000000000001445453044500205565ustar00rootroot00000000000000pecan-1.5.1/pecan/tests/scaffold_fixtures/__init__.py000066400000000000000000000000001445453044500226550ustar00rootroot00000000000000pecan-1.5.1/pecan/tests/scaffold_fixtures/content_sub/000077500000000000000000000000001445453044500231015ustar00rootroot00000000000000pecan-1.5.1/pecan/tests/scaffold_fixtures/content_sub/bar/000077500000000000000000000000001445453044500236455ustar00rootroot00000000000000pecan-1.5.1/pecan/tests/scaffold_fixtures/content_sub/bar/spam.txt_tmpl000066400000000000000000000000211445453044500263730ustar00rootroot00000000000000Pecan ${package} pecan-1.5.1/pecan/tests/scaffold_fixtures/content_sub/foo_tmpl000066400000000000000000000000171445453044500246410ustar00rootroot00000000000000YAR ${package} pecan-1.5.1/pecan/tests/scaffold_fixtures/file_sub/000077500000000000000000000000001445453044500223465ustar00rootroot00000000000000pecan-1.5.1/pecan/tests/scaffold_fixtures/file_sub/bar_+package+/000077500000000000000000000000001445453044500247135ustar00rootroot00000000000000pecan-1.5.1/pecan/tests/scaffold_fixtures/file_sub/bar_+package+/spam.txt000066400000000000000000000000061445453044500264100ustar00rootroot00000000000000Pecan pecan-1.5.1/pecan/tests/scaffold_fixtures/file_sub/foo_+package+000066400000000000000000000000041445453044500246470ustar00rootroot00000000000000YAR pecan-1.5.1/pecan/tests/scaffold_fixtures/simple/000077500000000000000000000000001445453044500220475ustar00rootroot00000000000000pecan-1.5.1/pecan/tests/scaffold_fixtures/simple/bar/000077500000000000000000000000001445453044500226135ustar00rootroot00000000000000pecan-1.5.1/pecan/tests/scaffold_fixtures/simple/bar/spam.txt000066400000000000000000000000061445453044500243100ustar00rootroot00000000000000Pecan pecan-1.5.1/pecan/tests/scaffold_fixtures/simple/foo000066400000000000000000000000041445453044500225470ustar00rootroot00000000000000YAR pecan-1.5.1/pecan/tests/templates/000077500000000000000000000000001445453044500170425ustar00rootroot00000000000000pecan-1.5.1/pecan/tests/templates/__init__.py000066400000000000000000000000001445453044500211410ustar00rootroot00000000000000pecan-1.5.1/pecan/tests/templates/form_colors.html000066400000000000000000000003171445453044500222550ustar00rootroot00000000000000 pecan-1.5.1/pecan/tests/templates/form_colors_invalid.html000066400000000000000000000002131445453044500237560ustar00rootroot00000000000000 pecan-1.5.1/pecan/tests/templates/form_colors_valid.html000066400000000000000000000002001445453044500234230ustar00rootroot00000000000000 pecan-1.5.1/pecan/tests/templates/form_login_invalid.html000066400000000000000000000002171445453044500235710ustar00rootroot00000000000000 pecan-1.5.1/pecan/tests/templates/form_login_valid.html000066400000000000000000000001641445453044500232430ustar00rootroot00000000000000 pecan-1.5.1/pecan/tests/templates/form_name.html000066400000000000000000000001131445453044500216660ustar00rootroot00000000000000pecan-1.5.1/pecan/tests/templates/form_name_invalid.html000066400000000000000000000002231445453044500233760ustar00rootroot00000000000000 Please enter a value
pecan-1.5.1/pecan/tests/templates/form_name_invalid_custom.html000066400000000000000000000002301445453044500247660ustar00rootroot00000000000000 Names must be unique
pecan-1.5.1/pecan/tests/templates/form_name_valid.html000066400000000000000000000000711445453044500230500ustar00rootroot00000000000000pecan-1.5.1/pecan/tests/templates/genshi.html000066400000000000000000000007301445453044500212050ustar00rootroot00000000000000 Hello, ${name}!

Hello, ${name}!

pecan-1.5.1/pecan/tests/templates/genshi_bad.html000066400000000000000000000010061445453044500220100ustar00rootroot00000000000000 Hello, ${name}!

Hello, ${name}!

pecan-1.5.1/pecan/tests/templates/jinja.html000066400000000000000000000001631445453044500210230ustar00rootroot00000000000000 Hello, {{name}}!

Hello, {{name}}!

pecan-1.5.1/pecan/tests/templates/jinja_bad.html000066400000000000000000000002431445453044500216300ustar00rootroot00000000000000 Hello, {{name}}!

Hello, {{name}}!

{# open a block without and name #} {% block %} pecan-1.5.1/pecan/tests/templates/kajiki.html000066400000000000000000000001551445453044500211730ustar00rootroot00000000000000 Hello, ${name}!

Hello, ${name}!

pecan-1.5.1/pecan/tests/templates/mako.html000066400000000000000000000001541445453044500206570ustar00rootroot00000000000000 Hello, ${name}!

Hello, ${name}!

pecan-1.5.1/pecan/tests/templates/mako_bad.html000066400000000000000000000000561445453044500214660ustar00rootroot00000000000000<% def bad_indentation: return None %> pecan-1.5.1/pecan/tests/test_base.py000066400000000000000000002172511445453044500173770ustar00rootroot00000000000000# -*- coding: utf-8 -*- import sys import os import json import traceback import warnings from io import StringIO, BytesIO import webob from webob.exc import HTTPNotFound from webtest import TestApp from pecan import ( Pecan, Request, Response, expose, request, response, redirect, abort, make_app, override_template, render, route ) from pecan.templating import ( _builtin_renderers as builtin_renderers, error_formatters, MakoRenderer ) from pecan.decorators import accept_noncanonical from pecan.tests import PecanTestCase import unittest class SampleRootController(object): pass class TestAppRoot(PecanTestCase): def test_controller_lookup_by_string_path(self): app = Pecan('pecan.tests.test_base.SampleRootController') assert app.root and app.root.__class__.__name__ == 'SampleRootController' class TestEmptyContent(PecanTestCase): @property def app_(self): class RootController(object): @expose() def index(self): pass @expose() def explicit_body(self): response.body = b'Hello, World!' @expose() def empty_body(self): response.body = b'' @expose() def explicit_text(self): response.text = 'Hello, World!' @expose() def empty_text(self): response.text = '' @expose() def explicit_json(self): response.json = {'foo': 'bar'} @expose() def explicit_json_body(self): response.json_body = {'foo': 'bar'} @expose() def non_unicode(self): return chr(0xc0) return TestApp(Pecan(RootController())) def test_empty_index(self): r = self.app_.get('/') self.assertEqual(r.status_int, 204) self.assertNotIn('Content-Type', r.headers) self.assertEqual(r.headers['Content-Length'], '0') self.assertEqual(len(r.body), 0) def test_index_with_non_unicode(self): r = self.app_.get('/non_unicode/') self.assertEqual(r.status_int, 200) def test_explicit_body(self): r = self.app_.get('/explicit_body/') self.assertEqual(r.status_int, 200) self.assertEqual(r.body, b'Hello, World!') def test_empty_body(self): r = self.app_.get('/empty_body/') self.assertEqual(r.status_int, 204) self.assertEqual(r.body, b'') def test_explicit_text(self): r = self.app_.get('/explicit_text/') self.assertEqual(r.status_int, 200) self.assertEqual(r.body, b'Hello, World!') def test_empty_text(self): r = self.app_.get('/empty_text/') self.assertEqual(r.status_int, 204) self.assertEqual(r.body, b'') def test_explicit_json(self): r = self.app_.get('/explicit_json/') self.assertEqual(r.status_int, 200) json_resp = json.loads(r.body.decode()) assert json_resp == {'foo': 'bar'} def test_explicit_json_body(self): r = self.app_.get('/explicit_json_body/') self.assertEqual(r.status_int, 200) json_resp = json.loads(r.body.decode()) assert json_resp == {'foo': 'bar'} class TestAppIterFile(PecanTestCase): @property def app_(self): class RootController(object): @expose() def index(self): body = BytesIO(b'Hello, World!') response.body_file = body @expose() def empty(self): body = BytesIO(b'') response.body_file = body return TestApp(Pecan(RootController())) def test_body_generator(self): r = self.app_.get('/') self.assertEqual(r.status_int, 200) assert r.body == b'Hello, World!' def test_empty_body_generator(self): r = self.app_.get('/empty') self.assertEqual(r.status_int, 204) assert len(r.body) == 0 class TestInvalidURLEncoding(PecanTestCase): @property def app_(self): class RootController(object): @expose() def _route(self, args, request): assert request.path return TestApp(Pecan(RootController())) def test_rest_with_non_utf_8_body(self): r = self.app_.get('/%aa/', expect_errors=True) assert r.status_int == 400 class TestIndexRouting(PecanTestCase): @property def app_(self): class RootController(object): @expose() def index(self): return 'Hello, World!' return TestApp(Pecan(RootController())) def test_empty_root(self): r = self.app_.get('/') assert r.status_int == 200 assert r.body == b'Hello, World!' def test_index(self): r = self.app_.get('/index') assert r.status_int == 200 assert r.body == b'Hello, World!' def test_index_html(self): r = self.app_.get('/index.html') assert r.status_int == 200 assert r.body == b'Hello, World!' class TestObjectDispatch(PecanTestCase): @property def app_(self): class SubSubController(object): @expose() def index(self): return '/sub/sub/' @expose() def deeper(self): return '/sub/sub/deeper' class SubController(object): @expose() def index(self): return '/sub/' @expose() def deeper(self): return '/sub/deeper' sub = SubSubController() class RootController(object): @expose() def index(self): return '/' @expose() def deeper(self): return '/deeper' sub = SubController() return TestApp(Pecan(RootController())) def test_index(self): r = self.app_.get('/') assert r.status_int == 200 assert r.body == b'/' def test_one_level(self): r = self.app_.get('/deeper') assert r.status_int == 200 assert r.body == b'/deeper' def test_one_level_with_trailing(self): r = self.app_.get('/sub/') assert r.status_int == 200 assert r.body == b'/sub/' def test_two_levels(self): r = self.app_.get('/sub/deeper') assert r.status_int == 200 assert r.body == b'/sub/deeper' def test_two_levels_with_trailing(self): r = self.app_.get('/sub/sub/') assert r.status_int == 200 def test_three_levels(self): r = self.app_.get('/sub/sub/deeper') assert r.status_int == 200 assert r.body == b'/sub/sub/deeper' class TestUnicodePathSegments(PecanTestCase): def test_unicode_methods(self): class RootController(object): pass setattr(RootController, '🌰', expose()(lambda self: 'Hello, World!')) app = TestApp(Pecan(RootController())) resp = app.get('/%F0%9F%8C%B0/') assert resp.status_int == 200 assert resp.body == b'Hello, World!' def test_unicode_child(self): class ChildController(object): @expose() def index(self): return 'Hello, World!' class RootController(object): pass setattr(RootController, '🌰', ChildController()) app = TestApp(Pecan(RootController())) resp = app.get('/%F0%9F%8C%B0/') assert resp.status_int == 200 assert resp.body == b'Hello, World!' class TestLookups(PecanTestCase): @property def app_(self): class LookupController(object): def __init__(self, someID): self.someID = someID @expose() def index(self): return '/%s' % self.someID @expose() def name(self): return '/%s/name' % self.someID class RootController(object): @expose() def index(self): return '/' @expose() def _lookup(self, someID, *remainder): return LookupController(someID), remainder return TestApp(Pecan(RootController())) def test_index(self): r = self.app_.get('/') assert r.status_int == 200 assert r.body == b'/' def test_lookup(self): r = self.app_.get('/100/') assert r.status_int == 200 assert r.body == b'/100' def test_lookup_with_method(self): r = self.app_.get('/100/name') assert r.status_int == 200 assert r.body == b'/100/name' def test_lookup_with_wrong_argspec(self): class RootController(object): @expose() def _lookup(self, someID): return 'Bad arg spec' # pragma: nocover app = TestApp(Pecan(RootController())) r = app.get('/foo/bar', expect_errors=True) assert r.status_int == 404 def test_lookup_with_wrong_return(self): class RootController(object): @expose() def _lookup(self, someID, *remainder): return 1 app = TestApp(Pecan(RootController())) self.assertRaises(TypeError, app.get, '/foo/bar', expect_errors=True) class TestCanonicalLookups(PecanTestCase): @property def app_(self): class LookupController(object): def __init__(self, someID): self.someID = someID @expose() def index(self): return self.someID class UserController(object): @expose() def _lookup(self, someID, *remainder): return LookupController(someID), remainder class RootController(object): users = UserController() return TestApp(Pecan(RootController())) def test_canonical_lookup(self): assert self.app_.get('/users', expect_errors=404).status_int == 404 assert self.app_.get('/users/', expect_errors=404).status_int == 404 assert self.app_.get('/users/100').status_int == 302 assert self.app_.get('/users/100/').body == b'100' class TestControllerArguments(PecanTestCase): @property def app_(self): class RootController(object): @expose() def index(self, id): return 'index: %s' % id @expose() def multiple(self, one, two): return 'multiple: %s, %s' % (one, two) @expose() def optional(self, id=None): return 'optional: %s' % str(id) @expose() def multiple_optional(self, one=None, two=None, three=None): return 'multiple_optional: %s, %s, %s' % (one, two, three) @expose() def variable_args(self, *args): return 'variable_args: %s' % ', '.join(args) @expose() def variable_kwargs(self, **kwargs): data = [ '%s=%s' % (key, kwargs[key]) for key in sorted(kwargs.keys()) ] return 'variable_kwargs: %s' % ', '.join(data) @expose() def variable_all(self, *args, **kwargs): data = [ '%s=%s' % (key, kwargs[key]) for key in sorted(kwargs.keys()) ] return 'variable_all: %s' % ', '.join(list(args) + data) @expose() def eater(self, id, dummy=None, *args, **kwargs): data = [ '%s=%s' % (key, kwargs[key]) for key in sorted(kwargs.keys()) ] return 'eater: %s, %s, %s' % ( id, dummy, ', '.join(list(args) + data) ) @staticmethod @expose() def static(id): return "id is %s" % id @expose() def _route(self, args, request): if hasattr(self, args[0]): return getattr(self, args[0]), args[1:] else: return self.index, args return TestApp(Pecan(RootController())) def test_required_argument(self): try: r = self.app_.get('/') assert r.status_int != 200 # pragma: nocover except Exception as ex: assert type(ex) == TypeError assert ex.args[0] in ( "index() takes exactly 2 arguments (1 given)", "index() missing 1 required positional argument: 'id'", ( "TestControllerArguments.app_..RootController." "index() missing 1 required positional argument: 'id'" ), ) # this messaging changed in Python 3.3 and again in Python 3.10 def test_single_argument(self): r = self.app_.get('/1') assert r.status_int == 200 assert r.body == b'index: 1' def test_single_argument_with_encoded_url(self): r = self.app_.get('/This%20is%20a%20test%21') assert r.status_int == 200 assert r.body == b'index: This is a test!' def test_single_argument_with_plus(self): r = self.app_.get('/foo+bar') assert r.status_int == 200 assert r.body == b'index: foo+bar' def test_single_argument_with_encoded_plus(self): r = self.app_.get('/foo%2Bbar') assert r.status_int == 200 assert r.body == b'index: foo+bar' def test_two_arguments(self): r = self.app_.get('/1/dummy', status=404) assert r.status_int == 404 def test_keyword_argument(self): r = self.app_.get('/?id=2') assert r.status_int == 200 assert r.body == b'index: 2' def test_keyword_argument_with_encoded_url(self): r = self.app_.get('/?id=This%20is%20a%20test%21') assert r.status_int == 200 assert r.body == b'index: This is a test!' def test_keyword_argument_with_plus(self): r = self.app_.get('/?id=foo+bar') assert r.status_int == 200 assert r.body == b'index: foo bar' def test_keyword_argument_with_encoded_plus(self): r = self.app_.get('/?id=foo%2Bbar') assert r.status_int == 200 assert r.body == b'index: foo+bar' def test_argument_and_keyword_argument(self): r = self.app_.get('/3?id=three') assert r.status_int == 200 assert r.body == b'index: 3' def test_encoded_argument_and_keyword_argument(self): r = self.app_.get('/This%20is%20a%20test%21?id=three') assert r.status_int == 200 assert r.body == b'index: This is a test!' def test_explicit_kwargs(self): r = self.app_.post('/', {'id': '4'}) assert r.status_int == 200 assert r.body == b'index: 4' def test_path_with_explicit_kwargs(self): r = self.app_.post('/4', {'id': 'four'}) assert r.status_int == 200 assert r.body == b'index: 4' def test_explicit_json_kwargs(self): r = self.app_.post_json('/', {'id': '4'}) assert r.status_int == 200 assert r.body == b'index: 4' def test_path_with_explicit_json_kwargs(self): r = self.app_.post_json('/4', {'id': 'four'}) assert r.status_int == 200 assert r.body == b'index: 4' def test_multiple_kwargs(self): r = self.app_.get('/?id=5&dummy=dummy') assert r.status_int == 200 assert r.body == b'index: 5' def test_kwargs_from_root(self): r = self.app_.post('/', {'id': '6', 'dummy': 'dummy'}) assert r.status_int == 200 assert r.body == b'index: 6' def test_json_kwargs_from_root(self): r = self.app_.post_json('/', {'id': '6', 'dummy': 'dummy'}) assert r.status_int == 200 assert r.body == b'index: 6' # multiple args def test_multiple_positional_arguments(self): r = self.app_.get('/multiple/one/two') assert r.status_int == 200 assert r.body == b'multiple: one, two' def test_multiple_positional_arguments_with_url_encode(self): r = self.app_.get('/multiple/One%20/Two%21') assert r.status_int == 200 assert r.body == b'multiple: One , Two!' def test_multiple_positional_arguments_with_kwargs(self): r = self.app_.get('/multiple?one=three&two=four') assert r.status_int == 200 assert r.body == b'multiple: three, four' def test_multiple_positional_arguments_with_url_encoded_kwargs(self): r = self.app_.get('/multiple?one=Three%20&two=Four%20%21') assert r.status_int == 200 assert r.body == b'multiple: Three , Four !' def test_positional_args_with_dictionary_kwargs(self): r = self.app_.post('/multiple', {'one': 'five', 'two': 'six'}) assert r.status_int == 200 assert r.body == b'multiple: five, six' def test_positional_args_with_json_kwargs(self): r = self.app_.post_json('/multiple', {'one': 'five', 'two': 'six'}) assert r.status_int == 200 assert r.body == b'multiple: five, six' def test_positional_args_with_url_encoded_dictionary_kwargs(self): r = self.app_.post('/multiple', {'one': 'Five%20', 'two': 'Six%20%21'}) assert r.status_int == 200 assert r.body == b'multiple: Five%20, Six%20%21' # optional arg def test_optional_arg(self): r = self.app_.get('/optional') assert r.status_int == 200 assert r.body == b'optional: None' def test_multiple_optional(self): r = self.app_.get('/optional/1') assert r.status_int == 200 assert r.body == b'optional: 1' def test_multiple_optional_url_encoded(self): r = self.app_.get('/optional/Some%20Number') assert r.status_int == 200 assert r.body == b'optional: Some Number' def test_multiple_optional_missing(self): r = self.app_.get('/optional/2/dummy', status=404) assert r.status_int == 404 def test_multiple_with_kwargs(self): r = self.app_.get('/optional?id=2') assert r.status_int == 200 assert r.body == b'optional: 2' def test_multiple_with_url_encoded_kwargs(self): r = self.app_.get('/optional?id=Some%20Number') assert r.status_int == 200 assert r.body == b'optional: Some Number' def test_multiple_args_with_url_encoded_kwargs(self): r = self.app_.get('/optional/3?id=three') assert r.status_int == 200 assert r.body == b'optional: 3' def test_url_encoded_positional_args(self): r = self.app_.get('/optional/Some%20Number?id=three') assert r.status_int == 200 assert r.body == b'optional: Some Number' def test_optional_arg_with_kwargs(self): r = self.app_.post('/optional', {'id': '4'}) assert r.status_int == 200 assert r.body == b'optional: 4' def test_optional_arg_with_json_kwargs(self): r = self.app_.post_json('/optional', {'id': '4'}) assert r.status_int == 200 assert r.body == b'optional: 4' def test_optional_arg_with_url_encoded_kwargs(self): r = self.app_.post('/optional', {'id': 'Some%20Number'}) assert r.status_int == 200 assert r.body == b'optional: Some%20Number' def test_multiple_positional_arguments_with_dictionary_kwargs(self): r = self.app_.post('/optional/5', {'id': 'five'}) assert r.status_int == 200 assert r.body == b'optional: 5' def test_multiple_positional_arguments_with_json_kwargs(self): r = self.app_.post_json('/optional/5', {'id': 'five'}) assert r.status_int == 200 assert r.body == b'optional: 5' def test_multiple_positional_url_encoded_arguments_with_kwargs(self): r = self.app_.post('/optional/Some%20Number', {'id': 'five'}) assert r.status_int == 200 assert r.body == b'optional: Some Number' def test_optional_arg_with_multiple_kwargs(self): r = self.app_.get('/optional?id=6&dummy=dummy') assert r.status_int == 200 assert r.body == b'optional: 6' def test_optional_arg_with_multiple_url_encoded_kwargs(self): r = self.app_.get('/optional?id=Some%20Number&dummy=dummy') assert r.status_int == 200 assert r.body == b'optional: Some Number' def test_optional_arg_with_multiple_dictionary_kwargs(self): r = self.app_.post('/optional', {'id': '7', 'dummy': 'dummy'}) assert r.status_int == 200 assert r.body == b'optional: 7' def test_optional_arg_with_multiple_json_kwargs(self): r = self.app_.post_json('/optional', {'id': '7', 'dummy': 'dummy'}) assert r.status_int == 200 assert r.body == b'optional: 7' def test_optional_arg_with_multiple_url_encoded_dictionary_kwargs(self): r = self.app_.post('/optional', { 'id': 'Some%20Number', 'dummy': 'dummy' }) assert r.status_int == 200 assert r.body == b'optional: Some%20Number' # multiple optional args def test_multiple_optional_positional_args(self): r = self.app_.get('/multiple_optional') assert r.status_int == 200 assert r.body == b'multiple_optional: None, None, None' def test_multiple_optional_positional_args_one_arg(self): r = self.app_.get('/multiple_optional/1') assert r.status_int == 200 assert r.body == b'multiple_optional: 1, None, None' def test_multiple_optional_positional_args_one_url_encoded_arg(self): r = self.app_.get('/multiple_optional/One%21') assert r.status_int == 200 assert r.body == b'multiple_optional: One!, None, None' def test_multiple_optional_positional_args_all_args(self): r = self.app_.get('/multiple_optional/1/2/3') assert r.status_int == 200 assert r.body == b'multiple_optional: 1, 2, 3' def test_multiple_optional_positional_args_all_url_encoded_args(self): r = self.app_.get('/multiple_optional/One%21/Two%21/Three%21') assert r.status_int == 200 assert r.body == b'multiple_optional: One!, Two!, Three!' def test_multiple_optional_positional_args_too_many_args(self): r = self.app_.get('/multiple_optional/1/2/3/dummy', status=404) assert r.status_int == 404 def test_multiple_optional_positional_args_with_kwargs(self): r = self.app_.get('/multiple_optional?one=1') assert r.status_int == 200 assert r.body == b'multiple_optional: 1, None, None' def test_multiple_optional_positional_args_with_url_encoded_kwargs(self): r = self.app_.get('/multiple_optional?one=One%21') assert r.status_int == 200 assert r.body == b'multiple_optional: One!, None, None' def test_multiple_optional_positional_args_with_string_kwargs(self): r = self.app_.get('/multiple_optional/1?one=one') assert r.status_int == 200 assert r.body == b'multiple_optional: 1, None, None' def test_multiple_optional_positional_args_with_encoded_str_kwargs(self): r = self.app_.get('/multiple_optional/One%21?one=one') assert r.status_int == 200 assert r.body == b'multiple_optional: One!, None, None' def test_multiple_optional_positional_args_with_dict_kwargs(self): r = self.app_.post('/multiple_optional', {'one': '1'}) assert r.status_int == 200 assert r.body == b'multiple_optional: 1, None, None' def test_multiple_optional_positional_args_with_json_kwargs(self): r = self.app_.post_json('/multiple_optional', {'one': '1'}) assert r.status_int == 200 assert r.body == b'multiple_optional: 1, None, None' def test_multiple_optional_positional_args_with_encoded_dict_kwargs(self): r = self.app_.post('/multiple_optional', {'one': 'One%21'}) assert r.status_int == 200 assert r.body == b'multiple_optional: One%21, None, None' def test_multiple_optional_positional_args_and_dict_kwargs(self): r = self.app_.post('/multiple_optional/1', {'one': 'one'}) assert r.status_int == 200 assert r.body == b'multiple_optional: 1, None, None' def test_multiple_optional_positional_args_and_json_kwargs(self): r = self.app_.post_json('/multiple_optional/1', {'one': 'one'}) assert r.status_int == 200 assert r.body == b'multiple_optional: 1, None, None' def test_multiple_optional_encoded_positional_args_and_dict_kwargs(self): r = self.app_.post('/multiple_optional/One%21', {'one': 'one'}) assert r.status_int == 200 assert r.body == b'multiple_optional: One!, None, None' def test_multiple_optional_args_with_multiple_kwargs(self): r = self.app_.get('/multiple_optional?one=1&two=2&three=3&four=4') assert r.status_int == 200 assert r.body == b'multiple_optional: 1, 2, 3' def test_multiple_optional_args_with_multiple_encoded_kwargs(self): r = self.app_.get( '/multiple_optional?one=One%21&two=Two%21&three=Three%21&four=4' ) assert r.status_int == 200 assert r.body == b'multiple_optional: One!, Two!, Three!' def test_multiple_optional_args_with_multiple_dict_kwargs(self): r = self.app_.post( '/multiple_optional', {'one': '1', 'two': '2', 'three': '3', 'four': '4'} ) assert r.status_int == 200 assert r.body == b'multiple_optional: 1, 2, 3' def test_multiple_optional_args_with_multiple_json_kwargs(self): r = self.app_.post_json( '/multiple_optional', {'one': '1', 'two': '2', 'three': '3', 'four': '4'} ) assert r.status_int == 200 assert r.body == b'multiple_optional: 1, 2, 3' def test_multiple_optional_args_with_multiple_encoded_dict_kwargs(self): r = self.app_.post( '/multiple_optional', { 'one': 'One%21', 'two': 'Two%21', 'three': 'Three%21', 'four': '4' } ) assert r.status_int == 200 assert r.body == b'multiple_optional: One%21, Two%21, Three%21' def test_multiple_optional_args_with_last_kwarg(self): r = self.app_.get('/multiple_optional?three=3') assert r.status_int == 200 assert r.body == b'multiple_optional: None, None, 3' def test_multiple_optional_args_with_last_encoded_kwarg(self): r = self.app_.get('/multiple_optional?three=Three%21') assert r.status_int == 200 assert r.body == b'multiple_optional: None, None, Three!' def test_multiple_optional_args_with_middle_arg(self): r = self.app_.get('/multiple_optional', {'two': '2'}) assert r.status_int == 200 assert r.body == b'multiple_optional: None, 2, None' def test_variable_args(self): r = self.app_.get('/variable_args') assert r.status_int == 200 assert r.body == b'variable_args: ' def test_multiple_variable_args(self): r = self.app_.get('/variable_args/1/dummy') assert r.status_int == 200 assert r.body == b'variable_args: 1, dummy' def test_multiple_encoded_variable_args(self): r = self.app_.get('/variable_args/Testing%20One%20Two/Three%21') assert r.status_int == 200 assert r.body == b'variable_args: Testing One Two, Three!' def test_variable_args_with_kwargs(self): r = self.app_.get('/variable_args?id=2&dummy=dummy') assert r.status_int == 200 assert r.body == b'variable_args: ' def test_variable_args_with_dict_kwargs(self): r = self.app_.post('/variable_args', {'id': '3', 'dummy': 'dummy'}) assert r.status_int == 200 assert r.body == b'variable_args: ' def test_variable_args_with_json_kwargs(self): r = self.app_.post_json( '/variable_args', {'id': '3', 'dummy': 'dummy'} ) assert r.status_int == 200 assert r.body == b'variable_args: ' def test_variable_kwargs(self): r = self.app_.get('/variable_kwargs') assert r.status_int == 200 assert r.body == b'variable_kwargs: ' def test_multiple_variable_kwargs(self): r = self.app_.get('/variable_kwargs/1/dummy', status=404) assert r.status_int == 404 def test_multiple_variable_kwargs_with_explicit_kwargs(self): r = self.app_.get('/variable_kwargs?id=2&dummy=dummy') assert r.status_int == 200 assert r.body == b'variable_kwargs: dummy=dummy, id=2' def test_multiple_variable_kwargs_with_explicit_encoded_kwargs(self): r = self.app_.get( '/variable_kwargs?id=Two%21&dummy=This%20is%20a%20test' ) assert r.status_int == 200 assert r.body == b'variable_kwargs: dummy=This is a test, id=Two!' def test_multiple_variable_kwargs_with_dict_kwargs(self): r = self.app_.post('/variable_kwargs', {'id': '3', 'dummy': 'dummy'}) assert r.status_int == 200 assert r.body == b'variable_kwargs: dummy=dummy, id=3' def test_multiple_variable_kwargs_with_json_kwargs(self): r = self.app_.post_json( '/variable_kwargs', {'id': '3', 'dummy': 'dummy'} ) assert r.status_int == 200 assert r.body == b'variable_kwargs: dummy=dummy, id=3' def test_multiple_variable_kwargs_with_encoded_dict_kwargs(self): r = self.app_.post( '/variable_kwargs', {'id': 'Three%21', 'dummy': 'This%20is%20a%20test'} ) assert r.status_int == 200 result = b'variable_kwargs: dummy=This%20is%20a%20test, id=Three%21' assert r.body == result def test_variable_all(self): r = self.app_.get('/variable_all') assert r.status_int == 200 assert r.body == b'variable_all: ' def test_variable_all_with_one_extra(self): r = self.app_.get('/variable_all/1') assert r.status_int == 200 assert r.body == b'variable_all: 1' def test_variable_all_with_two_extras(self): r = self.app_.get('/variable_all/2/dummy') assert r.status_int == 200 assert r.body == b'variable_all: 2, dummy' def test_variable_mixed(self): r = self.app_.get('/variable_all/3?month=1&day=12') assert r.status_int == 200 assert r.body == b'variable_all: 3, day=12, month=1' def test_variable_mixed_explicit(self): r = self.app_.get('/variable_all/4?id=four&month=1&day=12') assert r.status_int == 200 assert r.body == b'variable_all: 4, day=12, id=four, month=1' def test_variable_post(self): r = self.app_.post('/variable_all/5/dummy') assert r.status_int == 200 assert r.body == b'variable_all: 5, dummy' def test_variable_post_with_kwargs(self): r = self.app_.post('/variable_all/6', {'month': '1', 'day': '12'}) assert r.status_int == 200 assert r.body == b'variable_all: 6, day=12, month=1' def test_variable_post_with_json_kwargs(self): r = self.app_.post_json( '/variable_all/6', {'month': '1', 'day': '12'} ) assert r.status_int == 200 assert r.body == b'variable_all: 6, day=12, month=1' def test_variable_post_mixed(self): r = self.app_.post( '/variable_all/7', {'id': 'seven', 'month': '1', 'day': '12'} ) assert r.status_int == 200 assert r.body == b'variable_all: 7, day=12, id=seven, month=1' def test_variable_post_mixed_with_json(self): r = self.app_.post_json( '/variable_all/7', {'id': 'seven', 'month': '1', 'day': '12'} ) assert r.status_int == 200 assert r.body == b'variable_all: 7, day=12, id=seven, month=1' def test_duplicate_query_parameters_GET(self): r = self.app_.get('/variable_kwargs?list=1&list=2') assert r.status_int == 200 assert r.body == b"variable_kwargs: list=['1', '2']" def test_duplicate_query_parameters_POST(self): r = self.app_.post('/variable_kwargs', {'list': ['1', '2']}) assert r.status_int == 200 assert r.body == b"variable_kwargs: list=['1', '2']" def test_duplicate_query_parameters_POST_mixed(self): r = self.app_.post('/variable_kwargs?list=1&list=2', {'list': ['3', '4']}) assert r.status_int == 200 assert r.body == b"variable_kwargs: list=['1', '2', '3', '4']" def test_duplicate_query_parameters_POST_mixed_json(self): r = self.app_.post('/variable_kwargs?list=1&list=2', {'list': 3}) assert r.status_int == 200 assert r.body == b"variable_kwargs: list=['1', '2', '3']" def test_staticmethod(self): r = self.app_.get('/static/foobar') assert r.status_int == 200 assert r.body == b'id is foobar' def test_no_remainder(self): try: r = self.app_.get('/eater') assert r.status_int != 200 # pragma: nocover except Exception as ex: assert type(ex) == TypeError assert ex.args[0] in ( "eater() takes exactly 2 arguments (1 given)", "eater() missing 1 required positional argument: 'id'", ( "TestControllerArguments.app_..RootController." "eater() missing 1 required positional argument: 'id'" ), ) # this messaging changed in Python 3.3 and again in Python 3.10 def test_one_remainder(self): r = self.app_.get('/eater/1') assert r.status_int == 200 assert r.body == b'eater: 1, None, ' def test_two_remainders(self): r = self.app_.get('/eater/2/dummy') assert r.status_int == 200 assert r.body == b'eater: 2, dummy, ' def test_many_remainders(self): r = self.app_.get('/eater/3/dummy/foo/bar') assert r.status_int == 200 assert r.body == b'eater: 3, dummy, foo, bar' def test_remainder_with_kwargs(self): r = self.app_.get('/eater/4?month=1&day=12') assert r.status_int == 200 assert r.body == b'eater: 4, None, day=12, month=1' def test_remainder_with_many_kwargs(self): r = self.app_.get('/eater/5?id=five&month=1&day=12&dummy=dummy') assert r.status_int == 200 assert r.body == b'eater: 5, dummy, day=12, month=1' def test_post_remainder(self): r = self.app_.post('/eater/6') assert r.status_int == 200 assert r.body == b'eater: 6, None, ' def test_post_three_remainders(self): r = self.app_.post('/eater/7/dummy') assert r.status_int == 200 assert r.body == b'eater: 7, dummy, ' def test_post_many_remainders(self): r = self.app_.post('/eater/8/dummy/foo/bar') assert r.status_int == 200 assert r.body == b'eater: 8, dummy, foo, bar' def test_post_remainder_with_kwargs(self): r = self.app_.post('/eater/9', {'month': '1', 'day': '12'}) assert r.status_int == 200 assert r.body == b'eater: 9, None, day=12, month=1' def test_post_empty_remainder_with_json_kwargs(self): r = self.app_.post_json('/eater/9/', {'month': '1', 'day': '12'}) assert r.status_int == 200 assert r.body == b'eater: 9, None, day=12, month=1' def test_post_remainder_with_json_kwargs(self): r = self.app_.post_json('/eater/9', {'month': '1', 'day': '12'}) assert r.status_int == 200 assert r.body == b'eater: 9, None, day=12, month=1' def test_post_many_remainders_with_many_kwargs(self): r = self.app_.post( '/eater/10', {'id': 'ten', 'month': '1', 'day': '12', 'dummy': 'dummy'} ) assert r.status_int == 200 assert r.body == b'eater: 10, dummy, day=12, month=1' def test_post_many_remainders_with_many_json_kwargs(self): r = self.app_.post_json( '/eater/10', {'id': 'ten', 'month': '1', 'day': '12', 'dummy': 'dummy'} ) assert r.status_int == 200 assert r.body == b'eater: 10, dummy, day=12, month=1' class TestDefaultErrorRendering(PecanTestCase): def test_plain_error(self): class RootController(object): pass app = TestApp(Pecan(RootController())) r = app.get('/', status=404) assert r.status_int == 404 assert r.content_type == 'text/plain' assert r.body == HTTPNotFound().plain_body({}).encode('utf-8') def test_html_error(self): class RootController(object): pass app = TestApp(Pecan(RootController())) r = app.get('/', headers={'Accept': 'text/html'}, status=404) assert r.status_int == 404 assert r.content_type == 'text/html' assert r.body == HTTPNotFound().html_body({}).encode('utf-8') def test_json_error(self): class RootController(object): pass app = TestApp(Pecan(RootController())) r = app.get('/', headers={'Accept': 'application/json'}, status=404) assert r.status_int == 404 json_resp = json.loads(r.body.decode()) assert json_resp['code'] == 404 assert json_resp['description'] is None assert json_resp['title'] == 'Not Found' assert r.content_type == 'application/json' class TestAbort(PecanTestCase): def test_abort(self): class RootController(object): @expose() def index(self): abort(404) app = TestApp(Pecan(RootController())) r = app.get('/', status=404) assert r.status_int == 404 def test_abort_with_detail(self): class RootController(object): @expose() def index(self): abort(status_code=401, detail='Not Authorized') app = TestApp(Pecan(RootController())) r = app.get('/', status=401) assert r.status_int == 401 def test_abort_keeps_traceback(self): last_exc, last_traceback = None, None try: try: raise Exception('Bottom Exception') except: abort(404) except Exception: last_exc, _, last_traceback = sys.exc_info() assert last_exc is HTTPNotFound assert 'Bottom Exception' in traceback.format_tb(last_traceback)[-1] class TestScriptName(PecanTestCase): def setUp(self): super(TestScriptName, self).setUp() self.environ = {'SCRIPT_NAME': '/foo'} def test_handle_script_name(self): class RootController(object): @expose() def index(self): return 'Root Index' app = TestApp(Pecan(RootController()), extra_environ=self.environ) r = app.get('/foo/') assert r.status_int == 200 class TestRedirect(PecanTestCase): @property def app_(self): class RootController(object): @expose() def index(self): redirect('/testing') @expose() def internal(self): redirect('/testing', internal=True) @expose() def bad_internal(self): redirect('/testing', internal=True, code=301) @expose() def permanent(self): redirect('/testing', code=301) @expose() def testing(self): return 'it worked!' return TestApp(make_app(RootController(), debug=False)) def test_index(self): r = self.app_.get('/') assert r.status_int == 302 r = r.follow() assert r.status_int == 200 assert r.body == b'it worked!' def test_internal(self): r = self.app_.get('/internal') assert r.status_int == 200 assert r.body == b'it worked!' def test_internal_with_301(self): self.assertRaises(ValueError, self.app_.get, '/bad_internal') def test_permanent_redirect(self): r = self.app_.get('/permanent') assert r.status_int == 301 r = r.follow() assert r.status_int == 200 assert r.body == b'it worked!' def test_x_forward_proto(self): class ChildController(object): @expose() def index(self): redirect('/testing') # pragma: nocover class RootController(object): @expose() def index(self): redirect('/testing') # pragma: nocover @expose() def testing(self): return 'it worked!' # pragma: nocover child = ChildController() app = TestApp(make_app(RootController(), debug=True)) res = app.get( '/child', extra_environ=dict(HTTP_X_FORWARDED_PROTO='https') ) # non-canonical url will redirect, so we won't get a 301 assert res.status_int == 302 # should add trailing / and changes location to https assert res.location == 'https://localhost/child/' assert res.request.environ['HTTP_X_FORWARDED_PROTO'] == 'https' class TestInternalRedirectContext(PecanTestCase): @property def app_(self): class RootController(object): @expose() def redirect_with_context(self): request.context['foo'] = 'bar' redirect('/testing') @expose() def internal_with_context(self): request.context['foo'] = 'bar' redirect('/testing', internal=True) @expose('json') def testing(self): return request.context return TestApp(make_app(RootController(), debug=False)) def test_internal_with_request_context(self): r = self.app_.get('/internal_with_context') assert r.status_int == 200 assert json.loads(r.body.decode()) == {'foo': 'bar'} def test_context_does_not_bleed(self): r = self.app_.get('/redirect_with_context').follow() assert r.status_int == 200 assert json.loads(r.body.decode()) == {} class TestStreamedResponse(PecanTestCase): def test_streaming_response(self): class RootController(object): @expose(content_type='text/plain') def test(self, foo): if foo == 'stream': # mimic large file contents = BytesIO(b'stream') response.content_type = 'application/octet-stream' contents.seek(0, os.SEEK_END) response.content_length = contents.tell() contents.seek(0, os.SEEK_SET) response.app_iter = contents return response else: return 'plain text' app = TestApp(Pecan(RootController())) r = app.get('/test/stream') assert r.content_type == 'application/octet-stream' assert r.body == b'stream' r = app.get('/test/plain') assert r.content_type == 'text/plain' assert r.body == b'plain text' class TestManualResponse(PecanTestCase): def test_manual_response(self): class RootController(object): @expose() def index(self): resp = webob.Response(response.environ) resp.body = b'Hello, World!' return resp app = TestApp(Pecan(RootController())) r = app.get('/') assert r.body == b'Hello, World!' class TestCustomResponseandRequest(PecanTestCase): def test_custom_objects(self): class CustomRequest(Request): @property def headers(self): headers = super(CustomRequest, self).headers headers['X-Custom-Request'] = 'ABC' return headers class CustomResponse(Response): @property def headers(self): headers = super(CustomResponse, self).headers headers['X-Custom-Response'] = 'XYZ' return headers class RootController(object): @expose() def index(self): return request.headers.get('X-Custom-Request') app = TestApp(Pecan( RootController(), request_cls=CustomRequest, response_cls=CustomResponse )) r = app.get('/') assert r.body == b'ABC' assert r.headers.get('X-Custom-Response') == 'XYZ' class TestThreadLocalState(PecanTestCase): def test_thread_local_dir(self): """ Threadlocal proxies for request and response should properly proxy ``dir()`` calls to the underlying webob class. """ class RootController(object): @expose() def index(self): assert 'method' in dir(request) assert 'status' in dir(response) return '/' app = TestApp(Pecan(RootController())) r = app.get('/') assert r.status_int == 200 assert r.body == b'/' def test_request_state_cleanup(self): """ After a request, the state local() should be totally clean except for state.app (so that objects don't leak between requests) """ from pecan.core import state class RootController(object): @expose() def index(self): return '/' app = TestApp(Pecan(RootController())) r = app.get('/') assert r.status_int == 200 assert r.body == b'/' assert state.__dict__ == {} class TestFileTypeExtensions(PecanTestCase): @property def app_(self): """ Test extension splits """ class RootController(object): @expose(content_type=None) def _default(self, *args): ext = request.pecan['extension'] assert len(args) == 1 if ext: assert ext not in args[0] return ext or '' return TestApp(Pecan(RootController())) def test_html_extension(self): for path in ('/index.html', '/index.html/'): r = self.app_.get(path) assert r.status_int == 200 assert r.body == b'.html' def test_image_extension(self): for path in ('/index.png', '/index.png/'): r = self.app_.get(path) assert r.status_int == 200 assert r.body == b'.png' def test_hidden_file(self): for path in ('/.vimrc', '/.vimrc/'): r = self.app_.get(path) assert r.status_int == 204 assert r.body == b'' def test_multi_dot_extension(self): for path in ('/gradient.min.js', '/gradient.min.js/'): r = self.app_.get(path) assert r.status_int == 200 assert r.body == b'.js' def test_bad_content_type(self): class RootController(object): @expose() def index(self): return '/' app = TestApp(Pecan(RootController())) r = app.get('/') assert r.status_int == 200 assert r.body == b'/' r = app.get('/index.html', expect_errors=True) assert r.status_int == 200 assert r.body == b'/' with warnings.catch_warnings(): warnings.simplefilter("ignore") r = app.get('/index.txt', expect_errors=True) assert r.status_int == 404 def test_unknown_file_extension(self): class RootController(object): @expose(content_type=None) def _default(self, *args): assert 'example:x.tiny' in args assert request.pecan['extension'] is None return 'SOME VALUE' app = TestApp(Pecan(RootController())) r = app.get('/example:x.tiny') assert r.status_int == 200 assert r.body == b'SOME VALUE' def test_guessing_disabled(self): class RootController(object): @expose(content_type=None) def _default(self, *args): assert 'index.html' in args assert request.pecan['extension'] is None return 'SOME VALUE' app = TestApp(Pecan(RootController(), guess_content_type_from_ext=False)) r = app.get('/index.html') assert r.status_int == 200 assert r.body == b'SOME VALUE' def test_content_type_guessing_disabled(self): class ResourceController(object): def __init__(self, name): self.name = name assert self.name == 'file.html' @expose('json') def index(self): return dict(name=self.name) class RootController(object): @expose() def _lookup(self, name, *remainder): return ResourceController(name), remainder app = TestApp( Pecan(RootController(), guess_content_type_from_ext=False) ) r = app.get('/file.html/') assert r.status_int == 200 result = dict(json.loads(r.body.decode())) assert result == {'name': 'file.html'} r = app.get('/file.html') assert r.status_int == 302 r = r.follow() result = dict(json.loads(r.body.decode())) assert result == {'name': 'file.html'} class TestContentTypeByAcceptHeaders(PecanTestCase): @property def app_(self): """ Test that content type is set appropriately based on Accept headers. """ class RootController(object): @expose(content_type='text/html') @expose(content_type='application/json') def index(self, *args): return 'Foo' return TestApp(Pecan(RootController())) def test_missing_accept(self): r = self.app_.get('/', headers={ 'Accept': '' }) assert r.status_int == 200 assert r.content_type == 'text/html' def test_quality(self): r = self.app_.get('/', headers={ 'Accept': 'text/html,application/json;q=0.9,*/*;q=0.8' }) assert r.status_int == 200 assert r.content_type == 'text/html' r = self.app_.get('/', headers={ 'Accept': 'application/json,text/html;q=0.9,*/*;q=0.8' }) assert r.status_int == 200 assert r.content_type == 'application/json' def test_discarded_accept_parameters(self): r = self.app_.get('/', headers={ 'Accept': 'application/json;discard=me' }) assert r.status_int == 200 assert r.content_type == 'application/json' def test_file_extension_has_higher_precedence(self): r = self.app_.get('/index.html', headers={ 'Accept': 'application/json,text/html;q=0.9,*/*;q=0.8' }) assert r.status_int == 200 assert r.content_type == 'text/html' def test_not_acceptable(self): r = self.app_.get('/', headers={ 'Accept': 'application/xml', }, status=406) assert r.status_int == 406 def test_accept_header_missing(self): r = self.app_.get('/') assert r.status_int == 200 assert r.content_type == 'text/html' class TestCanonicalRouting(PecanTestCase): @property def app_(self): class ArgSubController(object): @expose() def index(self, arg): return arg class AcceptController(object): @accept_noncanonical @expose() def index(self): return 'accept' class SubController(object): @expose() def index(self, **kw): return 'subindex' class RootController(object): @expose() def index(self): return 'index' sub = SubController() arg = ArgSubController() accept = AcceptController() return TestApp(Pecan(RootController())) def test_root(self): r = self.app_.get('/') assert r.status_int == 200 assert b'index' in r.body def test_index(self): r = self.app_.get('/index') assert r.status_int == 200 assert b'index' in r.body def test_broken_clients(self): # for broken clients r = self.app_.get('', status=302) assert r.status_int == 302 assert r.location == 'http://localhost/' def test_sub_controller_with_trailing(self): r = self.app_.get('/sub/') assert r.status_int == 200 assert b'subindex' in r.body def test_sub_controller_redirect(self): r = self.app_.get('/sub', status=302) assert r.status_int == 302 assert r.location == 'http://localhost/sub/' def test_with_query_string(self): # try with query string r = self.app_.get('/sub?foo=bar', status=302) assert r.status_int == 302 assert r.location == 'http://localhost/sub/?foo=bar' def test_posts_fail(self): try: self.app_.post('/sub', dict(foo=1)) raise Exception("Post should fail") # pragma: nocover except Exception as e: assert isinstance(e, RuntimeError) def test_with_args(self): r = self.app_.get('/arg/index/foo') assert r.status_int == 200 assert r.body == b'foo' def test_accept_noncanonical(self): r = self.app_.get('/accept/') assert r.status_int == 200 assert r.body == b'accept' def test_accept_noncanonical_no_trailing_slash(self): r = self.app_.get('/accept') assert r.status_int == 200 assert r.body == b'accept' class TestNonCanonical(PecanTestCase): @property def app_(self): class ArgSubController(object): @expose() def index(self, arg): return arg # pragma: nocover class AcceptController(object): @accept_noncanonical @expose() def index(self): return 'accept' # pragma: nocover class SubController(object): @expose() def index(self, **kw): return 'subindex' class RootController(object): @expose() def index(self): return 'index' sub = SubController() arg = ArgSubController() accept = AcceptController() return TestApp(Pecan(RootController(), force_canonical=False)) def test_index(self): r = self.app_.get('/') assert r.status_int == 200 assert b'index' in r.body def test_subcontroller(self): r = self.app_.get('/sub') assert r.status_int == 200 assert b'subindex' in r.body def test_subcontroller_with_kwargs(self): r = self.app_.post('/sub', dict(foo=1)) assert r.status_int == 200 assert b'subindex' in r.body def test_sub_controller_with_trailing(self): r = self.app_.get('/sub/') assert r.status_int == 200 assert b'subindex' in r.body def test_proxy(self): class RootController(object): @expose() def index(self): request.testing = True assert request.testing is True del request.testing assert hasattr(request, 'testing') is False return '/' app = TestApp(make_app(RootController(), debug=True)) r = app.get('/') assert r.status_int == 200 def test_app_wrap(self): class RootController(object): pass wrapped_apps = [] def wrap(app): wrapped_apps.append(app) return app make_app(RootController(), wrap_app=wrap, debug=True) assert len(wrapped_apps) == 1 class TestLogging(PecanTestCase): def test_logging_setup(self): class RootController(object): @expose() def index(self): import logging logging.getLogger('pecantesting').info('HELLO WORLD') return "HELLO WORLD" f = StringIO() app = TestApp(make_app(RootController(), logging={ 'loggers': { 'pecantesting': { 'level': 'INFO', 'handlers': ['memory'] } }, 'handlers': { 'memory': { 'level': 'INFO', 'class': 'logging.StreamHandler', 'stream': f } } })) app.get('/') assert f.getvalue() == 'HELLO WORLD\n' def test_logging_setup_with_config_obj(self): class RootController(object): @expose() def index(self): import logging logging.getLogger('pecantesting').info('HELLO WORLD') return "HELLO WORLD" f = StringIO() from pecan.configuration import conf_from_dict app = TestApp(make_app(RootController(), logging=conf_from_dict({ 'loggers': { 'pecantesting': { 'level': 'INFO', 'handlers': ['memory'] } }, 'handlers': { 'memory': { 'level': 'INFO', 'class': 'logging.StreamHandler', 'stream': f } } }))) app.get('/') assert f.getvalue() == 'HELLO WORLD\n' class TestEngines(PecanTestCase): template_path = os.path.join(os.path.dirname(__file__), 'templates') @unittest.skipIf('genshi' not in builtin_renderers, 'Genshi not installed') def test_genshi(self): class RootController(object): @expose('genshi:genshi.html') def index(self, name='Jonathan'): return dict(name=name) @expose('genshi:genshi_bad.html') def badtemplate(self): return dict() app = TestApp( Pecan(RootController(), template_path=self.template_path) ) r = app.get('/') assert r.status_int == 200 assert b"

Hello, Jonathan!

" in r.body r = app.get('/index.html?name=World') assert r.status_int == 200 assert b"

Hello, World!

" in r.body error_msg = None try: r = app.get('/badtemplate.html') except Exception as e: for error_f in error_formatters: error_msg = error_f(e) if error_msg: break assert error_msg is not None @unittest.skipIf('kajiki' not in builtin_renderers, 'Kajiki not installed') def test_kajiki(self): class RootController(object): @expose('kajiki:kajiki.html') def index(self, name='Jonathan'): return dict(name=name) app = TestApp( Pecan(RootController(), template_path=self.template_path) ) r = app.get('/') assert r.status_int == 200 assert b"

Hello, Jonathan!

" in r.body r = app.get('/index.html?name=World') assert r.status_int == 200 assert b"

Hello, World!

" in r.body @unittest.skipIf('jinja' not in builtin_renderers, 'Jinja not installed') def test_jinja(self): class RootController(object): @expose('jinja:jinja.html') def index(self, name='Jonathan'): return dict(name=name) @expose('jinja:jinja_bad.html') def badtemplate(self): return dict() app = TestApp( Pecan(RootController(), template_path=self.template_path) ) r = app.get('/') assert r.status_int == 200 assert b"

Hello, Jonathan!

" in r.body error_msg = None try: r = app.get('/badtemplate.html') except Exception as e: for error_f in error_formatters: error_msg = error_f(e) if error_msg: break assert error_msg is not None @unittest.skipIf('mako' not in builtin_renderers, 'Mako not installed') def test_mako(self): class RootController(object): @expose('mako:mako.html') def index(self, name='Jonathan'): return dict(name=name) @expose('mako:mako_bad.html') def badtemplate(self): return dict() app = TestApp( Pecan(RootController(), template_path=self.template_path) ) r = app.get('/') assert r.status_int == 200 assert b"

Hello, Jonathan!

" in r.body r = app.get('/index.html?name=World') assert r.status_int == 200 assert b"

Hello, World!

" in r.body error_msg = None try: r = app.get('/badtemplate.html') except Exception as e: for error_f in error_formatters: error_msg = error_f(e) if error_msg: break assert error_msg is not None def test_renderer_not_found(self): class RootController(object): @expose('mako3:mako.html') def index(self, name='Jonathan'): return dict(name=name) app = TestApp( Pecan(RootController(), template_path=self.template_path) ) try: r = app.get('/') except Exception as e: expected = e assert 'support for "mako3" was not found;' in str(expected) def test_json(self): expected_result = dict( name='Jonathan', age=30, nested=dict(works=True) ) class RootController(object): @expose('json') def index(self): return expected_result app = TestApp(Pecan(RootController())) r = app.get('/') assert r.status_int == 200 result = json.loads(r.body.decode()) assert result == expected_result def test_custom_renderer(self): class RootController(object): @expose('backwards:mako.html') def index(self, name='Joe'): return dict(name=name) class BackwardsRenderer(MakoRenderer): # Custom renderer that reverses all string namespace values def render(self, template_path, namespace): namespace = dict( (k, v[::-1]) for k, v in namespace.items() ) return super(BackwardsRenderer, self).render(template_path, namespace) app = TestApp(Pecan( RootController(), template_path=self.template_path, custom_renderers={'backwards': BackwardsRenderer} )) r = app.get('/') assert r.status_int == 200 assert b"

Hello, eoJ!

" in r.body r = app.get('/index.html?name=Tim') assert r.status_int == 200 assert b"

Hello, miT!

" in r.body def test_override_template(self): class RootController(object): @expose('foo.html') def index(self): override_template(None, content_type='text/plain') return 'Override' app = TestApp(Pecan(RootController())) r = app.get('/') assert r.status_int == 200 assert b'Override' in r.body assert r.content_type == 'text/plain' def test_render(self): class RootController(object): @expose() def index(self, name='Jonathan'): return render('mako.html', dict(name=name)) app = TestApp( Pecan(RootController(), template_path=self.template_path) ) r = app.get('/') assert r.status_int == 200 assert b"

Hello, Jonathan!

" in r.body def test_default_json_renderer(self): class RootController(object): @expose() def index(self, name='Bill'): return dict(name=name) app = TestApp(Pecan(RootController(), default_renderer='json')) r = app.get('/') assert r.status_int == 200 result = dict(json.loads(r.body.decode())) assert result == {'name': 'Bill'} def test_default_json_renderer_with_explicit_content_type(self): class RootController(object): @expose(content_type='text/plain') def index(self, name='Bill'): return name app = TestApp(Pecan(RootController(), default_renderer='json')) r = app.get('/') assert r.status_int == 200 assert r.body == b"Bill" class TestDeprecatedRouteMethod(PecanTestCase): @property def app_(self): class RootController(object): @expose() def index(self, *args): return ', '.join(args) @expose() def _route(self, args): return self.index, args return TestApp(Pecan(RootController())) def test_required_argument(self): with warnings.catch_warnings(): warnings.simplefilter("ignore") r = self.app_.get('/foo/bar/') assert r.status_int == 200 assert b'foo, bar' in r.body class TestExplicitRoute(PecanTestCase): def test_alternate_route(self): class RootController(object): @expose(route='some-path') def some_path(self): return 'Hello, World!' app = TestApp(Pecan(RootController())) r = app.get('/some-path/') assert r.status_int == 200 assert r.body == b'Hello, World!' r = app.get('/some_path/', expect_errors=True) assert r.status_int == 404 def test_manual_route(self): class SubController(object): @expose(route='some-path') def some_path(self): return 'Hello, World!' class RootController(object): pass route(RootController, 'some-controller', SubController()) app = TestApp(Pecan(RootController())) r = app.get('/some-controller/some-path/') assert r.status_int == 200 assert r.body == b'Hello, World!' r = app.get('/some-controller/some_path/', expect_errors=True) assert r.status_int == 404 def test_manual_route_conflict(self): class SubController(object): pass class RootController(object): @expose() def hello(self): return 'Hello, World!' self.assertRaises( RuntimeError, route, RootController, 'hello', SubController() ) def test_custom_route_on_index(self): class RootController(object): @expose(route='some-path') def index(self): return 'Hello, World!' app = TestApp(Pecan(RootController())) r = app.get('/some-path/') assert r.status_int == 200 assert r.body == b'Hello, World!' r = app.get('/') assert r.status_int == 200 assert r.body == b'Hello, World!' r = app.get('/index/', expect_errors=True) assert r.status_int == 404 def test_custom_route_with_attribute_conflict(self): class RootController(object): @expose(route='mock') def greet(self): return 'Hello, World!' @expose() def mock(self): return 'You are not worthy!' app = TestApp(Pecan(RootController())) self.assertRaises( RuntimeError, app.get, '/mock/' ) def test_conflicting_custom_routes(self): class RootController(object): @expose(route='testing') def foo(self): return 'Foo!' @expose(route='testing') def bar(self): return 'Bar!' app = TestApp(Pecan(RootController())) self.assertRaises( RuntimeError, app.get, '/testing/' ) def test_conflicting_custom_routes_in_subclass(self): class BaseController(object): @expose(route='testing') def foo(self): return request.path class ChildController(BaseController): pass class RootController(BaseController): child = ChildController() app = TestApp(Pecan(RootController())) r = app.get('/testing/') assert r.body == b'/testing/' r = app.get('/child/testing/') assert r.body == b'/child/testing/' def test_custom_route_prohibited_on_lookup(self): try: class RootController(object): @expose(route='some-path') def _lookup(self): return 'Hello, World!' except ValueError: pass else: raise AssertionError( '_lookup cannot be used with a custom path segment' ) def test_custom_route_prohibited_on_default(self): try: class RootController(object): @expose(route='some-path') def _default(self): return 'Hello, World!' except ValueError: pass else: raise AssertionError( '_default cannot be used with a custom path segment' ) def test_custom_route_prohibited_on_route(self): try: class RootController(object): @expose(route='some-path') def _route(self): return 'Hello, World!' except ValueError: pass else: raise AssertionError( '_route cannot be used with a custom path segment' ) def test_custom_route_with_generic_controllers(self): class RootController(object): @expose(route='some-path', generic=True) def foo(self): return 'Hello, World!' @foo.when(method='POST') def handle_post(self): return 'POST!' app = TestApp(Pecan(RootController())) r = app.get('/some-path/') assert r.status_int == 200 assert r.body == b'Hello, World!' r = app.get('/foo/', expect_errors=True) assert r.status_int == 404 r = app.post('/some-path/') assert r.status_int == 200 assert r.body == b'POST!' r = app.post('/foo/', expect_errors=True) assert r.status_int == 404 def test_custom_route_prohibited_on_generic_controllers(self): try: class RootController(object): @expose(generic=True) def foo(self): return 'Hello, World!' @foo.when(method='POST', route='some-path') def handle_post(self): return 'POST!' except ValueError: pass else: raise AssertionError( 'generic controllers cannot be used with a custom path segment' ) def test_invalid_route_arguments(self): class C(object): def secret(self): return {} self.assertRaises(TypeError, route) self.assertRaises(TypeError, route, 'some-path', lambda x: x) self.assertRaises(TypeError, route, 'some-path', C.secret) self.assertRaises(TypeError, route, C, {}, C()) for path in ( 'VARIED-case-PATH', 'this,custom,path', '123-path', 'path(with-parens)', 'path;with;semicolons', 'path:with:colons', 'v2.0', '~username', 'somepath!', 'four*four', 'one+two', '@twitterhandle', 'package=pecan' ): handler = C() route(C, path, handler) assert getattr(C, path, handler) self.assertRaises(ValueError, route, C, '/path/', C()) self.assertRaises(ValueError, route, C, '.', C()) self.assertRaises(ValueError, route, C, '..', C()) self.assertRaises(ValueError, route, C, 'path?', C()) self.assertRaises(ValueError, route, C, 'percent%20encoded', C()) pecan-1.5.1/pecan/tests/test_commands.py000066400000000000000000000031131445453044500202540ustar00rootroot00000000000000from pecan.tests import PecanTestCase class TestCommandManager(PecanTestCase): def test_commands(self): from pecan.commands import ServeCommand, ShellCommand, CreateCommand from pecan.commands.base import CommandManager m = CommandManager() assert m.commands['serve'] == ServeCommand assert m.commands['shell'] == ShellCommand assert m.commands['create'] == CreateCommand class TestCommandRunner(PecanTestCase): def test_commands(self): from pecan.commands import ( ServeCommand, ShellCommand, CreateCommand, CommandRunner ) runner = CommandRunner() assert runner.commands['serve'] == ServeCommand assert runner.commands['shell'] == ShellCommand assert runner.commands['create'] == CreateCommand def test_run(self): from pecan.commands import CommandRunner runner = CommandRunner() self.assertRaises( RuntimeError, runner.run, ['serve', 'missing_file.py'] ) class TestCreateCommand(PecanTestCase): def test_run(self): from pecan.commands import CreateCommand class FakeArg(object): project_name = 'default' template_name = 'default' class FakeScaffold(object): def copy_to(self, project_name): assert project_name == 'default' class FakeManager(object): scaffolds = { 'default': FakeScaffold } c = CreateCommand() c.manager = FakeManager() c.run(FakeArg()) pecan-1.5.1/pecan/tests/test_conf.py000066400000000000000000000307641445453044500174140ustar00rootroot00000000000000import os import tempfile import unittest from webtest import TestApp import pecan from pecan.tests import PecanTestCase __here__ = os.path.dirname(__file__) class TestConf(PecanTestCase): def test_update_config_fail_identifier(self): """Fail when naming does not pass correctness""" from pecan import configuration bad_dict = {'bad name': 'value'} self.assertRaises(ValueError, configuration.Config, bad_dict) def test_update_config_fail_message(self): """When failing, the __force_dict__ key is suggested""" from pecan import configuration bad_dict = {'bad name': 'value'} try: configuration.Config(bad_dict) except ValueError as error: assert "consider using the '__force_dict__'" in str(error) def test_update_set_config(self): """Update an empty configuration with the default values""" from pecan import configuration conf = configuration.initconf() conf.update(configuration.conf_from_file(os.path.join( __here__, 'config_fixtures/config.py' ))) self.assertEqual(conf.app.root, None) self.assertEqual(conf.app.template_path, 'myproject/templates') self.assertEqual(conf.app.static_root, 'public') self.assertEqual(conf.server.host, '1.1.1.1') self.assertEqual(conf.server.port, '8081') def test_update_set_default_config(self): """Update an empty configuration with the default values""" from pecan import configuration conf = configuration.initconf() conf.update(configuration.conf_from_file(os.path.join( __here__, 'config_fixtures/empty.py' ))) self.assertEqual(conf.app.root, None) self.assertEqual(conf.app.template_path, '') self.assertEqual(conf.app.static_root, 'public') self.assertEqual(conf.server.host, '0.0.0.0') self.assertEqual(conf.server.port, '8080') def test_update_force_dict(self): """Update an empty configuration with the default values""" from pecan import configuration conf = configuration.initconf() conf.update(configuration.conf_from_file(os.path.join( __here__, 'config_fixtures/forcedict.py' ))) self.assertEqual(conf.app.root, None) self.assertEqual(conf.app.template_path, '') self.assertEqual(conf.app.static_root, 'public') self.assertEqual(conf.server.host, '0.0.0.0') self.assertEqual(conf.server.port, '8080') self.assertTrue(isinstance(conf.beaker, dict)) self.assertEqual(conf.beaker['session.key'], 'key') self.assertEqual(conf.beaker['session.type'], 'cookie') self.assertEqual( conf.beaker['session.validate_key'], '1a971a7df182df3e1dec0af7c6913ec7' ) self.assertEqual(conf.beaker.get('__force_dict__'), None) def test_update_config_with_dict(self): from pecan import configuration conf = configuration.initconf() d = {'attr': True} conf['attr'] = d self.assertTrue(conf.attr.attr) def test_config_repr(self): from pecan import configuration conf = configuration.Config({'a': 1}) self.assertEqual(repr(conf), "Config({'a': 1})") def test_config_from_dict(self): from pecan import configuration conf = configuration.conf_from_dict({}) conf['path'] = '%(confdir)s' self.assertTrue(os.path.samefile(conf['path'], os.getcwd())) def test_config_from_file(self): from pecan import configuration path = os.path.join( os.path.dirname(__file__), 'config_fixtures', 'config.py' ) configuration.conf_from_file(path) def test_config_illegal_ids(self): from pecan import configuration conf = configuration.Config({}) conf.update(configuration.conf_from_file(os.path.join( __here__, 'config_fixtures/bad/module_and_underscore.py' ))) self.assertEqual([], list(conf)) def test_config_missing_file(self): from pecan import configuration path = ('doesnotexist.py',) configuration.Config({}) self.assertRaises( RuntimeError, configuration.conf_from_file, os.path.join(__here__, 'config_fixtures', *path) ) def test_config_missing_file_on_path(self): from pecan import configuration path = ('bad', 'bad', 'doesnotexist.py',) configuration.Config({}) self.assertRaises( RuntimeError, configuration.conf_from_file, os.path.join(__here__, 'config_fixtures', *path) ) def test_config_with_syntax_error(self): from pecan import configuration with tempfile.NamedTemporaryFile('wb') as f: f.write(b'\n'.join([b'if false', b'var = 3'])) f.flush() configuration.Config({}) self.assertRaises( SyntaxError, configuration.conf_from_file, f.name ) def test_config_with_non_package_relative_import(self): from pecan import configuration with tempfile.NamedTemporaryFile('wb', suffix='.py') as f: f.write(b'\n'.join([b'from . import variables'])) f.flush() configuration.Config({}) try: configuration.conf_from_file(f.name) except (ValueError, SystemError, ImportError) as e: assert 'relative import' in str(e) else: raise AssertionError( "A relative import-related error should have been raised" ) def test_config_with_bad_import(self): from pecan import configuration path = ('bad', 'importerror.py') configuration.Config({}) self.assertRaises( ImportError, configuration.conf_from_file, os.path.join( __here__, 'config_fixtures', *path ) ) def test_config_dir(self): from pecan import configuration conf = configuration.Config({}) self.assertEqual([], dir(conf)) conf = configuration.Config({'a': 1}) self.assertEqual(['a'], dir(conf)) def test_config_bad_key(self): from pecan import configuration conf = configuration.Config({'a': 1}) assert conf.a == 1 self.assertRaises(AttributeError, getattr, conf, 'b') def test_config_get_valid_key(self): from pecan import configuration conf = configuration.Config({'a': 1}) assert conf.get('a') == 1 def test_config_get_invalid_key(self): from pecan import configuration conf = configuration.Config({'a': 1}) assert conf.get('b') is None def test_config_get_invalid_key_return_default(self): from pecan import configuration conf = configuration.Config({'a': 1}) assert conf.get('b', True) is True def test_config_to_dict(self): from pecan import configuration conf = configuration.initconf() assert isinstance(conf, configuration.Config) to_dict = conf.to_dict() assert isinstance(to_dict, dict) assert to_dict['server']['host'] == '0.0.0.0' assert to_dict['server']['port'] == '8080' assert to_dict['app']['modules'] == [] assert to_dict['app']['root'] is None assert to_dict['app']['static_root'] == 'public' assert to_dict['app']['template_path'] == '' def test_config_to_dict_nested(self): from pecan import configuration """have more than one level nesting and convert to dict""" conf = configuration.initconf() nested = {'one': {'two': 2}} conf['nested'] = nested to_dict = conf.to_dict() assert isinstance(to_dict, dict) assert to_dict['server']['host'] == '0.0.0.0' assert to_dict['server']['port'] == '8080' assert to_dict['app']['modules'] == [] assert to_dict['app']['root'] is None assert to_dict['app']['static_root'] == 'public' assert to_dict['app']['template_path'] == '' assert to_dict['nested']['one']['two'] == 2 def test_config_to_dict_prefixed(self): from pecan import configuration """Add a prefix for keys""" conf = configuration.initconf() assert isinstance(conf, configuration.Config) to_dict = conf.to_dict('prefix_') assert isinstance(to_dict, dict) assert to_dict['prefix_server']['prefix_host'] == '0.0.0.0' assert to_dict['prefix_server']['prefix_port'] == '8080' assert to_dict['prefix_app']['prefix_modules'] == [] assert to_dict['prefix_app']['prefix_root'] is None assert to_dict['prefix_app']['prefix_static_root'] == 'public' assert to_dict['prefix_app']['prefix_template_path'] == '' class TestGlobalConfig(PecanTestCase): def tearDown(self): from pecan import configuration configuration.set_config( dict(configuration.initconf()), overwrite=True ) def test_paint_from_dict(self): from pecan import configuration configuration.set_config({'foo': 'bar'}) assert dict(configuration._runtime_conf) != {'foo': 'bar'} self.assertEqual(configuration._runtime_conf.foo, 'bar') def test_overwrite_from_dict(self): from pecan import configuration configuration.set_config({'foo': 'bar'}, overwrite=True) assert dict(configuration._runtime_conf) == {'foo': 'bar'} def test_paint_from_file(self): from pecan import configuration configuration.set_config(os.path.join( __here__, 'config_fixtures/foobar.py' )) assert dict(configuration._runtime_conf) != {'foo': 'bar'} assert configuration._runtime_conf.foo == 'bar' def test_overwrite_from_file(self): from pecan import configuration configuration.set_config( os.path.join( __here__, 'config_fixtures/foobar.py', ), overwrite=True ) assert dict(configuration._runtime_conf) == {'foo': 'bar'} def test_set_config_none_type(self): from pecan import configuration self.assertRaises(RuntimeError, configuration.set_config, None) def test_set_config_to_dir(self): from pecan import configuration self.assertRaises(RuntimeError, configuration.set_config, '/') class TestConfFromEnv(PecanTestCase): # # Note that there is a good chance of pollution if ``tearDown`` does not # reset the configuration like this class does. If implementing new classes # for configuration this tearDown **needs to be implemented** # def setUp(self): super(TestConfFromEnv, self).setUp() self.addCleanup(self._remove_config_key) from pecan import configuration self.get_conf_path_from_env = configuration.get_conf_path_from_env def _remove_config_key(self): os.environ.pop('PECAN_CONFIG', None) def test_invalid_path(self): os.environ['PECAN_CONFIG'] = '/' msg = "PECAN_CONFIG was set to an invalid path: /" self.assertRaisesRegex( RuntimeError, msg, self.get_conf_path_from_env ) def test_is_not_set(self): msg = "PECAN_CONFIG is not set and " \ "no config file was passed as an argument." self.assertRaisesRegex( RuntimeError, msg, self.get_conf_path_from_env ) def test_return_valid_path(self): __here__ = os.path.abspath(__file__) os.environ['PECAN_CONFIG'] = __here__ assert self.get_conf_path_from_env() == __here__ class TestConfigCleanup(unittest.TestCase): def setUp(self): class RootController(object): @pecan.expose() def index(self): return 'Hello, World!' self.app = TestApp(pecan.Pecan(RootController())) def tearDown(self): pecan.configuration.set_config(pecan.configuration.DEFAULT, overwrite=True) def test_conf_default(self): assert pecan.conf.server.to_dict() == { 'port': '8080', 'host': '0.0.0.0' } def test_conf_changed(self): pecan.conf.server = pecan.configuration.Config({'port': '80'}) assert pecan.conf.server.to_dict() == {'port': '80'} pecan-1.5.1/pecan/tests/test_generic.py000066400000000000000000000055431445453044500201000ustar00rootroot00000000000000from json import dumps from webtest import TestApp from pecan import Pecan, expose, abort from pecan.tests import PecanTestCase class TestGeneric(PecanTestCase): def test_simple_generic(self): class RootController(object): @expose(generic=True) def index(self): pass @index.when(method='POST', template='json') def do_post(self): return dict(result='POST') @index.when(method='GET') def do_get(self): return 'GET' app = TestApp(Pecan(RootController())) r = app.get('/') assert r.status_int == 200 assert r.body == b'GET' r = app.post('/') assert r.status_int == 200 assert r.body == dumps(dict(result='POST')).encode('utf-8') r = app.get('/do_get', status=404) assert r.status_int == 404 def test_generic_allow_header(self): class RootController(object): @expose(generic=True) def index(self): abort(405) @index.when(method='POST', template='json') def do_post(self): return dict(result='POST') @index.when(method='GET') def do_get(self): return 'GET' @index.when(method='PATCH') def do_patch(self): return 'PATCH' app = TestApp(Pecan(RootController())) r = app.delete('/', expect_errors=True) assert r.status_int == 405 assert r.headers['Allow'] == 'GET, PATCH, POST' def test_nested_generic(self): class SubSubController(object): @expose(generic=True) def index(self): return 'GET' @index.when(method='DELETE', template='json') def do_delete(self, name, *args): return dict(result=name, args=', '.join(args)) class SubController(object): sub = SubSubController() class RootController(object): sub = SubController() app = TestApp(Pecan(RootController())) r = app.get('/sub/sub/') assert r.status_int == 200 assert r.body == b'GET' r = app.delete('/sub/sub/joe/is/cool') assert r.status_int == 200 assert r.body == dumps( dict(result='joe', args='is, cool') ).encode('utf-8') class TestGenericWithSpecialMethods(PecanTestCase): def test_generics_not_allowed(self): class C(object): def _default(self): pass def _lookup(self): pass def _route(self): pass for method in (C._default, C._lookup, C._route): self.assertRaises( ValueError, expose(generic=True), getattr(method, '__func__', method) ) pecan-1.5.1/pecan/tests/test_hooks.py000066400000000000000000001444551445453044500176150ustar00rootroot00000000000000import inspect import operator from io import StringIO from webtest import TestApp from pecan import make_app, expose, redirect, abort, rest, Request, Response from pecan.hooks import ( PecanHook, TransactionHook, HookController, RequestViewerHook ) from pecan.configuration import Config from pecan.decorators import transactional, after_commit, after_rollback from pecan.tests import PecanTestCase # The `inspect.Arguments` namedtuple is different between PY2/3 kwargs = operator.attrgetter('varkw') class TestHooks(PecanTestCase): def test_basic_single_hook(self): run_hook = [] class RootController(object): @expose() def index(self): run_hook.append('inside') return 'Hello, World!' class SimpleHook(PecanHook): def on_route(self, state): run_hook.append('on_route') def before(self, state): run_hook.append('before') def after(self, state): run_hook.append('after') def on_error(self, state, e): run_hook.append('error') app = TestApp(make_app(RootController(), hooks=[SimpleHook()])) response = app.get('/') assert response.status_int == 200 assert response.body == b'Hello, World!' assert len(run_hook) == 4 assert run_hook[0] == 'on_route' assert run_hook[1] == 'before' assert run_hook[2] == 'inside' assert run_hook[3] == 'after' def test_basic_multi_hook(self): run_hook = [] class RootController(object): @expose() def index(self): run_hook.append('inside') return 'Hello, World!' class SimpleHook(PecanHook): def __init__(self, id): self.id = str(id) def on_route(self, state): run_hook.append('on_route' + self.id) def before(self, state): run_hook.append('before' + self.id) def after(self, state): run_hook.append('after' + self.id) def on_error(self, state, e): run_hook.append('error' + self.id) app = TestApp(make_app(RootController(), hooks=[ SimpleHook(1), SimpleHook(2), SimpleHook(3) ])) response = app.get('/') assert response.status_int == 200 assert response.body == b'Hello, World!' assert len(run_hook) == 10 assert run_hook[0] == 'on_route1' assert run_hook[1] == 'on_route2' assert run_hook[2] == 'on_route3' assert run_hook[3] == 'before1' assert run_hook[4] == 'before2' assert run_hook[5] == 'before3' assert run_hook[6] == 'inside' assert run_hook[7] == 'after3' assert run_hook[8] == 'after2' assert run_hook[9] == 'after1' def test_partial_hooks(self): run_hook = [] class RootController(object): @expose() def index(self): run_hook.append('inside') return 'Hello World!' @expose() def causeerror(self): return [][1] class ErrorHook(PecanHook): def on_error(self, state, e): run_hook.append('error') class OnRouteHook(PecanHook): def on_route(self, state): run_hook.append('on_route') app = TestApp(make_app(RootController(), hooks=[ ErrorHook(), OnRouteHook() ])) response = app.get('/') assert response.status_int == 200 assert response.body == b'Hello World!' assert len(run_hook) == 2 assert run_hook[0] == 'on_route' assert run_hook[1] == 'inside' run_hook = [] try: response = app.get('/causeerror') except Exception as e: assert isinstance(e, IndexError) assert len(run_hook) == 2 assert run_hook[0] == 'on_route' assert run_hook[1] == 'error' def test_on_error_response_hook(self): run_hook = [] class RootController(object): @expose() def causeerror(self): return [][1] class ErrorHook(PecanHook): def on_error(self, state, e): run_hook.append('error') r = Response() r.text = 'on_error' return r app = TestApp(make_app(RootController(), hooks=[ ErrorHook() ])) response = app.get('/causeerror') assert len(run_hook) == 1 assert run_hook[0] == 'error' assert response.text == 'on_error' def test_prioritized_hooks(self): run_hook = [] class RootController(object): @expose() def index(self): run_hook.append('inside') return 'Hello, World!' class SimpleHook(PecanHook): def __init__(self, id, priority=None): self.id = str(id) if priority: self.priority = priority def on_route(self, state): run_hook.append('on_route' + self.id) def before(self, state): run_hook.append('before' + self.id) def after(self, state): run_hook.append('after' + self.id) def on_error(self, state, e): run_hook.append('error' + self.id) papp = make_app(RootController(), hooks=[ SimpleHook(1, 3), SimpleHook(2, 2), SimpleHook(3, 1) ]) app = TestApp(papp) response = app.get('/') assert response.status_int == 200 assert response.body == b'Hello, World!' assert len(run_hook) == 10 assert run_hook[0] == 'on_route3' assert run_hook[1] == 'on_route2' assert run_hook[2] == 'on_route1' assert run_hook[3] == 'before3' assert run_hook[4] == 'before2' assert run_hook[5] == 'before1' assert run_hook[6] == 'inside' assert run_hook[7] == 'after1' assert run_hook[8] == 'after2' assert run_hook[9] == 'after3' def test_basic_isolated_hook(self): run_hook = [] class SimpleHook(PecanHook): def on_route(self, state): run_hook.append('on_route') def before(self, state): run_hook.append('before') def after(self, state): run_hook.append('after') def on_error(self, state, e): run_hook.append('error') class SubSubController(object): @expose() def index(self): run_hook.append('inside_sub_sub') return 'Deep inside here!' class SubController(HookController): __hooks__ = [SimpleHook()] @expose() def index(self): run_hook.append('inside_sub') return 'Inside here!' sub = SubSubController() class RootController(object): @expose() def index(self): run_hook.append('inside') return 'Hello, World!' sub = SubController() app = TestApp(make_app(RootController())) response = app.get('/') assert response.status_int == 200 assert response.body == b'Hello, World!' assert len(run_hook) == 1 assert run_hook[0] == 'inside' run_hook = [] response = app.get('/sub/') assert response.status_int == 200 assert response.body == b'Inside here!' assert len(run_hook) == 3 assert run_hook[0] == 'before' assert run_hook[1] == 'inside_sub' assert run_hook[2] == 'after' run_hook = [] response = app.get('/sub/sub/') assert response.status_int == 200 assert response.body == b'Deep inside here!' assert len(run_hook) == 3 assert run_hook[0] == 'before' assert run_hook[1] == 'inside_sub_sub' assert run_hook[2] == 'after' def test_isolated_hook_with_global_hook(self): run_hook = [] class SimpleHook(PecanHook): def __init__(self, id): self.id = str(id) def on_route(self, state): run_hook.append('on_route' + self.id) def before(self, state): run_hook.append('before' + self.id) def after(self, state): run_hook.append('after' + self.id) def on_error(self, state, e): run_hook.append('error' + self.id) class SubController(HookController): __hooks__ = [SimpleHook(2)] @expose() def index(self): run_hook.append('inside_sub') return 'Inside here!' class RootController(object): @expose() def index(self): run_hook.append('inside') return 'Hello, World!' sub = SubController() app = TestApp(make_app(RootController(), hooks=[SimpleHook(1)])) response = app.get('/') assert response.status_int == 200 assert response.body == b'Hello, World!' assert len(run_hook) == 4 assert run_hook[0] == 'on_route1' assert run_hook[1] == 'before1' assert run_hook[2] == 'inside' assert run_hook[3] == 'after1' run_hook = [] response = app.get('/sub/') assert response.status_int == 200 assert response.body == b'Inside here!' assert len(run_hook) == 6 assert run_hook[0] == 'on_route1' assert run_hook[1] == 'before2' assert run_hook[2] == 'before1' assert run_hook[3] == 'inside_sub' assert run_hook[4] == 'after1' assert run_hook[5] == 'after2' def test_mixin_hooks(self): run_hook = [] class HelperHook(PecanHook): priority = 2 def before(self, state): run_hook.append('helper - before hook') # we'll use the same hook instance to avoid duplicate calls helper_hook = HelperHook() class LastHook(PecanHook): priority = 200 def before(self, state): run_hook.append('last - before hook') class SimpleHook(PecanHook): priority = 1 def before(self, state): run_hook.append('simple - before hook') class HelperMixin(object): __hooks__ = [helper_hook] class LastMixin(object): __hooks__ = [LastHook()] class SubController(HookController, HelperMixin): __hooks__ = [LastHook()] @expose() def index(self): return "This is sub controller!" class RootController(HookController, LastMixin): __hooks__ = [SimpleHook(), helper_hook] @expose() def index(self): run_hook.append('inside') return 'Hello, World!' sub = SubController() papp = make_app(RootController()) app = TestApp(papp) response = app.get('/') assert response.status_int == 200 assert response.body == b'Hello, World!' assert len(run_hook) == 4 assert run_hook[0] == 'simple - before hook', run_hook[0] assert run_hook[1] == 'helper - before hook', run_hook[1] assert run_hook[2] == 'last - before hook', run_hook[2] assert run_hook[3] == 'inside', run_hook[3] run_hook = [] response = app.get('/sub/') assert response.status_int == 200 assert response.body == b'This is sub controller!' assert len(run_hook) == 4, run_hook assert run_hook[0] == 'simple - before hook', run_hook[0] assert run_hook[1] == 'helper - before hook', run_hook[1] assert run_hook[2] == 'last - before hook', run_hook[2] # LastHook is invoked once again - # for each different instance of the Hook in the two Controllers assert run_hook[3] == 'last - before hook', run_hook[3] def test_internal_redirect_with_after_hook(self): run_hook = [] class RootController(object): @expose() def internal(self): redirect('/testing', internal=True) @expose() def testing(self): return 'it worked!' class SimpleHook(PecanHook): def after(self, state): run_hook.append('after') app = TestApp(make_app(RootController(), hooks=[SimpleHook()])) response = app.get('/internal') assert response.body == b'it worked!' assert len(run_hook) == 1 class TestStateAccess(PecanTestCase): def setUp(self): super(TestStateAccess, self).setUp() self.args = None class RootController(object): @expose() def index(self): return 'Hello, World!' @expose() def greet(self, name): return 'Hello, %s!' % name @expose() def greetmore(self, *args): return 'Hello, %s!' % args[0] @expose() def kwargs(self, **kw): return 'Hello, %s!' % kw['name'] @expose() def mixed(self, first, second, *args): return 'Mixed' class SimpleHook(PecanHook): def before(inself, state): self.args = (state.controller, state.arguments) self.root = RootController() self.app = TestApp(make_app(self.root, hooks=[SimpleHook()])) def test_no_args(self): self.app.get('/') assert self.args[0] == self.root.index assert isinstance(self.args[1], inspect.Arguments) assert self.args[1].args == [] assert self.args[1].varargs == [] assert kwargs(self.args[1]) == {} def test_single_arg(self): self.app.get('/greet/joe') assert self.args[0] == self.root.greet assert isinstance(self.args[1], inspect.Arguments) assert self.args[1].args == ['joe'] assert self.args[1].varargs == [] assert kwargs(self.args[1]) == {} def test_single_vararg(self): self.app.get('/greetmore/joe') assert self.args[0] == self.root.greetmore assert isinstance(self.args[1], inspect.Arguments) assert self.args[1].args == [] assert self.args[1].varargs == ['joe'] assert kwargs(self.args[1]) == {} def test_single_kw(self): self.app.get('/kwargs/?name=joe') assert self.args[0] == self.root.kwargs assert isinstance(self.args[1], inspect.Arguments) assert self.args[1].args == [] assert self.args[1].varargs == [] assert kwargs(self.args[1]) == {'name': 'joe'} def test_single_kw_post(self): self.app.post('/kwargs/', params={'name': 'joe'}) assert self.args[0] == self.root.kwargs assert isinstance(self.args[1], inspect.Arguments) assert self.args[1].args == [] assert self.args[1].varargs == [] assert kwargs(self.args[1]) == {'name': 'joe'} def test_mixed_args(self): self.app.get('/mixed/foo/bar/spam/eggs') assert self.args[0] == self.root.mixed assert isinstance(self.args[1], inspect.Arguments) assert self.args[1].args == ['foo', 'bar'] assert self.args[1].varargs == ['spam', 'eggs'] class TestStateAccessWithoutThreadLocals(PecanTestCase): def setUp(self): super(TestStateAccessWithoutThreadLocals, self).setUp() self.args = None class RootController(object): @expose() def index(self, req, resp): return 'Hello, World!' @expose() def greet(self, req, resp, name): return 'Hello, %s!' % name @expose() def greetmore(self, req, resp, *args): return 'Hello, %s!' % args[0] @expose() def kwargs(self, req, resp, **kw): return 'Hello, %s!' % kw['name'] @expose() def mixed(self, req, resp, first, second, *args): return 'Mixed' class SimpleHook(PecanHook): def before(inself, state): self.args = (state.controller, state.arguments) self.root = RootController() self.app = TestApp(make_app( self.root, hooks=[SimpleHook()], use_context_locals=False )) def test_no_args(self): self.app.get('/') assert self.args[0] == self.root.index assert isinstance(self.args[1], inspect.Arguments) assert len(self.args[1].args) == 2 assert isinstance(self.args[1].args[0], Request) assert isinstance(self.args[1].args[1], Response) assert self.args[1].varargs == [] assert kwargs(self.args[1]) == {} def test_single_arg(self): self.app.get('/greet/joe') assert self.args[0] == self.root.greet assert isinstance(self.args[1], inspect.Arguments) assert len(self.args[1].args) == 3 assert isinstance(self.args[1].args[0], Request) assert isinstance(self.args[1].args[1], Response) assert self.args[1].args[2] == 'joe' assert self.args[1].varargs == [] assert kwargs(self.args[1]) == {} def test_single_vararg(self): self.app.get('/greetmore/joe') assert self.args[0] == self.root.greetmore assert isinstance(self.args[1], inspect.Arguments) assert len(self.args[1].args) == 2 assert isinstance(self.args[1].args[0], Request) assert isinstance(self.args[1].args[1], Response) assert self.args[1].varargs == ['joe'] assert kwargs(self.args[1]) == {} def test_single_kw(self): self.app.get('/kwargs/?name=joe') assert self.args[0] == self.root.kwargs assert isinstance(self.args[1], inspect.Arguments) assert len(self.args[1].args) == 2 assert isinstance(self.args[1].args[0], Request) assert isinstance(self.args[1].args[1], Response) assert self.args[1].varargs == [] assert kwargs(self.args[1]) == {'name': 'joe'} def test_single_kw_post(self): self.app.post('/kwargs/', params={'name': 'joe'}) assert self.args[0] == self.root.kwargs assert isinstance(self.args[1], inspect.Arguments) assert len(self.args[1].args) == 2 assert isinstance(self.args[1].args[0], Request) assert isinstance(self.args[1].args[1], Response) assert self.args[1].varargs == [] assert kwargs(self.args[1]) == {'name': 'joe'} def test_mixed_args(self): self.app.get('/mixed/foo/bar/spam/eggs') assert self.args[0] == self.root.mixed assert isinstance(self.args[1], inspect.Arguments) assert len(self.args[1].args) == 4 assert isinstance(self.args[1].args[0], Request) assert isinstance(self.args[1].args[1], Response) assert self.args[1].args[2:] == ['foo', 'bar'] assert self.args[1].varargs == ['spam', 'eggs'] class TestRestControllerStateAccess(PecanTestCase): def setUp(self): super(TestRestControllerStateAccess, self).setUp() self.args = None class RootController(rest.RestController): @expose() def _default(self, _id, *args, **kw): return 'Default' @expose() def get_all(self, **kw): return 'All' @expose() def get_one(self, _id, *args, **kw): return 'One' @expose() def post(self, *args, **kw): return 'POST' @expose() def put(self, _id, *args, **kw): return 'PUT' @expose() def delete(self, _id, *args, **kw): return 'DELETE' class SimpleHook(PecanHook): def before(inself, state): self.args = (state.controller, state.arguments) self.root = RootController() self.app = TestApp(make_app(self.root, hooks=[SimpleHook()])) def test_get_all(self): self.app.get('/') assert self.args[0] == self.root.get_all assert isinstance(self.args[1], inspect.Arguments) assert self.args[1].args == [] assert self.args[1].varargs == [] assert kwargs(self.args[1]) == {} def test_get_all_with_kwargs(self): self.app.get('/?foo=bar') assert self.args[0] == self.root.get_all assert isinstance(self.args[1], inspect.Arguments) assert self.args[1].args == [] assert self.args[1].varargs == [] assert kwargs(self.args[1]) == {'foo': 'bar'} def test_get_one(self): self.app.get('/1') assert self.args[0] == self.root.get_one assert isinstance(self.args[1], inspect.Arguments) assert self.args[1].args == ['1'] assert self.args[1].varargs == [] assert kwargs(self.args[1]) == {} def test_get_one_with_varargs(self): self.app.get('/1/2/3') assert self.args[0] == self.root.get_one assert isinstance(self.args[1], inspect.Arguments) assert self.args[1].args == ['1'] assert self.args[1].varargs == ['2', '3'] assert kwargs(self.args[1]) == {} def test_get_one_with_kwargs(self): self.app.get('/1?foo=bar') assert self.args[0] == self.root.get_one assert isinstance(self.args[1], inspect.Arguments) assert self.args[1].args == ['1'] assert self.args[1].varargs == [] assert kwargs(self.args[1]) == {'foo': 'bar'} def test_post(self): self.app.post('/') assert self.args[0] == self.root.post assert isinstance(self.args[1], inspect.Arguments) assert self.args[1].args == [] assert self.args[1].varargs == [] assert kwargs(self.args[1]) == {} def test_post_with_varargs(self): self.app.post('/foo/bar') assert self.args[0] == self.root.post assert isinstance(self.args[1], inspect.Arguments) assert self.args[1].args == [] assert self.args[1].varargs == ['foo', 'bar'] assert kwargs(self.args[1]) == {} def test_post_with_kwargs(self): self.app.post('/', params={'foo': 'bar'}) assert self.args[0] == self.root.post assert isinstance(self.args[1], inspect.Arguments) assert self.args[1].args == [] assert self.args[1].varargs == [] assert kwargs(self.args[1]) == {'foo': 'bar'} def test_put(self): self.app.put('/1') assert self.args[0] == self.root.put assert isinstance(self.args[1], inspect.Arguments) assert self.args[1].args == ['1'] assert self.args[1].varargs == [] assert kwargs(self.args[1]) == {} def test_put_with_method_argument(self): self.app.post('/1?_method=put') assert self.args[0] == self.root.put assert isinstance(self.args[1], inspect.Arguments) assert self.args[1].args == ['1'] assert self.args[1].varargs == [] assert kwargs(self.args[1]) == {'_method': 'put'} def test_put_with_varargs(self): self.app.put('/1/2/3') assert self.args[0] == self.root.put assert isinstance(self.args[1], inspect.Arguments) assert self.args[1].args == ['1'] assert self.args[1].varargs == ['2', '3'] assert kwargs(self.args[1]) == {} def test_put_with_kwargs(self): self.app.put('/1?foo=bar') assert self.args[0] == self.root.put assert isinstance(self.args[1], inspect.Arguments) assert self.args[1].args == ['1'] assert self.args[1].varargs == [] assert kwargs(self.args[1]) == {'foo': 'bar'} def test_delete(self): self.app.delete('/1') assert self.args[0] == self.root.delete assert isinstance(self.args[1], inspect.Arguments) assert self.args[1].args == ['1'] assert self.args[1].varargs == [] assert kwargs(self.args[1]) == {} def test_delete_with_method_argument(self): self.app.post('/1?_method=delete') assert self.args[0] == self.root.delete assert isinstance(self.args[1], inspect.Arguments) assert self.args[1].args == ['1'] assert self.args[1].varargs == [] assert kwargs(self.args[1]) == {'_method': 'delete'} def test_delete_with_varargs(self): self.app.delete('/1/2/3') assert self.args[0] == self.root.delete assert isinstance(self.args[1], inspect.Arguments) assert self.args[1].args == ['1'] assert self.args[1].varargs == ['2', '3'] assert kwargs(self.args[1]) == {} def test_delete_with_kwargs(self): self.app.delete('/1?foo=bar') assert self.args[0] == self.root.delete assert isinstance(self.args[1], inspect.Arguments) assert self.args[1].args == ['1'] assert self.args[1].varargs == [] assert kwargs(self.args[1]) == {'foo': 'bar'} def test_post_with_invalid_method_kwarg(self): self.app.post('/1?_method=invalid') assert self.args[0] == self.root._default assert isinstance(self.args[1], inspect.Arguments) assert self.args[1].args == ['1'] assert self.args[1].varargs == [] assert kwargs(self.args[1]) == {'_method': 'invalid'} class TestTransactionHook(PecanTestCase): def test_transaction_hook(self): run_hook = [] class RootController(object): @expose() def index(self): run_hook.append('inside') return 'Hello, World!' @expose() def redirect(self): redirect('/') @expose() def error(self): return [][1] def gen(event): return lambda: run_hook.append(event) app = TestApp(make_app(RootController(), hooks=[ TransactionHook( start=gen('start'), start_ro=gen('start_ro'), commit=gen('commit'), rollback=gen('rollback'), clear=gen('clear') ) ])) response = app.get('/') assert response.status_int == 200 assert response.body == b'Hello, World!' assert len(run_hook) == 3 assert run_hook[0] == 'start_ro' assert run_hook[1] == 'inside' assert run_hook[2] == 'clear' run_hook = [] response = app.post('/') assert response.status_int == 200 assert response.body == b'Hello, World!' assert len(run_hook) == 4 assert run_hook[0] == 'start' assert run_hook[1] == 'inside' assert run_hook[2] == 'commit' assert run_hook[3] == 'clear' # # test hooks for GET /redirect # This controller should always be non-transactional # run_hook = [] response = app.get('/redirect') assert response.status_int == 302 assert len(run_hook) == 2 assert run_hook[0] == 'start_ro' assert run_hook[1] == 'clear' # # test hooks for POST /redirect # This controller should always be transactional, # even in the case of redirects # run_hook = [] response = app.post('/redirect') assert response.status_int == 302 assert len(run_hook) == 3 assert run_hook[0] == 'start' assert run_hook[1] == 'commit' assert run_hook[2] == 'clear' run_hook = [] try: response = app.post('/error') except IndexError: pass assert len(run_hook) == 3 assert run_hook[0] == 'start' assert run_hook[1] == 'rollback' assert run_hook[2] == 'clear' def test_transaction_hook_with_after_actions(self): run_hook = [] def action(name): def action_impl(): run_hook.append(name) return action_impl class RootController(object): @expose() @after_commit(action('action-one')) def index(self): run_hook.append('inside') return 'Index Method!' @expose() @transactional() @after_commit(action('action-two')) def decorated(self): run_hook.append('inside') return 'Decorated Method!' @expose() @after_rollback(action('action-three')) def rollback(self): abort(500) @expose() @transactional() @after_rollback(action('action-four')) def rollback_decorated(self): abort(500) def gen(event): return lambda: run_hook.append(event) app = TestApp(make_app(RootController(), hooks=[ TransactionHook( start=gen('start'), start_ro=gen('start_ro'), commit=gen('commit'), rollback=gen('rollback'), clear=gen('clear') ) ])) response = app.get('/') assert response.status_int == 200 assert response.body == b'Index Method!' assert len(run_hook) == 3 assert run_hook[0] == 'start_ro' assert run_hook[1] == 'inside' assert run_hook[2] == 'clear' run_hook = [] response = app.post('/') assert response.status_int == 200 assert response.body == b'Index Method!' assert len(run_hook) == 5 assert run_hook[0] == 'start' assert run_hook[1] == 'inside' assert run_hook[2] == 'commit' assert run_hook[3] == 'action-one' assert run_hook[4] == 'clear' run_hook = [] response = app.get('/decorated') assert response.status_int == 200 assert response.body == b'Decorated Method!' assert len(run_hook) == 7 assert run_hook[0] == 'start_ro' assert run_hook[1] == 'clear' assert run_hook[2] == 'start' assert run_hook[3] == 'inside' assert run_hook[4] == 'commit' assert run_hook[5] == 'action-two' assert run_hook[6] == 'clear' run_hook = [] response = app.get('/rollback', expect_errors=True) assert response.status_int == 500 assert len(run_hook) == 2 assert run_hook[0] == 'start_ro' assert run_hook[1] == 'clear' run_hook = [] response = app.post('/rollback', expect_errors=True) assert response.status_int == 500 assert len(run_hook) == 4 assert run_hook[0] == 'start' assert run_hook[1] == 'rollback' assert run_hook[2] == 'action-three' assert run_hook[3] == 'clear' run_hook = [] response = app.get('/rollback_decorated', expect_errors=True) assert response.status_int == 500 assert len(run_hook) == 6 assert run_hook[0] == 'start_ro' assert run_hook[1] == 'clear' assert run_hook[2] == 'start' assert run_hook[3] == 'rollback' assert run_hook[4] == 'action-four' assert run_hook[5] == 'clear' run_hook = [] response = app.get('/fourohfour', status=404) assert response.status_int == 404 assert len(run_hook) == 2 assert run_hook[0] == 'start_ro' assert run_hook[1] == 'clear' def test_transaction_hook_with_transactional_decorator(self): run_hook = [] class RootController(object): @expose() def index(self): run_hook.append('inside') return 'Hello, World!' @expose() def redirect(self): redirect('/') @expose() @transactional() def redirect_transactional(self): redirect('/') @expose() @transactional(False) def redirect_rollback(self): redirect('/') @expose() def error(self): return [][1] @expose() @transactional(False) def error_rollback(self): return [][1] @expose() @transactional() def error_transactional(self): return [][1] def gen(event): return lambda: run_hook.append(event) app = TestApp(make_app(RootController(), hooks=[ TransactionHook( start=gen('start'), start_ro=gen('start_ro'), commit=gen('commit'), rollback=gen('rollback'), clear=gen('clear') ) ])) response = app.get('/') assert response.status_int == 200 assert response.body == b'Hello, World!' assert len(run_hook) == 3 assert run_hook[0] == 'start_ro' assert run_hook[1] == 'inside' assert run_hook[2] == 'clear' run_hook = [] # test hooks for / response = app.post('/') assert response.status_int == 200 assert response.body == b'Hello, World!' assert len(run_hook) == 4 assert run_hook[0] == 'start' assert run_hook[1] == 'inside' assert run_hook[2] == 'commit' assert run_hook[3] == 'clear' # # test hooks for GET /redirect # This controller should always be non-transactional # run_hook = [] response = app.get('/redirect') assert response.status_int == 302 assert len(run_hook) == 2 assert run_hook[0] == 'start_ro' assert run_hook[1] == 'clear' # # test hooks for POST /redirect # This controller should always be transactional, # even in the case of redirects # run_hook = [] response = app.post('/redirect') assert response.status_int == 302 assert len(run_hook) == 3 assert run_hook[0] == 'start' assert run_hook[1] == 'commit' assert run_hook[2] == 'clear' # # test hooks for GET /redirect_transactional # This controller should always be transactional, # even in the case of redirects # run_hook = [] response = app.get('/redirect_transactional') assert response.status_int == 302 assert len(run_hook) == 5 assert run_hook[0] == 'start_ro' assert run_hook[1] == 'clear' assert run_hook[2] == 'start' assert run_hook[3] == 'commit' assert run_hook[4] == 'clear' # # test hooks for POST /redirect_transactional # This controller should always be transactional, # even in the case of redirects # run_hook = [] response = app.post('/redirect_transactional') assert response.status_int == 302 assert len(run_hook) == 3 assert run_hook[0] == 'start' assert run_hook[1] == 'commit' assert run_hook[2] == 'clear' # # test hooks for GET /redirect_rollback # This controller should always be transactional, # *except* in the case of redirects # run_hook = [] response = app.get('/redirect_rollback') assert response.status_int == 302 assert len(run_hook) == 5 assert run_hook[0] == 'start_ro' assert run_hook[1] == 'clear' assert run_hook[2] == 'start' assert run_hook[3] == 'rollback' assert run_hook[4] == 'clear' # # test hooks for POST /redirect_rollback # This controller should always be transactional, # *except* in the case of redirects # run_hook = [] response = app.post('/redirect_rollback') assert response.status_int == 302 assert len(run_hook) == 3 assert run_hook[0] == 'start' assert run_hook[1] == 'rollback' assert run_hook[2] == 'clear' # # Exceptions (other than HTTPFound) should *always* # rollback no matter what # run_hook = [] try: response = app.post('/error') except IndexError: pass assert len(run_hook) == 3 assert run_hook[0] == 'start' assert run_hook[1] == 'rollback' assert run_hook[2] == 'clear' run_hook = [] try: response = app.get('/error') except IndexError: pass assert len(run_hook) == 2 assert run_hook[0] == 'start_ro' assert run_hook[1] == 'clear' run_hook = [] try: response = app.post('/error_transactional') except IndexError: pass assert len(run_hook) == 3 assert run_hook[0] == 'start' assert run_hook[1] == 'rollback' assert run_hook[2] == 'clear' run_hook = [] try: response = app.get('/error_transactional') except IndexError: pass assert len(run_hook) == 5 assert run_hook[0] == 'start_ro' assert run_hook[1] == 'clear' assert run_hook[2] == 'start' assert run_hook[3] == 'rollback' assert run_hook[4] == 'clear' run_hook = [] try: response = app.post('/error_rollback') except IndexError: pass assert len(run_hook) == 3 assert run_hook[0] == 'start' assert run_hook[1] == 'rollback' assert run_hook[2] == 'clear' run_hook = [] try: response = app.get('/error_rollback') except IndexError: pass assert len(run_hook) == 5 assert run_hook[0] == 'start_ro' assert run_hook[1] == 'clear' assert run_hook[2] == 'start' assert run_hook[3] == 'rollback' assert run_hook[4] == 'clear' def test_transaction_hook_with_transactional_class_decorator(self): run_hook = [] @transactional() class RootController(object): @expose() def index(self): run_hook.append('inside') return 'Hello, World!' @expose() def redirect(self): redirect('/') @expose() @transactional(False) def redirect_rollback(self): redirect('/') @expose() def error(self): return [][1] @expose(generic=True) def generic(self): pass @generic.when(method='GET') def generic_get(self): run_hook.append('inside') return 'generic get' @generic.when(method='POST') def generic_post(self): run_hook.append('inside') return 'generic post' def gen(event): return lambda: run_hook.append(event) app = TestApp(make_app(RootController(), hooks=[ TransactionHook( start=gen('start'), start_ro=gen('start_ro'), commit=gen('commit'), rollback=gen('rollback'), clear=gen('clear') ) ])) response = app.get('/') assert response.status_int == 200 assert response.body == b'Hello, World!' assert len(run_hook) == 6 assert run_hook[0] == 'start_ro' assert run_hook[1] == 'clear' assert run_hook[2] == 'start' assert run_hook[3] == 'inside' assert run_hook[4] == 'commit' assert run_hook[5] == 'clear' run_hook = [] # test hooks for / response = app.post('/') assert response.status_int == 200 assert response.body == b'Hello, World!' assert len(run_hook) == 4 assert run_hook[0] == 'start' assert run_hook[1] == 'inside' assert run_hook[2] == 'commit' assert run_hook[3] == 'clear' # # test hooks for GET /redirect # This controller should always be transactional, # even in the case of redirects # run_hook = [] response = app.get('/redirect') assert response.status_int == 302 assert len(run_hook) == 5 assert run_hook[0] == 'start_ro' assert run_hook[1] == 'clear' assert run_hook[2] == 'start' assert run_hook[3] == 'commit' assert run_hook[4] == 'clear' # # test hooks for POST /redirect # This controller should always be transactional, # even in the case of redirects # run_hook = [] response = app.post('/redirect') assert response.status_int == 302 assert len(run_hook) == 3 assert run_hook[0] == 'start' assert run_hook[1] == 'commit' assert run_hook[2] == 'clear' # # test hooks for GET /redirect_rollback # This controller should always be transactional, # *except* in the case of redirects # run_hook = [] response = app.get('/redirect_rollback') assert response.status_int == 302 assert len(run_hook) == 5 assert run_hook[0] == 'start_ro' assert run_hook[1] == 'clear' assert run_hook[2] == 'start' assert run_hook[3] == 'rollback' assert run_hook[4] == 'clear' # # test hooks for POST /redirect_rollback # This controller should always be transactional, # *except* in the case of redirects # run_hook = [] response = app.post('/redirect_rollback') assert response.status_int == 302 assert len(run_hook) == 3 assert run_hook[0] == 'start' assert run_hook[1] == 'rollback' assert run_hook[2] == 'clear' # # Exceptions (other than HTTPFound) should *always* # rollback no matter what # run_hook = [] try: response = app.post('/error') except IndexError: pass assert len(run_hook) == 3 assert run_hook[0] == 'start' assert run_hook[1] == 'rollback' assert run_hook[2] == 'clear' run_hook = [] try: response = app.get('/error') except IndexError: pass assert len(run_hook) == 5 assert run_hook[0] == 'start_ro' assert run_hook[1] == 'clear' assert run_hook[2] == 'start' assert run_hook[3] == 'rollback' assert run_hook[4] == 'clear' # # test hooks for GET /generic # This controller should always be transactional, # run_hook = [] response = app.get('/generic') assert response.status_int == 200 assert response.body == b'generic get' assert len(run_hook) == 6 assert run_hook[0] == 'start_ro' assert run_hook[1] == 'clear' assert run_hook[2] == 'start' assert run_hook[3] == 'inside' assert run_hook[4] == 'commit' assert run_hook[5] == 'clear' # # test hooks for POST /generic # This controller should always be transactional, # run_hook = [] response = app.post('/generic') assert response.status_int == 200 assert response.body == b'generic post' assert len(run_hook) == 4 assert run_hook[0] == 'start' assert run_hook[1] == 'inside' assert run_hook[2] == 'commit' assert run_hook[3] == 'clear' def test_transaction_hook_with_broken_hook(self): """ In a scenario where a preceding hook throws an exception, ensure that TransactionHook still rolls back properly. """ run_hook = [] class RootController(object): @expose() def index(self): return 'Hello, World!' def gen(event): return lambda: run_hook.append(event) class MyCustomException(Exception): pass class MyHook(PecanHook): def on_route(self, state): raise MyCustomException('BROKEN!') app = TestApp(make_app(RootController(), hooks=[ MyHook(), TransactionHook( start=gen('start'), start_ro=gen('start_ro'), commit=gen('commit'), rollback=gen('rollback'), clear=gen('clear') ) ])) self.assertRaises( MyCustomException, app.get, '/' ) assert len(run_hook) == 1 assert run_hook[0] == 'clear' class TestRequestViewerHook(PecanTestCase): def test_basic_single_default_hook(self): _stdout = StringIO() class RootController(object): @expose() def index(self): return 'Hello, World!' app = TestApp( make_app( RootController(), hooks=lambda: [ RequestViewerHook(writer=_stdout) ] ) ) response = app.get('/') out = _stdout.getvalue() assert response.status_int == 200 assert response.body == b'Hello, World!' assert 'path' in out assert 'method' in out assert 'status' in out assert 'method' in out assert 'params' in out assert 'hooks' in out assert '200 OK' in out assert "['RequestViewerHook']" in out assert '/' in out def test_bad_response_from_app(self): """When exceptions are raised the hook deals with them properly""" _stdout = StringIO() class RootController(object): @expose() def index(self): return 'Hello, World!' app = TestApp( make_app( RootController(), hooks=lambda: [ RequestViewerHook(writer=_stdout) ] ) ) response = app.get('/404', expect_errors=True) out = _stdout.getvalue() assert response.status_int == 404 assert 'path' in out assert 'method' in out assert 'status' in out assert 'method' in out assert 'params' in out assert 'hooks' in out assert '404 Not Found' in out assert "['RequestViewerHook']" in out assert '/' in out def test_single_item(self): _stdout = StringIO() class RootController(object): @expose() def index(self): return 'Hello, World!' app = TestApp( make_app( RootController(), hooks=lambda: [ RequestViewerHook( config={'items': ['path']}, writer=_stdout ) ] ) ) response = app.get('/') out = _stdout.getvalue() assert response.status_int == 200 assert response.body == b'Hello, World!' assert '/' in out assert 'path' in out assert 'method' not in out assert 'status' not in out assert 'method' not in out assert 'params' not in out assert 'hooks' not in out assert '200 OK' not in out assert "['RequestViewerHook']" not in out def test_single_blacklist_item(self): _stdout = StringIO() class RootController(object): @expose() def index(self): return 'Hello, World!' app = TestApp( make_app( RootController(), hooks=lambda: [ RequestViewerHook( config={'blacklist': ['/']}, writer=_stdout ) ] ) ) response = app.get('/') out = _stdout.getvalue() assert response.status_int == 200 assert response.body == b'Hello, World!' assert out == '' def test_item_not_in_defaults(self): _stdout = StringIO() class RootController(object): @expose() def index(self): return 'Hello, World!' app = TestApp( make_app( RootController(), hooks=lambda: [ RequestViewerHook( config={'items': ['date']}, writer=_stdout ) ] ) ) response = app.get('/') out = _stdout.getvalue() assert response.status_int == 200 assert response.body == b'Hello, World!' assert 'date' in out assert 'method' not in out assert 'status' not in out assert 'method' not in out assert 'params' not in out assert 'hooks' not in out assert '200 OK' not in out assert "['RequestViewerHook']" not in out assert '/' not in out def test_hook_formatting(self): hooks = [''] viewer = RequestViewerHook() formatted = viewer.format_hooks(hooks) assert formatted == ['RequestViewerHook'] def test_deal_with_pecan_configs(self): """If config comes from pecan.conf convert it to dict""" conf = Config(conf_dict={'items': ['url']}) viewer = RequestViewerHook(conf) assert viewer.items == ['url'] class TestRestControllerWithHooks(PecanTestCase): def test_restcontroller_with_hooks(self): class SomeHook(PecanHook): def before(self, state): state.response.headers['X-Testing'] = 'XYZ' class BaseController(rest.RestController): @expose() def delete(self, _id): return 'Deleting %s' % _id class RootController(BaseController, HookController): __hooks__ = [SomeHook()] @expose() def get_all(self): return 'Hello, World!' @staticmethod def static(cls): return 'static' @property def foo(self): return 'bar' def testing123(self): return 'bar' unhashable = [1, 'two', 3] app = TestApp( make_app( RootController() ) ) response = app.get('/') assert response.status_int == 200 assert response.body == b'Hello, World!' assert response.headers['X-Testing'] == 'XYZ' response = app.delete('/100/') assert response.status_int == 200 assert response.body == b'Deleting 100' assert response.headers['X-Testing'] == 'XYZ' pecan-1.5.1/pecan/tests/test_jsonify.py000066400000000000000000000147721445453044500201510ustar00rootroot00000000000000from datetime import datetime, date from decimal import Decimal from json import loads try: from sqlalchemy import orm, schema, types from sqlalchemy.engine import create_engine from sqlalchemy.orm import registry except ImportError: create_engine = None # noqa from webtest import TestApp from webob.multidict import MultiDict from pecan.jsonify import jsonify, encode, ResultProxy, RowProxy from pecan import Pecan, expose from pecan.tests import PecanTestCase def make_person(): class Person(object): def __init__(self, first_name, last_name): self.first_name = first_name self.last_name = last_name @property def name(self): return '%s %s' % (self.first_name, self.last_name) return Person def test_simple_rule(): Person = make_person() # create a Person instance p = Person('Jonathan', 'LaCour') # register a generic JSON rule @jsonify.when_type(Person) def jsonify_person(obj): return dict( name=obj.name ) # encode the object using our new rule result = loads(encode(p)) assert result['name'] == 'Jonathan LaCour' assert len(result) == 1 class TestJsonify(PecanTestCase): def test_simple_jsonify(self): Person = make_person() # register a generic JSON rule @jsonify.when_type(Person) def jsonify_person(obj): return dict( name=obj.name ) class RootController(object): @expose('json') def index(self): # create a Person instance p = Person('Jonathan', 'LaCour') return p app = TestApp(Pecan(RootController())) r = app.get('/') assert r.status_int == 200 assert loads(r.body.decode()) == {'name': 'Jonathan LaCour'} class TestJsonifyGenericEncoder(PecanTestCase): def test_json_callable(self): class JsonCallable(object): def __init__(self, arg): self.arg = arg def __json__(self): return {"arg": self.arg} result = encode(JsonCallable('foo')) assert loads(result) == {'arg': 'foo'} def test_datetime(self): today = date.today() now = datetime.now() result = encode(today) assert loads(result) == str(today) result = encode(now) assert loads(result) == str(now) def test_decimal(self): # XXX Testing for float match which is inexact d = Decimal('1.1') result = encode(d) assert loads(result) == float(d) def test_multidict(self): md = MultiDict() md.add('arg', 'foo') md.add('arg', 'bar') result = encode(md) assert loads(result) == {'arg': ['foo', 'bar']} def test_fallback_to_builtin_encoder(self): class Foo(object): pass self.assertRaises(TypeError, encode, Foo()) class TestJsonifySQLAlchemyGenericEncoder(PecanTestCase): def setUp(self): super(TestJsonifySQLAlchemyGenericEncoder, self).setUp() if not create_engine: self.create_fake_proxies() else: self.create_sa_proxies() def create_fake_proxies(self): # create a fake SA object class FakeSAObject(object): def __init__(self): self._sa_class_manager = object() self._sa_instance_state = 'awesome' self.id = 1 self.first_name = 'Jonathan' self.last_name = 'LaCour' # create a fake result proxy class FakeResultProxy(ResultProxy): def __init__(self): self.rowcount = -1 self.rows = [] def __iter__(self): return iter(self.rows) def append(self, row): self.rows.append(row) # create a fake row proxy class FakeRowProxy(RowProxy): def __init__(self, arg=None): self.row = dict(arg) def __getitem__(self, key): return self.row.__getitem__(key) def keys(self): return self.row.keys() # get the SA objects self.sa_object = FakeSAObject() self.result_proxy = FakeResultProxy() self.result_proxy.append( FakeRowProxy([ ('id', 1), ('first_name', 'Jonathan'), ('last_name', 'LaCour') ]) ) self.result_proxy.append( FakeRowProxy([ ('id', 2), ('first_name', 'Ryan'), ('last_name', 'Petrello') ])) self.row_proxy = FakeRowProxy([ ('id', 1), ('first_name', 'Jonathan'), ('last_name', 'LaCour') ]) def create_sa_proxies(self): # create the table and mapper mapper_registry = registry() metadata = schema.MetaData() user_table = schema.Table( 'user', metadata, schema.Column('id', types.Integer, primary_key=True), schema.Column('first_name', types.Unicode(25)), schema.Column('last_name', types.Unicode(25)) ) class User(object): pass mapper_registry.map_imperatively(User, user_table) # create the session engine = create_engine('sqlite:///:memory:') metadata.bind = engine metadata.create_all(metadata.bind) session = orm.sessionmaker(bind=engine)() # add some dummy data session.add(User(first_name='Jonathan', last_name='LaCour')) session.add(User(first_name='Ryan', last_name='Petrello')) session.commit() # get the SA objects self.sa_object = session.query(User).first() select = user_table.select() self.result_proxy = session.execute(select) self.row_proxy = session.execute(select).fetchone() def test_sa_object(self): result = encode(self.sa_object) assert loads(result) == { 'id': 1, 'first_name': 'Jonathan', 'last_name': 'LaCour' } def test_result_proxy(self): result = encode(self.result_proxy) assert loads(result) == {'count': 2, 'rows': [ {'id': 1, 'first_name': 'Jonathan', 'last_name': 'LaCour'}, {'id': 2, 'first_name': 'Ryan', 'last_name': 'Petrello'} ]} def test_row_proxy(self): result = encode(self.row_proxy) assert loads(result) == { 'id': 1, 'first_name': 'Jonathan', 'last_name': 'LaCour' } pecan-1.5.1/pecan/tests/test_no_thread_locals.py000066400000000000000000001407241445453044500217650ustar00rootroot00000000000000import time from json import dumps, loads import warnings from unittest import mock from webtest import TestApp import webob from pecan import Pecan, expose, abort, Request, Response from pecan.rest import RestController from pecan.hooks import PecanHook, HookController from pecan.tests import PecanTestCase class TestThreadingLocalUsage(PecanTestCase): @property def root(self): class RootController(object): @expose() def index(self, req, resp): assert isinstance(req, webob.BaseRequest) assert isinstance(resp, webob.Response) return 'Hello, World!' @expose() def warning(self): return ("This should be unroutable because (req, resp) are not" " arguments. It should raise a TypeError.") @expose(generic=True) def generic(self): return ("This should be unroutable because (req, resp) are not" " arguments. It should raise a TypeError.") @generic.when(method='PUT') def generic_put(self, _id): return ("This should be unroutable because (req, resp) are not" " arguments. It should raise a TypeError.") return RootController def test_locals_are_not_used(self): with mock.patch('threading.local', side_effect=AssertionError()): app = TestApp(Pecan(self.root(), use_context_locals=False)) r = app.get('/') assert r.status_int == 200 assert r.body == b'Hello, World!' self.assertRaises(AssertionError, Pecan, self.root) def test_threadlocal_argument_warning(self): with mock.patch('threading.local', side_effect=AssertionError()): app = TestApp(Pecan(self.root(), use_context_locals=False)) self.assertRaises( TypeError, app.get, '/warning/' ) def test_threadlocal_argument_warning_on_generic(self): with mock.patch('threading.local', side_effect=AssertionError()): app = TestApp(Pecan(self.root(), use_context_locals=False)) self.assertRaises( TypeError, app.get, '/generic/' ) def test_threadlocal_argument_warning_on_generic_delegate(self): with mock.patch('threading.local', side_effect=AssertionError()): app = TestApp(Pecan(self.root(), use_context_locals=False)) self.assertRaises( TypeError, app.put, '/generic/' ) class TestIndexRouting(PecanTestCase): @property def app_(self): class RootController(object): @expose() def index(self, req, resp): assert isinstance(req, webob.BaseRequest) assert isinstance(resp, webob.Response) return 'Hello, World!' return TestApp(Pecan(RootController(), use_context_locals=False)) def test_empty_root(self): r = self.app_.get('/') assert r.status_int == 200 assert r.body == b'Hello, World!' def test_index(self): r = self.app_.get('/index') assert r.status_int == 200 assert r.body == b'Hello, World!' def test_index_html(self): r = self.app_.get('/index.html') assert r.status_int == 200 assert r.body == b'Hello, World!' class TestManualResponse(PecanTestCase): def test_manual_response(self): class RootController(object): @expose() def index(self, req, resp): resp = webob.Response(resp.environ) resp.body = b'Hello, World!' return resp app = TestApp(Pecan(RootController(), use_context_locals=False)) r = app.get('/') assert r.body == b'Hello, World!', r.body class TestDispatch(PecanTestCase): @property def app_(self): class SubSubController(object): @expose() def index(self, req, resp): assert isinstance(req, webob.BaseRequest) assert isinstance(resp, webob.Response) return '/sub/sub/' @expose() def deeper(self, req, resp): assert isinstance(req, webob.BaseRequest) assert isinstance(resp, webob.Response) return '/sub/sub/deeper' class SubController(object): @expose() def index(self, req, resp): assert isinstance(req, webob.BaseRequest) assert isinstance(resp, webob.Response) return '/sub/' @expose() def deeper(self, req, resp): assert isinstance(req, webob.BaseRequest) assert isinstance(resp, webob.Response) return '/sub/deeper' sub = SubSubController() class RootController(object): @expose() def index(self, req, resp): assert isinstance(req, webob.BaseRequest) assert isinstance(resp, webob.Response) return '/' @expose() def deeper(self, req, resp): assert isinstance(req, webob.BaseRequest) assert isinstance(resp, webob.Response) return '/deeper' sub = SubController() return TestApp(Pecan(RootController(), use_context_locals=False)) def test_index(self): r = self.app_.get('/') assert r.status_int == 200 assert r.body == b'/' def test_one_level(self): r = self.app_.get('/deeper') assert r.status_int == 200 assert r.body == b'/deeper' def test_one_level_with_trailing(self): r = self.app_.get('/sub/') assert r.status_int == 200 assert r.body == b'/sub/' def test_two_levels(self): r = self.app_.get('/sub/deeper') assert r.status_int == 200 assert r.body == b'/sub/deeper' def test_two_levels_with_trailing(self): r = self.app_.get('/sub/sub/') assert r.status_int == 200 def test_three_levels(self): r = self.app_.get('/sub/sub/deeper') assert r.status_int == 200 assert r.body == b'/sub/sub/deeper' class TestLookups(PecanTestCase): @property def app_(self): class LookupController(object): def __init__(self, someID): self.someID = someID @expose() def index(self, req, resp): return '/%s' % self.someID @expose() def name(self, req, resp): return '/%s/name' % self.someID class RootController(object): @expose() def index(self, req, resp): return '/' @expose() def _lookup(self, someID, *remainder): return LookupController(someID), remainder return TestApp(Pecan(RootController(), use_context_locals=False)) def test_index(self): r = self.app_.get('/') assert r.status_int == 200 assert r.body == b'/' def test_lookup(self): r = self.app_.get('/100/') assert r.status_int == 200 assert r.body == b'/100' def test_lookup_with_method(self): r = self.app_.get('/100/name') assert r.status_int == 200 assert r.body == b'/100/name' def test_lookup_with_wrong_argspec(self): class RootController(object): @expose() def _lookup(self, someID): return 'Bad arg spec' # pragma: nocover with warnings.catch_warnings(): warnings.simplefilter("ignore") app = TestApp(Pecan(RootController(), use_context_locals=False)) r = app.get('/foo/bar', expect_errors=True) assert r.status_int == 404 class TestCanonicalLookups(PecanTestCase): @property def app_(self): class LookupController(object): def __init__(self, someID): self.someID = someID @expose() def index(self, req, resp): return self.someID class UserController(object): @expose() def _lookup(self, someID, *remainder): return LookupController(someID), remainder class RootController(object): users = UserController() return TestApp(Pecan(RootController(), use_context_locals=False)) def test_canonical_lookup(self): assert self.app_.get('/users', expect_errors=404).status_int == 404 assert self.app_.get('/users/', expect_errors=404).status_int == 404 assert self.app_.get('/users/100').status_int == 302 assert self.app_.get('/users/100/').body == b'100' class TestControllerArguments(PecanTestCase): @property def app_(self): class RootController(object): @expose() def index(self, req, resp, id): return 'index: %s' % id @expose() def multiple(self, req, resp, one, two): return 'multiple: %s, %s' % (one, two) @expose() def optional(self, req, resp, id=None): return 'optional: %s' % str(id) @expose() def multiple_optional(self, req, resp, one=None, two=None, three=None): return 'multiple_optional: %s, %s, %s' % (one, two, three) @expose() def variable_args(self, req, resp, *args): return 'variable_args: %s' % ', '.join(args) @expose() def variable_kwargs(self, req, resp, **kwargs): data = [ '%s=%s' % (key, kwargs[key]) for key in sorted(kwargs.keys()) ] return 'variable_kwargs: %s' % ', '.join(data) @expose() def variable_all(self, req, resp, *args, **kwargs): data = [ '%s=%s' % (key, kwargs[key]) for key in sorted(kwargs.keys()) ] return 'variable_all: %s' % ', '.join(list(args) + data) @expose() def eater(self, req, resp, id, dummy=None, *args, **kwargs): data = [ '%s=%s' % (key, kwargs[key]) for key in sorted(kwargs.keys()) ] return 'eater: %s, %s, %s' % ( id, dummy, ', '.join(list(args) + data) ) @expose() def _route(self, args, request): if hasattr(self, args[0]): return getattr(self, args[0]), args[1:] else: return self.index, args return TestApp(Pecan(RootController(), use_context_locals=False)) def test_required_argument(self): try: r = self.app_.get('/') assert r.status_int != 200 # pragma: nocover except Exception as ex: assert type(ex) == TypeError assert ex.args[0] in ( "index() takes exactly 2 arguments (1 given)", "index() missing 1 required positional argument: 'id'", ( "TestControllerArguments.app_..RootController." "index() missing 1 required positional argument: 'id'" ), ) # this messaging changed in Python 3.3 and again in Python 3.10 def test_single_argument(self): r = self.app_.get('/1') assert r.status_int == 200 assert r.body == b'index: 1' def test_single_argument_with_encoded_url(self): r = self.app_.get('/This%20is%20a%20test%21') assert r.status_int == 200 assert r.body == b'index: This is a test!' def test_two_arguments(self): r = self.app_.get('/1/dummy', status=404) assert r.status_int == 404 def test_keyword_argument(self): r = self.app_.get('/?id=2') assert r.status_int == 200 assert r.body == b'index: 2' def test_keyword_argument_with_encoded_url(self): r = self.app_.get('/?id=This%20is%20a%20test%21') assert r.status_int == 200 assert r.body == b'index: This is a test!' def test_argument_and_keyword_argument(self): r = self.app_.get('/3?id=three') assert r.status_int == 200 assert r.body == b'index: 3' def test_encoded_argument_and_keyword_argument(self): r = self.app_.get('/This%20is%20a%20test%21?id=three') assert r.status_int == 200 assert r.body == b'index: This is a test!' def test_explicit_kwargs(self): r = self.app_.post('/', {'id': '4'}) assert r.status_int == 200 assert r.body == b'index: 4' def test_path_with_explicit_kwargs(self): r = self.app_.post('/4', {'id': 'four'}) assert r.status_int == 200 assert r.body == b'index: 4' def test_multiple_kwargs(self): r = self.app_.get('/?id=5&dummy=dummy') assert r.status_int == 200 assert r.body == b'index: 5' def test_kwargs_from_root(self): r = self.app_.post('/', {'id': '6', 'dummy': 'dummy'}) assert r.status_int == 200 assert r.body == b'index: 6' # multiple args def test_multiple_positional_arguments(self): r = self.app_.get('/multiple/one/two') assert r.status_int == 200 assert r.body == b'multiple: one, two' def test_multiple_positional_arguments_with_url_encode(self): r = self.app_.get('/multiple/One%20/Two%21') assert r.status_int == 200 assert r.body == b'multiple: One , Two!' def test_multiple_positional_arguments_with_kwargs(self): r = self.app_.get('/multiple?one=three&two=four') assert r.status_int == 200 assert r.body == b'multiple: three, four' def test_multiple_positional_arguments_with_url_encoded_kwargs(self): r = self.app_.get('/multiple?one=Three%20&two=Four%20%21') assert r.status_int == 200 assert r.body == b'multiple: Three , Four !' def test_positional_args_with_dictionary_kwargs(self): r = self.app_.post('/multiple', {'one': 'five', 'two': 'six'}) assert r.status_int == 200 assert r.body == b'multiple: five, six' def test_positional_args_with_url_encoded_dictionary_kwargs(self): r = self.app_.post('/multiple', {'one': 'Five%20', 'two': 'Six%20%21'}) assert r.status_int == 200 assert r.body == b'multiple: Five%20, Six%20%21' # optional arg def test_optional_arg(self): r = self.app_.get('/optional') assert r.status_int == 200 assert r.body == b'optional: None' def test_multiple_optional(self): r = self.app_.get('/optional/1') assert r.status_int == 200 assert r.body == b'optional: 1' def test_multiple_optional_url_encoded(self): r = self.app_.get('/optional/Some%20Number') assert r.status_int == 200 assert r.body == b'optional: Some Number' def test_multiple_optional_missing(self): r = self.app_.get('/optional/2/dummy', status=404) assert r.status_int == 404 def test_multiple_with_kwargs(self): r = self.app_.get('/optional?id=2') assert r.status_int == 200 assert r.body == b'optional: 2' def test_multiple_with_url_encoded_kwargs(self): r = self.app_.get('/optional?id=Some%20Number') assert r.status_int == 200 assert r.body == b'optional: Some Number' def test_multiple_args_with_url_encoded_kwargs(self): r = self.app_.get('/optional/3?id=three') assert r.status_int == 200 assert r.body == b'optional: 3' def test_url_encoded_positional_args(self): r = self.app_.get('/optional/Some%20Number?id=three') assert r.status_int == 200 assert r.body == b'optional: Some Number' def test_optional_arg_with_kwargs(self): r = self.app_.post('/optional', {'id': '4'}) assert r.status_int == 200 assert r.body == b'optional: 4' def test_optional_arg_with_url_encoded_kwargs(self): r = self.app_.post('/optional', {'id': 'Some%20Number'}) assert r.status_int == 200 assert r.body == b'optional: Some%20Number' def test_multiple_positional_arguments_with_dictionary_kwargs(self): r = self.app_.post('/optional/5', {'id': 'five'}) assert r.status_int == 200 assert r.body == b'optional: 5' def test_multiple_positional_url_encoded_arguments_with_kwargs(self): r = self.app_.post('/optional/Some%20Number', {'id': 'five'}) assert r.status_int == 200 assert r.body == b'optional: Some Number' def test_optional_arg_with_multiple_kwargs(self): r = self.app_.get('/optional?id=6&dummy=dummy') assert r.status_int == 200 assert r.body == b'optional: 6' def test_optional_arg_with_multiple_url_encoded_kwargs(self): r = self.app_.get('/optional?id=Some%20Number&dummy=dummy') assert r.status_int == 200 assert r.body == b'optional: Some Number' def test_optional_arg_with_multiple_dictionary_kwargs(self): r = self.app_.post('/optional', {'id': '7', 'dummy': 'dummy'}) assert r.status_int == 200 assert r.body == b'optional: 7' def test_optional_arg_with_multiple_url_encoded_dictionary_kwargs(self): r = self.app_.post('/optional', { 'id': 'Some%20Number', 'dummy': 'dummy' }) assert r.status_int == 200 assert r.body == b'optional: Some%20Number' # multiple optional args def test_multiple_optional_positional_args(self): r = self.app_.get('/multiple_optional') assert r.status_int == 200 assert r.body == b'multiple_optional: None, None, None' def test_multiple_optional_positional_args_one_arg(self): r = self.app_.get('/multiple_optional/1') assert r.status_int == 200 assert r.body == b'multiple_optional: 1, None, None' def test_multiple_optional_positional_args_one_url_encoded_arg(self): r = self.app_.get('/multiple_optional/One%21') assert r.status_int == 200 assert r.body == b'multiple_optional: One!, None, None' def test_multiple_optional_positional_args_all_args(self): r = self.app_.get('/multiple_optional/1/2/3') assert r.status_int == 200 assert r.body == b'multiple_optional: 1, 2, 3' def test_multiple_optional_positional_args_all_url_encoded_args(self): r = self.app_.get('/multiple_optional/One%21/Two%21/Three%21') assert r.status_int == 200 assert r.body == b'multiple_optional: One!, Two!, Three!' def test_multiple_optional_positional_args_too_many_args(self): r = self.app_.get('/multiple_optional/1/2/3/dummy', status=404) assert r.status_int == 404 def test_multiple_optional_positional_args_with_kwargs(self): r = self.app_.get('/multiple_optional?one=1') assert r.status_int == 200 assert r.body == b'multiple_optional: 1, None, None' def test_multiple_optional_positional_args_with_url_encoded_kwargs(self): r = self.app_.get('/multiple_optional?one=One%21') assert r.status_int == 200 assert r.body == b'multiple_optional: One!, None, None' def test_multiple_optional_positional_args_with_string_kwargs(self): r = self.app_.get('/multiple_optional/1?one=one') assert r.status_int == 200 assert r.body == b'multiple_optional: 1, None, None' def test_multiple_optional_positional_args_with_encoded_str_kwargs(self): r = self.app_.get('/multiple_optional/One%21?one=one') assert r.status_int == 200 assert r.body == b'multiple_optional: One!, None, None' def test_multiple_optional_positional_args_with_dict_kwargs(self): r = self.app_.post('/multiple_optional', {'one': '1'}) assert r.status_int == 200 assert r.body == b'multiple_optional: 1, None, None' def test_multiple_optional_positional_args_with_encoded_dict_kwargs(self): r = self.app_.post('/multiple_optional', {'one': 'One%21'}) assert r.status_int == 200 assert r.body == b'multiple_optional: One%21, None, None' def test_multiple_optional_positional_args_and_dict_kwargs(self): r = self.app_.post('/multiple_optional/1', {'one': 'one'}) assert r.status_int == 200 assert r.body == b'multiple_optional: 1, None, None' def test_multiple_optional_encoded_positional_args_and_dict_kwargs(self): r = self.app_.post('/multiple_optional/One%21', {'one': 'one'}) assert r.status_int == 200 assert r.body == b'multiple_optional: One!, None, None' def test_multiple_optional_args_with_multiple_kwargs(self): r = self.app_.get('/multiple_optional?one=1&two=2&three=3&four=4') assert r.status_int == 200 assert r.body == b'multiple_optional: 1, 2, 3' def test_multiple_optional_args_with_multiple_encoded_kwargs(self): r = self.app_.get( '/multiple_optional?one=One%21&two=Two%21&three=Three%21&four=4' ) assert r.status_int == 200 assert r.body == b'multiple_optional: One!, Two!, Three!' def test_multiple_optional_args_with_multiple_dict_kwargs(self): r = self.app_.post( '/multiple_optional', {'one': '1', 'two': '2', 'three': '3', 'four': '4'} ) assert r.status_int == 200 assert r.body == b'multiple_optional: 1, 2, 3' def test_multiple_optional_args_with_multiple_encoded_dict_kwargs(self): r = self.app_.post( '/multiple_optional', { 'one': 'One%21', 'two': 'Two%21', 'three': 'Three%21', 'four': '4' } ) assert r.status_int == 200 assert r.body == b'multiple_optional: One%21, Two%21, Three%21' def test_multiple_optional_args_with_last_kwarg(self): r = self.app_.get('/multiple_optional?three=3') assert r.status_int == 200 assert r.body == b'multiple_optional: None, None, 3' def test_multiple_optional_args_with_last_encoded_kwarg(self): r = self.app_.get('/multiple_optional?three=Three%21') assert r.status_int == 200 assert r.body == b'multiple_optional: None, None, Three!' def test_multiple_optional_args_with_middle_arg(self): r = self.app_.get('/multiple_optional', {'two': '2'}) assert r.status_int == 200 assert r.body == b'multiple_optional: None, 2, None' def test_variable_args(self): r = self.app_.get('/variable_args') assert r.status_int == 200 assert r.body == b'variable_args: ' def test_multiple_variable_args(self): r = self.app_.get('/variable_args/1/dummy') assert r.status_int == 200 assert r.body == b'variable_args: 1, dummy' def test_multiple_encoded_variable_args(self): r = self.app_.get('/variable_args/Testing%20One%20Two/Three%21') assert r.status_int == 200 assert r.body == b'variable_args: Testing One Two, Three!' def test_variable_args_with_kwargs(self): r = self.app_.get('/variable_args?id=2&dummy=dummy') assert r.status_int == 200 assert r.body == b'variable_args: ' def test_variable_args_with_dict_kwargs(self): r = self.app_.post('/variable_args', {'id': '3', 'dummy': 'dummy'}) assert r.status_int == 200 assert r.body == b'variable_args: ' def test_variable_kwargs(self): r = self.app_.get('/variable_kwargs') assert r.status_int == 200 assert r.body == b'variable_kwargs: ' def test_multiple_variable_kwargs(self): r = self.app_.get('/variable_kwargs/1/dummy', status=404) assert r.status_int == 404 def test_multiple_variable_kwargs_with_explicit_kwargs(self): r = self.app_.get('/variable_kwargs?id=2&dummy=dummy') assert r.status_int == 200 assert r.body == b'variable_kwargs: dummy=dummy, id=2' def test_multiple_variable_kwargs_with_explicit_encoded_kwargs(self): r = self.app_.get( '/variable_kwargs?id=Two%21&dummy=This%20is%20a%20test' ) assert r.status_int == 200 assert r.body == b'variable_kwargs: dummy=This is a test, id=Two!' def test_multiple_variable_kwargs_with_dict_kwargs(self): r = self.app_.post('/variable_kwargs', {'id': '3', 'dummy': 'dummy'}) assert r.status_int == 200 assert r.body == b'variable_kwargs: dummy=dummy, id=3' def test_multiple_variable_kwargs_with_encoded_dict_kwargs(self): r = self.app_.post( '/variable_kwargs', {'id': 'Three%21', 'dummy': 'This%20is%20a%20test'} ) assert r.status_int == 200 result = b'variable_kwargs: dummy=This%20is%20a%20test, id=Three%21' assert r.body == result def test_variable_all(self): r = self.app_.get('/variable_all') assert r.status_int == 200 assert r.body == b'variable_all: ' def test_variable_all_with_one_extra(self): r = self.app_.get('/variable_all/1') assert r.status_int == 200 assert r.body == b'variable_all: 1' def test_variable_all_with_two_extras(self): r = self.app_.get('/variable_all/2/dummy') assert r.status_int == 200 assert r.body == b'variable_all: 2, dummy' def test_variable_mixed(self): r = self.app_.get('/variable_all/3?month=1&day=12') assert r.status_int == 200 assert r.body == b'variable_all: 3, day=12, month=1' def test_variable_mixed_explicit(self): r = self.app_.get('/variable_all/4?id=four&month=1&day=12') assert r.status_int == 200 assert r.body == b'variable_all: 4, day=12, id=four, month=1' def test_variable_post(self): r = self.app_.post('/variable_all/5/dummy') assert r.status_int == 200 assert r.body == b'variable_all: 5, dummy' def test_variable_post_with_kwargs(self): r = self.app_.post('/variable_all/6', {'month': '1', 'day': '12'}) assert r.status_int == 200 assert r.body == b'variable_all: 6, day=12, month=1' def test_variable_post_mixed(self): r = self.app_.post( '/variable_all/7', {'id': 'seven', 'month': '1', 'day': '12'} ) assert r.status_int == 200 assert r.body == b'variable_all: 7, day=12, id=seven, month=1' def test_no_remainder(self): try: r = self.app_.get('/eater') assert r.status_int != 200 # pragma: nocover except Exception as ex: assert type(ex) == TypeError assert ex.args[0] in ( "eater() takes exactly 2 arguments (1 given)", "eater() missing 1 required positional argument: 'id'", ( "TestControllerArguments.app_..RootController." "eater() missing 1 required positional argument: 'id'" ), ) # this messaging changed in Python 3.3 and again in Python 3.10 def test_one_remainder(self): r = self.app_.get('/eater/1') assert r.status_int == 200 assert r.body == b'eater: 1, None, ' def test_two_remainders(self): r = self.app_.get('/eater/2/dummy') assert r.status_int == 200 assert r.body == b'eater: 2, dummy, ' def test_many_remainders(self): r = self.app_.get('/eater/3/dummy/foo/bar') assert r.status_int == 200 assert r.body == b'eater: 3, dummy, foo, bar' def test_remainder_with_kwargs(self): r = self.app_.get('/eater/4?month=1&day=12') assert r.status_int == 200 assert r.body == b'eater: 4, None, day=12, month=1' def test_remainder_with_many_kwargs(self): r = self.app_.get('/eater/5?id=five&month=1&day=12&dummy=dummy') assert r.status_int == 200 assert r.body == b'eater: 5, dummy, day=12, month=1' def test_post_remainder(self): r = self.app_.post('/eater/6') assert r.status_int == 200 assert r.body == b'eater: 6, None, ' def test_post_three_remainders(self): r = self.app_.post('/eater/7/dummy') assert r.status_int == 200 assert r.body == b'eater: 7, dummy, ' def test_post_many_remainders(self): r = self.app_.post('/eater/8/dummy/foo/bar') assert r.status_int == 200 assert r.body == b'eater: 8, dummy, foo, bar' def test_post_remainder_with_kwargs(self): r = self.app_.post('/eater/9', {'month': '1', 'day': '12'}) assert r.status_int == 200 assert r.body == b'eater: 9, None, day=12, month=1' def test_post_many_remainders_with_many_kwargs(self): r = self.app_.post( '/eater/10', {'id': 'ten', 'month': '1', 'day': '12', 'dummy': 'dummy'} ) assert r.status_int == 200 assert r.body == b'eater: 10, dummy, day=12, month=1' class TestRestController(PecanTestCase): @property def app_(self): class OthersController(object): @expose() def index(self, req, resp): return 'OTHERS' @expose() def echo(self, req, resp, value): return str(value) class ThingsController(RestController): data = ['zero', 'one', 'two', 'three'] _custom_actions = {'count': ['GET'], 'length': ['GET', 'POST']} others = OthersController() @expose() def get_one(self, req, resp, id): return self.data[int(id)] @expose('json') def get_all(self, req, resp): return dict(items=self.data) @expose() def length(self, req, resp, id, value=None): length = len(self.data[int(id)]) if value: length += len(value) return str(length) @expose() def post(self, req, resp, value): self.data.append(value) resp.status = 302 return 'CREATED' @expose() def edit(self, req, resp, id): return 'EDIT %s' % self.data[int(id)] @expose() def put(self, req, resp, id, value): self.data[int(id)] = value return 'UPDATED' @expose() def get_delete(self, req, resp, id): return 'DELETE %s' % self.data[int(id)] @expose() def delete(self, req, resp, id): del self.data[int(id)] return 'DELETED' @expose() def trace(self, req, resp): return 'TRACE' @expose() def post_options(self, req, resp): return 'OPTIONS' @expose() def options(self, req, resp): abort(500) @expose() def other(self, req, resp): abort(500) class RootController(object): things = ThingsController() # create the app return TestApp(Pecan(RootController(), use_context_locals=False)) def test_get_all(self): r = self.app_.get('/things') assert r.status_int == 200 assert r.body == dumps( dict(items=['zero', 'one', 'two', 'three']) ).encode('utf-8') def test_get_one(self): for i, value in enumerate([b'zero', b'one', b'two', b'three']): r = self.app_.get('/things/%d' % i) assert r.status_int == 200 assert r.body == value def test_post(self): r = self.app_.post('/things', {'value': 'four'}) assert r.status_int == 302 assert r.body == b'CREATED' def test_custom_action(self): r = self.app_.get('/things/3/edit') assert r.status_int == 200 assert r.body == b'EDIT three' def test_put(self): r = self.app_.put('/things/3', {'value': 'THREE!'}) assert r.status_int == 200 assert r.body == b'UPDATED' def test_put_with_method_parameter_and_get(self): r = self.app_.get('/things/3?_method=put', {'value': 'X'}, status=405) assert r.status_int == 405 def test_put_with_method_parameter_and_post(self): r = self.app_.post('/things/3?_method=put', {'value': 'THREE!'}) assert r.status_int == 200 assert r.body == b'UPDATED' def test_get_delete(self): r = self.app_.get('/things/3/delete') assert r.status_int == 200 assert r.body == b'DELETE three' def test_delete_method(self): r = self.app_.delete('/things/3') assert r.status_int == 200 assert r.body == b'DELETED' def test_delete_with_method_parameter(self): r = self.app_.get('/things/3?_method=DELETE', status=405) assert r.status_int == 405 def test_delete_with_method_parameter_and_post(self): r = self.app_.post('/things/3?_method=DELETE') assert r.status_int == 200 assert r.body == b'DELETED' def test_custom_method_type(self): r = self.app_.request('/things', method='TRACE') assert r.status_int == 200 assert r.body == b'TRACE' def test_custom_method_type_with_method_parameter(self): r = self.app_.get('/things?_method=TRACE') assert r.status_int == 200 assert r.body == b'TRACE' def test_options(self): r = self.app_.request('/things', method='OPTIONS') assert r.status_int == 200 assert r.body == b'OPTIONS' def test_options_with_method_parameter(self): r = self.app_.post('/things', {'_method': 'OPTIONS'}) assert r.status_int == 200 assert r.body == b'OPTIONS' def test_other_custom_action(self): with warnings.catch_warnings(): warnings.simplefilter("ignore") r = self.app_.request('/things/other', method='MISC', status=405) assert r.status_int == 405 def test_other_custom_action_with_method_parameter(self): r = self.app_.post('/things/other', {'_method': 'MISC'}, status=405) assert r.status_int == 405 def test_nested_controller_with_trailing_slash(self): with warnings.catch_warnings(): warnings.simplefilter("ignore") r = self.app_.request('/things/others/', method='MISC') assert r.status_int == 200 assert r.body == b'OTHERS' def test_nested_controller_without_trailing_slash(self): with warnings.catch_warnings(): warnings.simplefilter("ignore") r = self.app_.request('/things/others', method='MISC', status=302) assert r.status_int == 302 def test_invalid_custom_action(self): r = self.app_.get('/things?_method=BAD', status=405) assert r.status_int == 405 def test_named_action(self): # test custom "GET" request "length" r = self.app_.get('/things/1/length') assert r.status_int == 200 assert r.body == b'3' def test_named_nested_action(self): # test custom "GET" request through subcontroller r = self.app_.get('/things/others/echo?value=test') assert r.status_int == 200 assert r.body == b'test' def test_nested_post(self): # test custom "POST" request through subcontroller r = self.app_.post('/things/others/echo', {'value': 'test'}) assert r.status_int == 200 assert r.body == b'test' class TestHooks(PecanTestCase): def test_basic_single_hook(self): run_hook = [] class RootController(object): @expose() def index(self, req, resp): run_hook.append('inside') return 'Hello, World!' class SimpleHook(PecanHook): def on_route(self, state): run_hook.append('on_route') def before(self, state): run_hook.append('before') def after(self, state): run_hook.append('after') def on_error(self, state, e): run_hook.append('error') app = TestApp(Pecan( RootController(), hooks=[SimpleHook()], use_context_locals=False )) response = app.get('/') assert response.status_int == 200 assert response.body == b'Hello, World!' assert len(run_hook) == 4 assert run_hook[0] == 'on_route' assert run_hook[1] == 'before' assert run_hook[2] == 'inside' assert run_hook[3] == 'after' def test_basic_multi_hook(self): run_hook = [] class RootController(object): @expose() def index(self, req, resp): run_hook.append('inside') return 'Hello, World!' class SimpleHook(PecanHook): def __init__(self, id): self.id = str(id) def on_route(self, state): run_hook.append('on_route' + self.id) def before(self, state): run_hook.append('before' + self.id) def after(self, state): run_hook.append('after' + self.id) def on_error(self, state, e): run_hook.append('error' + self.id) app = TestApp(Pecan(RootController(), hooks=[ SimpleHook(1), SimpleHook(2), SimpleHook(3) ], use_context_locals=False)) response = app.get('/') assert response.status_int == 200 assert response.body == b'Hello, World!' assert len(run_hook) == 10 assert run_hook[0] == 'on_route1' assert run_hook[1] == 'on_route2' assert run_hook[2] == 'on_route3' assert run_hook[3] == 'before1' assert run_hook[4] == 'before2' assert run_hook[5] == 'before3' assert run_hook[6] == 'inside' assert run_hook[7] == 'after3' assert run_hook[8] == 'after2' assert run_hook[9] == 'after1' def test_partial_hooks(self): run_hook = [] class RootController(object): @expose() def index(self, req, resp): run_hook.append('inside') return 'Hello World!' @expose() def causeerror(self, req, resp): return [][1] class ErrorHook(PecanHook): def on_error(self, state, e): run_hook.append('error') class OnRouteHook(PecanHook): def on_route(self, state): run_hook.append('on_route') app = TestApp(Pecan(RootController(), hooks=[ ErrorHook(), OnRouteHook() ], use_context_locals=False)) response = app.get('/') assert response.status_int == 200 assert response.body == b'Hello World!' assert len(run_hook) == 2 assert run_hook[0] == 'on_route' assert run_hook[1] == 'inside' run_hook = [] try: response = app.get('/causeerror') except Exception as e: assert isinstance(e, IndexError) assert len(run_hook) == 2 assert run_hook[0] == 'on_route' assert run_hook[1] == 'error' def test_on_error_response_hook(self): run_hook = [] class RootController(object): @expose() def causeerror(self, req, resp): return [][1] class ErrorHook(PecanHook): def on_error(self, state, e): run_hook.append('error') r = webob.Response() r.text = 'on_error' return r app = TestApp(Pecan(RootController(), hooks=[ ErrorHook() ], use_context_locals=False)) response = app.get('/causeerror') assert len(run_hook) == 1 assert run_hook[0] == 'error' assert response.text == 'on_error' def test_prioritized_hooks(self): run_hook = [] class RootController(object): @expose() def index(self, req, resp): run_hook.append('inside') return 'Hello, World!' class SimpleHook(PecanHook): def __init__(self, id, priority=None): self.id = str(id) if priority: self.priority = priority def on_route(self, state): run_hook.append('on_route' + self.id) def before(self, state): run_hook.append('before' + self.id) def after(self, state): run_hook.append('after' + self.id) def on_error(self, state, e): run_hook.append('error' + self.id) papp = Pecan(RootController(), hooks=[ SimpleHook(1, 3), SimpleHook(2, 2), SimpleHook(3, 1) ], use_context_locals=False) app = TestApp(papp) response = app.get('/') assert response.status_int == 200 assert response.body == b'Hello, World!' assert len(run_hook) == 10 assert run_hook[0] == 'on_route3' assert run_hook[1] == 'on_route2' assert run_hook[2] == 'on_route1' assert run_hook[3] == 'before3' assert run_hook[4] == 'before2' assert run_hook[5] == 'before1' assert run_hook[6] == 'inside' assert run_hook[7] == 'after1' assert run_hook[8] == 'after2' assert run_hook[9] == 'after3' def test_basic_isolated_hook(self): run_hook = [] class SimpleHook(PecanHook): def on_route(self, state): run_hook.append('on_route') def before(self, state): run_hook.append('before') def after(self, state): run_hook.append('after') def on_error(self, state, e): run_hook.append('error') class SubSubController(object): @expose() def index(self, req, resp): run_hook.append('inside_sub_sub') return 'Deep inside here!' class SubController(HookController): __hooks__ = [SimpleHook()] @expose() def index(self, req, resp): run_hook.append('inside_sub') return 'Inside here!' sub = SubSubController() class RootController(object): @expose() def index(self, req, resp): run_hook.append('inside') return 'Hello, World!' sub = SubController() app = TestApp(Pecan(RootController(), use_context_locals=False)) response = app.get('/') assert response.status_int == 200 assert response.body == b'Hello, World!' assert len(run_hook) == 1 assert run_hook[0] == 'inside' run_hook = [] response = app.get('/sub/') assert response.status_int == 200 assert response.body == b'Inside here!' assert len(run_hook) == 3 assert run_hook[0] == 'before' assert run_hook[1] == 'inside_sub' assert run_hook[2] == 'after' run_hook = [] response = app.get('/sub/sub/') assert response.status_int == 200 assert response.body == b'Deep inside here!' assert len(run_hook) == 3 assert run_hook[0] == 'before' assert run_hook[1] == 'inside_sub_sub' assert run_hook[2] == 'after' def test_isolated_hook_with_global_hook(self): run_hook = [] class SimpleHook(PecanHook): def __init__(self, id): self.id = str(id) def on_route(self, state): run_hook.append('on_route' + self.id) def before(self, state): run_hook.append('before' + self.id) def after(self, state): run_hook.append('after' + self.id) def on_error(self, state, e): run_hook.append('error' + self.id) class SubController(HookController): __hooks__ = [SimpleHook(2)] @expose() def index(self, req, resp): run_hook.append('inside_sub') return 'Inside here!' class RootController(object): @expose() def index(self, req, resp): run_hook.append('inside') return 'Hello, World!' sub = SubController() app = TestApp(Pecan( RootController(), hooks=[SimpleHook(1)], use_context_locals=False )) response = app.get('/') assert response.status_int == 200 assert response.body == b'Hello, World!' assert len(run_hook) == 4 assert run_hook[0] == 'on_route1' assert run_hook[1] == 'before1' assert run_hook[2] == 'inside' assert run_hook[3] == 'after1' run_hook = [] response = app.get('/sub/') assert response.status_int == 200 assert response.body == b'Inside here!' assert len(run_hook) == 6 assert run_hook[0] == 'on_route1' assert run_hook[1] == 'before2' assert run_hook[2] == 'before1' assert run_hook[3] == 'inside_sub' assert run_hook[4] == 'after1' assert run_hook[5] == 'after2' class TestGeneric(PecanTestCase): @property def root(self): class RootController(object): def __init__(self, unique): self.unique = unique @expose(generic=True, template='json') def index(self, req, resp): assert self.__class__.__name__ == 'RootController' assert isinstance(req, Request) assert isinstance(resp, Response) assert self.unique == req.headers.get('X-Unique') return {'hello': 'world'} @index.when(method='POST', template='json') def index_post(self, req, resp): assert self.__class__.__name__ == 'RootController' assert isinstance(req, Request) assert isinstance(resp, Response) assert self.unique == req.headers.get('X-Unique') return req.json @expose(template='json') def echo(self, req, resp): assert self.__class__.__name__ == 'RootController' assert isinstance(req, Request) assert isinstance(resp, Response) assert self.unique == req.headers.get('X-Unique') return req.json @expose(template='json') def extra(self, req, resp, first, second): assert self.__class__.__name__ == 'RootController' assert isinstance(req, Request) assert isinstance(resp, Response) assert self.unique == req.headers.get('X-Unique') return {'first': first, 'second': second} return RootController def test_generics_with_im_self_default(self): uniq = str(time.time()) with mock.patch('threading.local', side_effect=AssertionError()): app = TestApp(Pecan(self.root(uniq), use_context_locals=False)) r = app.get('/', headers={'X-Unique': uniq}) assert r.status_int == 200 json_resp = loads(r.body.decode()) assert json_resp['hello'] == 'world' def test_generics_with_im_self_with_method(self): uniq = str(time.time()) with mock.patch('threading.local', side_effect=AssertionError()): app = TestApp(Pecan(self.root(uniq), use_context_locals=False)) r = app.post_json('/', {'foo': 'bar'}, headers={'X-Unique': uniq}) assert r.status_int == 200 json_resp = loads(r.body.decode()) assert json_resp['foo'] == 'bar' def test_generics_with_im_self_with_path(self): uniq = str(time.time()) with mock.patch('threading.local', side_effect=AssertionError()): app = TestApp(Pecan(self.root(uniq), use_context_locals=False)) r = app.post_json('/echo/', {'foo': 'bar'}, headers={'X-Unique': uniq}) assert r.status_int == 200 json_resp = loads(r.body.decode()) assert json_resp['foo'] == 'bar' def test_generics_with_im_self_with_extra_args(self): uniq = str(time.time()) with mock.patch('threading.local', side_effect=AssertionError()): app = TestApp(Pecan(self.root(uniq), use_context_locals=False)) r = app.get('/extra/123/456', headers={'X-Unique': uniq}) assert r.status_int == 200 json_resp = loads(r.body.decode()) assert json_resp['first'] == '123' assert json_resp['second'] == '456' pecan-1.5.1/pecan/tests/test_rest.py000066400000000000000000001276551445453044500174520ustar00rootroot00000000000000# -*- coding: utf-8 -*- from json import dumps, loads import unittest import struct import sys import warnings from webtest import TestApp from pecan import abort, expose, make_app, response, redirect from pecan.rest import RestController from pecan.tests import PecanTestCase class TestRestController(PecanTestCase): def test_basic_rest(self): class OthersController(object): @expose() def index(self): return 'OTHERS' @expose() def echo(self, value): return str(value) class ThingsController(RestController): data = ['zero', 'one', 'two', 'three'] _custom_actions = {'count': ['GET'], 'length': ['GET', 'POST']} others = OthersController() @expose() def get_one(self, id): return self.data[int(id)] @expose('json') def get_all(self): return dict(items=self.data) @expose() def length(self, id, value=None): length = len(self.data[int(id)]) if value: length += len(value) return str(length) @expose() def get_count(self): return str(len(self.data)) @expose() def new(self): return 'NEW' @expose() def post(self, value): self.data.append(value) response.status = 302 return 'CREATED' @expose() def edit(self, id): return 'EDIT %s' % self.data[int(id)] @expose() def put(self, id, value): self.data[int(id)] = value return 'UPDATED' @expose() def get_delete(self, id): return 'DELETE %s' % self.data[int(id)] @expose() def delete(self, id): del self.data[int(id)] return 'DELETED' @expose() def trace(self): return 'TRACE' @expose() def post_options(self): return 'OPTIONS' @expose() def options(self): abort(500) @expose() def other(self): abort(500) class RootController(object): things = ThingsController() # create the app app = TestApp(make_app(RootController())) # test get_all r = app.get('/things') assert r.status_int == 200 assert r.body == dumps( dict(items=ThingsController.data) ).encode('utf-8') # test get_one for i, value in enumerate(ThingsController.data): r = app.get('/things/%d' % i) assert r.status_int == 200 assert r.body == value.encode('utf-8') # test post r = app.post('/things', {'value': 'four'}) assert r.status_int == 302 assert r.body == b'CREATED' # make sure it works r = app.get('/things/4') assert r.status_int == 200 assert r.body == b'four' # test edit r = app.get('/things/3/edit') assert r.status_int == 200 assert r.body == b'EDIT three' # test put r = app.put('/things/4', {'value': 'FOUR'}) assert r.status_int == 200 assert r.body == b'UPDATED' # make sure it works r = app.get('/things/4') assert r.status_int == 200 assert r.body == b'FOUR' # test put with _method parameter and GET r = app.get('/things/4?_method=put', {'value': 'FOUR!'}, status=405) assert r.status_int == 405 # make sure it works r = app.get('/things/4') assert r.status_int == 200 assert r.body == b'FOUR' # test put with _method parameter and POST r = app.post('/things/4?_method=put', {'value': 'FOUR!'}) assert r.status_int == 200 assert r.body == b'UPDATED' # make sure it works r = app.get('/things/4') assert r.status_int == 200 assert r.body == b'FOUR!' # test get delete r = app.get('/things/4/delete') assert r.status_int == 200 assert r.body == b'DELETE FOUR!' # test delete r = app.delete('/things/4') assert r.status_int == 200 assert r.body == b'DELETED' # make sure it works r = app.get('/things') assert r.status_int == 200 assert len(loads(r.body.decode())['items']) == 4 # test delete with _method parameter and GET r = app.get('/things/3?_method=DELETE', status=405) assert r.status_int == 405 # make sure it works r = app.get('/things') assert r.status_int == 200 assert len(loads(r.body.decode())['items']) == 4 # test delete with _method parameter and POST r = app.post('/things/3?_method=DELETE') assert r.status_int == 200 assert r.body == b'DELETED' # make sure it works r = app.get('/things') assert r.status_int == 200 assert len(loads(r.body.decode())['items']) == 3 # test "TRACE" custom action r = app.request('/things', method='TRACE') assert r.status_int == 200 assert r.body == b'TRACE' # test "TRACE" custom action with _method parameter r = app.get('/things?_method=TRACE') assert r.status_int == 200 assert r.body == b'TRACE' # test the "OPTIONS" custom action r = app.request('/things', method='OPTIONS') assert r.status_int == 200 assert r.body == b'OPTIONS' # test the "OPTIONS" custom action with the _method parameter r = app.post('/things', {'_method': 'OPTIONS'}) assert r.status_int == 200 assert r.body == b'OPTIONS' # test the "other" custom action with warnings.catch_warnings(): warnings.simplefilter("ignore") r = app.request('/things/other', method='MISC', status=405) assert r.status_int == 405 # test the "other" custom action with the _method parameter r = app.post('/things/other', {'_method': 'MISC'}, status=405) assert r.status_int == 405 # test the "others" custom action with warnings.catch_warnings(): warnings.simplefilter("ignore") r = app.request('/things/others/', method='MISC') assert r.status_int == 200 assert r.body == b'OTHERS' # test the "others" custom action missing trailing slash with warnings.catch_warnings(): warnings.simplefilter("ignore") r = app.request('/things/others', method='MISC', status=302) assert r.status_int == 302 # test the "others" custom action with the _method parameter r = app.get('/things/others/?_method=MISC') assert r.status_int == 200 assert r.body == b'OTHERS' # test an invalid custom action r = app.get('/things?_method=BAD', status=405) assert r.status_int == 405 # test custom "GET" request "count" r = app.get('/things/count') assert r.status_int == 200 assert r.body == b'3' # test custom "GET" request "length" r = app.get('/things/1/length') assert r.status_int == 200 assert r.body == b'3' # test custom "GET" request through subcontroller r = app.get('/things/others/echo?value=test') assert r.status_int == 200 assert r.body == b'test' # test custom "POST" request "length" r = app.post('/things/1/length', {'value': 'test'}) assert r.status_int == 200 assert r.body == b'7' # test custom "POST" request through subcontroller r = app.post('/things/others/echo', {'value': 'test'}) assert r.status_int == 200 assert r.body == b'test' def test_getall_with_trailing_slash(self): class ThingsController(RestController): data = ['zero', 'one', 'two', 'three'] @expose('json') def get_all(self): return dict(items=self.data) class RootController(object): things = ThingsController() # create the app app = TestApp(make_app(RootController())) # test get_all r = app.get('/things/') assert r.status_int == 200 assert r.body == dumps( dict(items=ThingsController.data) ).encode('utf-8') def test_405_with_lookup(self): class LookupController(RestController): def __init__(self, _id): self._id = _id @expose() def get_all(self): return 'ID: %s' % self._id class ThingsController(RestController): @expose() def _lookup(self, _id, *remainder): return LookupController(_id), remainder class RootController(object): things = ThingsController() # create the app app = TestApp(make_app(RootController())) # these should 405 for path in ('/things', '/things/'): r = app.get(path, expect_errors=True) assert r.status_int == 405 r = app.get('/things/foo') assert r.status_int == 200 assert r.body == b'ID: foo' def test_getall_with_lookup(self): class LookupController(RestController): def __init__(self, _id): self._id = _id @expose() def get_all(self): return 'ID: %s' % self._id class ThingsController(RestController): data = ['zero', 'one', 'two', 'three'] @expose() def _lookup(self, _id, *remainder): return LookupController(_id), remainder @expose('json') def get_all(self): return dict(items=self.data) class RootController(object): things = ThingsController() # create the app app = TestApp(make_app(RootController())) # test get_all for path in ('/things', '/things/'): r = app.get(path) assert r.status_int == 200 assert r.body == dumps( dict(items=ThingsController.data) ).encode('utf-8') r = app.get('/things/foo') assert r.status_int == 200 assert r.body == b'ID: foo' def test_simple_nested_rest(self): class BarController(RestController): @expose() def post(self): return "BAR-POST" @expose() def delete(self, id_): return "BAR-%s" % id_ class FooController(RestController): bar = BarController() @expose() def post(self): return "FOO-POST" @expose() def delete(self, id_): return "FOO-%s" % id_ class RootController(object): foo = FooController() # create the app app = TestApp(make_app(RootController())) r = app.post('/foo') assert r.status_int == 200 assert r.body == b"FOO-POST" r = app.delete('/foo/1') assert r.status_int == 200 assert r.body == b"FOO-1" r = app.post('/foo/bar') assert r.status_int == 200 assert r.body == b"BAR-POST" r = app.delete('/foo/bar/2') assert r.status_int == 200 assert r.body == b"BAR-2" def test_complicated_nested_rest(self): class BarsController(RestController): data = [['zero-zero', 'zero-one'], ['one-zero', 'one-one']] @expose() def get_one(self, foo_id, id): return self.data[int(foo_id)][int(id)] @expose('json') def get_all(self, foo_id): return dict(items=self.data[int(foo_id)]) @expose() def new(self, foo_id): return 'NEW FOR %s' % foo_id @expose() def post(self, foo_id, value): foo_id = int(foo_id) if len(self.data) < foo_id + 1: self.data.extend([[]] * (foo_id - len(self.data) + 1)) self.data[foo_id].append(value) response.status = 302 return 'CREATED FOR %s' % foo_id @expose() def edit(self, foo_id, id): return 'EDIT %s' % self.data[int(foo_id)][int(id)] @expose() def put(self, foo_id, id, value): self.data[int(foo_id)][int(id)] = value return 'UPDATED' @expose() def get_delete(self, foo_id, id): return 'DELETE %s' % self.data[int(foo_id)][int(id)] @expose() def delete(self, foo_id, id): del self.data[int(foo_id)][int(id)] return 'DELETED' class FoosController(RestController): data = ['zero', 'one'] bars = BarsController() @expose() def get_one(self, id): return self.data[int(id)] @expose('json') def get_all(self): return dict(items=self.data) @expose() def new(self): return 'NEW' @expose() def edit(self, id): return 'EDIT %s' % self.data[int(id)] @expose() def post(self, value): self.data.append(value) response.status = 302 return 'CREATED' @expose() def put(self, id, value): self.data[int(id)] = value return 'UPDATED' @expose() def get_delete(self, id): return 'DELETE %s' % self.data[int(id)] @expose() def delete(self, id): del self.data[int(id)] return 'DELETED' class RootController(object): foos = FoosController() # create the app app = TestApp(make_app(RootController())) # test get_all r = app.get('/foos') assert r.status_int == 200 assert r.body == dumps( dict(items=FoosController.data) ).encode('utf-8') # test nested get_all r = app.get('/foos/1/bars') assert r.status_int == 200 assert r.body == dumps( dict(items=BarsController.data[1]) ).encode('utf-8') # test get_one for i, value in enumerate(FoosController.data): r = app.get('/foos/%d' % i) assert r.status_int == 200 assert r.body == value.encode('utf-8') # test nested get_one for i, value in enumerate(FoosController.data): for j, value in enumerate(BarsController.data[i]): r = app.get('/foos/%s/bars/%s' % (i, j)) assert r.status_int == 200 assert r.body == value.encode('utf-8') # test post r = app.post('/foos', {'value': 'two'}) assert r.status_int == 302 assert r.body == b'CREATED' # make sure it works r = app.get('/foos/2') assert r.status_int == 200 assert r.body == b'two' # test nested post r = app.post('/foos/2/bars', {'value': 'two-zero'}) assert r.status_int == 302 assert r.body == b'CREATED FOR 2' # make sure it works r = app.get('/foos/2/bars/0') assert r.status_int == 200 assert r.body == b'two-zero' # test edit r = app.get('/foos/1/edit') assert r.status_int == 200 assert r.body == b'EDIT one' # test nested edit r = app.get('/foos/1/bars/1/edit') assert r.status_int == 200 assert r.body == b'EDIT one-one' # test put r = app.put('/foos/2', {'value': 'TWO'}) assert r.status_int == 200 assert r.body == b'UPDATED' # make sure it works r = app.get('/foos/2') assert r.status_int == 200 assert r.body == b'TWO' # test nested put r = app.put('/foos/2/bars/0', {'value': 'TWO-ZERO'}) assert r.status_int == 200 assert r.body == b'UPDATED' # make sure it works r = app.get('/foos/2/bars/0') assert r.status_int == 200 assert r.body == b'TWO-ZERO' # test put with _method parameter and GET r = app.get('/foos/2?_method=put', {'value': 'TWO!'}, status=405) assert r.status_int == 405 # make sure it works r = app.get('/foos/2') assert r.status_int == 200 assert r.body == b'TWO' # test nested put with _method parameter and GET r = app.get( '/foos/2/bars/0?_method=put', {'value': 'ZERO-TWO!'}, status=405 ) assert r.status_int == 405 # make sure it works r = app.get('/foos/2/bars/0') assert r.status_int == 200 assert r.body == b'TWO-ZERO' # test put with _method parameter and POST r = app.post('/foos/2?_method=put', {'value': 'TWO!'}) assert r.status_int == 200 assert r.body == b'UPDATED' # make sure it works r = app.get('/foos/2') assert r.status_int == 200 assert r.body == b'TWO!' # test nested put with _method parameter and POST r = app.post('/foos/2/bars/0?_method=put', {'value': 'TWO-ZERO!'}) assert r.status_int == 200 assert r.body == b'UPDATED' # make sure it works r = app.get('/foos/2/bars/0') assert r.status_int == 200 assert r.body == b'TWO-ZERO!' # test get delete r = app.get('/foos/2/delete') assert r.status_int == 200 assert r.body == b'DELETE TWO!' # test nested get delete r = app.get('/foos/2/bars/0/delete') assert r.status_int == 200 assert r.body == b'DELETE TWO-ZERO!' # test nested delete r = app.delete('/foos/2/bars/0') assert r.status_int == 200 assert r.body == b'DELETED' # make sure it works r = app.get('/foos/2/bars') assert r.status_int == 200 assert len(loads(r.body.decode())['items']) == 0 # test delete r = app.delete('/foos/2') assert r.status_int == 200 assert r.body == b'DELETED' # make sure it works r = app.get('/foos') assert r.status_int == 200 assert len(loads(r.body.decode())['items']) == 2 # test nested delete with _method parameter and GET r = app.get('/foos/1/bars/1?_method=DELETE', status=405) assert r.status_int == 405 # make sure it works r = app.get('/foos/1/bars') assert r.status_int == 200 assert len(loads(r.body.decode())['items']) == 2 # test delete with _method parameter and GET r = app.get('/foos/1?_method=DELETE', status=405) assert r.status_int == 405 # make sure it works r = app.get('/foos') assert r.status_int == 200 assert len(loads(r.body.decode())['items']) == 2 # test nested delete with _method parameter and POST r = app.post('/foos/1/bars/1?_method=DELETE') assert r.status_int == 200 assert r.body == b'DELETED' # make sure it works r = app.get('/foos/1/bars') assert r.status_int == 200 assert len(loads(r.body.decode())['items']) == 1 # test delete with _method parameter and POST r = app.post('/foos/1?_method=DELETE') assert r.status_int == 200 assert r.body == b'DELETED' # make sure it works r = app.get('/foos') assert r.status_int == 200 assert len(loads(r.body.decode())['items']) == 1 def test_nested_get_all(self): class BarsController(RestController): @expose() def get_one(self, foo_id, id): return '4' @expose() def get_all(self, foo_id): return '3' class FoosController(RestController): bars = BarsController() @expose() def get_one(self, id): return '2' @expose() def get_all(self): return '1' class RootController(object): foos = FoosController() # create the app app = TestApp(make_app(RootController())) r = app.get('/foos/') assert r.status_int == 200 assert r.body == b'1' r = app.get('/foos/1/') assert r.status_int == 200 assert r.body == b'2' r = app.get('/foos/1/bars/') assert r.status_int == 200 assert r.body == b'3' r = app.get('/foos/1/bars/2/') assert r.status_int == 200 assert r.body == b'4' r = app.get('/foos/bars/', status=404) assert r.status_int == 404 r = app.get('/foos/bars/1', status=404) assert r.status_int == 404 def test_nested_get_all_with_lookup(self): class BarsController(RestController): @expose() def get_one(self, foo_id, id): return '4' @expose() def get_all(self, foo_id): return '3' @expose('json') def _lookup(self, id, *remainder): redirect('/lookup-hit/') class FoosController(RestController): bars = BarsController() @expose() def get_one(self, id): return '2' @expose() def get_all(self): return '1' class RootController(object): foos = FoosController() # create the app app = TestApp(make_app(RootController())) r = app.get('/foos/') assert r.status_int == 200 assert r.body == b'1' r = app.get('/foos/1/') assert r.status_int == 200 assert r.body == b'2' r = app.get('/foos/1/bars/') assert r.status_int == 200 assert r.body == b'3' r = app.get('/foos/1/bars/2/') assert r.status_int == 200 assert r.body == b'4' r = app.get('/foos/bars/') assert r.status_int == 302 assert r.headers['Location'].endswith('/lookup-hit/') r = app.get('/foos/bars/1') assert r.status_int == 302 assert r.headers['Location'].endswith('/lookup-hit/') def test_bad_rest(self): class ThingsController(RestController): pass class RootController(object): things = ThingsController() # create the app app = TestApp(make_app(RootController())) # test get_all r = app.get('/things', status=405) assert r.status_int == 405 # test get_one r = app.get('/things/1', status=405) assert r.status_int == 405 # test post r = app.post('/things', {'value': 'one'}, status=405) assert r.status_int == 405 # test edit r = app.get('/things/1/edit', status=405) assert r.status_int == 405 # test put r = app.put('/things/1', {'value': 'ONE'}, status=405) # test put with _method parameter and GET r = app.get('/things/1?_method=put', {'value': 'ONE!'}, status=405) assert r.status_int == 405 # test put with _method parameter and POST r = app.post('/things/1?_method=put', {'value': 'ONE!'}, status=405) assert r.status_int == 405 # test get delete r = app.get('/things/1/delete', status=405) assert r.status_int == 405 # test delete r = app.delete('/things/1', status=405) assert r.status_int == 405 # test delete with _method parameter and GET r = app.get('/things/1?_method=DELETE', status=405) assert r.status_int == 405 # test delete with _method parameter and POST r = app.post('/things/1?_method=DELETE', status=405) assert r.status_int == 405 # test "TRACE" custom action with warnings.catch_warnings(): warnings.simplefilter("ignore") r = app.request('/things', method='TRACE', status=405) assert r.status_int == 405 def test_nested_rest_with_missing_intermediate_id(self): class BarsController(RestController): data = [['zero-zero', 'zero-one'], ['one-zero', 'one-one']] @expose('json') def get_all(self, foo_id): return dict(items=self.data[int(foo_id)]) class FoosController(RestController): data = ['zero', 'one'] bars = BarsController() @expose() def get_one(self, id): return self.data[int(id)] @expose('json') def get_all(self): return dict(items=self.data) class RootController(object): foos = FoosController() # create the app app = TestApp(make_app(RootController())) # test get_all r = app.get('/foos') self.assertEqual(r.status_int, 200) self.assertEqual(r.body, dumps( dict(items=FoosController.data) ).encode('utf-8')) # test nested get_all r = app.get('/foos/1/bars') self.assertEqual(r.status_int, 200) self.assertEqual(r.body, dumps( dict(items=BarsController.data[1]) ).encode('utf-8')) r = app.get('/foos/bars', expect_errors=True) self.assertEqual(r.status_int, 404) def test_custom_with_trailing_slash(self): class CustomController(RestController): _custom_actions = { 'detail': ['GET'], 'create': ['POST'], 'update': ['PUT'], 'remove': ['DELETE'], } @expose() def detail(self): return 'DETAIL' @expose() def create(self): return 'CREATE' @expose() def update(self, id): return id @expose() def remove(self, id): return id app = TestApp(make_app(CustomController())) r = app.get('/detail') assert r.status_int == 200 assert r.body == b'DETAIL' r = app.get('/detail/') assert r.status_int == 200 assert r.body == b'DETAIL' r = app.post('/create') assert r.status_int == 200 assert r.body == b'CREATE' r = app.post('/create/') assert r.status_int == 200 assert r.body == b'CREATE' r = app.put('/update/123') assert r.status_int == 200 assert r.body == b'123' r = app.put('/update/123/') assert r.status_int == 200 assert r.body == b'123' r = app.delete('/remove/456') assert r.status_int == 200 assert r.body == b'456' r = app.delete('/remove/456/') assert r.status_int == 200 assert r.body == b'456' def test_custom_delete(self): class OthersController(object): @expose() def index(self): return 'DELETE' @expose() def reset(self, id): return str(id) class ThingsController(RestController): others = OthersController() @expose() def delete_fail(self): abort(500) class RootController(object): things = ThingsController() # create the app app = TestApp(make_app(RootController())) # test bad delete r = app.delete('/things/delete_fail', status=405) assert r.status_int == 405 # test bad delete with _method parameter and GET r = app.get('/things/delete_fail?_method=delete', status=405) assert r.status_int == 405 # test bad delete with _method parameter and POST r = app.post('/things/delete_fail', {'_method': 'delete'}, status=405) assert r.status_int == 405 # test custom delete without ID r = app.delete('/things/others/') assert r.status_int == 200 assert r.body == b'DELETE' # test custom delete without ID with _method parameter and GET r = app.get('/things/others/?_method=delete', status=405) assert r.status_int == 405 # test custom delete without ID with _method parameter and POST r = app.post('/things/others/', {'_method': 'delete'}) assert r.status_int == 200 assert r.body == b'DELETE' # test custom delete with ID r = app.delete('/things/others/reset/1') assert r.status_int == 200 assert r.body == b'1' # test custom delete with ID with _method parameter and GET r = app.get('/things/others/reset/1?_method=delete', status=405) assert r.status_int == 405 # test custom delete with ID with _method parameter and POST r = app.post('/things/others/reset/1', {'_method': 'delete'}) assert r.status_int == 200 assert r.body == b'1' def test_get_with_var_args(self): class OthersController(object): @expose() def index(self, one, two, three): return 'NESTED: %s, %s, %s' % (one, two, three) class ThingsController(RestController): others = OthersController() @expose() def get_one(self, *args): return ', '.join(args) class RootController(object): things = ThingsController() # create the app app = TestApp(make_app(RootController())) # test get request r = app.get('/things/one/two/three') assert r.status_int == 200 assert r.body == b'one, two, three' # test nested get request r = app.get('/things/one/two/three/others/') assert r.status_int == 200 assert r.body == b'NESTED: one, two, three' def test_sub_nested_rest(self): class BazsController(RestController): data = [[['zero-zero-zero']]] @expose() def get_one(self, foo_id, bar_id, id): return self.data[int(foo_id)][int(bar_id)][int(id)] class BarsController(RestController): data = [['zero-zero']] bazs = BazsController() @expose() def get_one(self, foo_id, id): return self.data[int(foo_id)][int(id)] class FoosController(RestController): data = ['zero'] bars = BarsController() @expose() def get_one(self, id): return self.data[int(id)] class RootController(object): foos = FoosController() # create the app app = TestApp(make_app(RootController())) # test sub-nested get_one r = app.get('/foos/0/bars/0/bazs/0') assert r.status_int == 200 assert r.body == b'zero-zero-zero' def test_sub_nested_rest_with_overwrites(self): class FinalController(object): @expose() def index(self): return 'FINAL' @expose() def named(self): return 'NAMED' class BazsController(RestController): data = [[['zero-zero-zero']]] final = FinalController() @expose() def get_one(self, foo_id, bar_id, id): return self.data[int(foo_id)][int(bar_id)][int(id)] @expose() def post(self): return 'POST-GRAND-CHILD' @expose() def put(self, id): return 'PUT-GRAND-CHILD' class BarsController(RestController): data = [['zero-zero']] bazs = BazsController() @expose() def get_one(self, foo_id, id): return self.data[int(foo_id)][int(id)] @expose() def post(self): return 'POST-CHILD' @expose() def put(self, id): return 'PUT-CHILD' class FoosController(RestController): data = ['zero'] bars = BarsController() @expose() def get_one(self, id): return self.data[int(id)] @expose() def post(self): return 'POST' @expose() def put(self, id): return 'PUT' class RootController(object): foos = FoosController() # create the app app = TestApp(make_app(RootController())) r = app.post('/foos') assert r.status_int == 200 assert r.body == b'POST' r = app.put('/foos/0') assert r.status_int == 200 assert r.body == b'PUT' r = app.post('/foos/bars') assert r.status_int == 200 assert r.body == b'POST-CHILD' r = app.put('/foos/bars/0') assert r.status_int == 200 assert r.body == b'PUT-CHILD' r = app.post('/foos/bars/bazs') assert r.status_int == 200 assert r.body == b'POST-GRAND-CHILD' r = app.put('/foos/bars/bazs/0') assert r.status_int == 200 assert r.body == b'PUT-GRAND-CHILD' r = app.get('/foos/bars/bazs/final/') assert r.status_int == 200 assert r.body == b'FINAL' r = app.get('/foos/bars/bazs/final/named') assert r.status_int == 200 assert r.body == b'NAMED' def test_post_with_kwargs_only(self): class RootController(RestController): @expose() def get_all(self): return 'INDEX' @expose('json') def post(self, **kw): return kw # create the app app = TestApp(make_app(RootController())) r = app.get('/') assert r.status_int == 200 assert r.body == b'INDEX' kwargs = {'foo': 'bar', 'spam': 'eggs'} r = app.post('/', kwargs) assert r.status_int == 200 assert r.namespace['foo'] == 'bar' assert r.namespace['spam'] == 'eggs' def test_nested_rest_with_lookup(self): class SubController(RestController): @expose() def get_all(self): return "SUB" class FinalController(RestController): def __init__(self, id_): self.id_ = id_ @expose() def get_all(self): return "FINAL-%s" % self.id_ @expose() def post(self): return "POST-%s" % self.id_ class LookupController(RestController): sub = SubController() def __init__(self, id_): self.id_ = id_ @expose() def _lookup(self, id_, *remainder): return FinalController(id_), remainder @expose() def get_all(self): raise AssertionError("Never Reached") @expose() def post(self): return "POST-LOOKUP-%s" % self.id_ @expose() def put(self, id_): return "PUT-LOOKUP-%s-%s" % (self.id_, id_) @expose() def delete(self, id_): return "DELETE-LOOKUP-%s-%s" % (self.id_, id_) class FooController(RestController): @expose() def _lookup(self, id_, *remainder): return LookupController(id_), remainder @expose() def get_one(self, id_): return "GET ONE" @expose() def get_all(self): return "INDEX" @expose() def post(self): return "POST" @expose() def put(self, id_): return "PUT-%s" % id_ @expose() def delete(self, id_): return "DELETE-%s" % id_ class RootController(RestController): foo = FooController() app = TestApp(make_app(RootController())) r = app.get('/foo') assert r.status_int == 200 assert r.body == b'INDEX' r = app.post('/foo') assert r.status_int == 200 assert r.body == b'POST' r = app.get('/foo/1') assert r.status_int == 200 assert r.body == b'GET ONE' r = app.post('/foo/1') assert r.status_int == 200 assert r.body == b'POST-LOOKUP-1' r = app.put('/foo/1') assert r.status_int == 200 assert r.body == b'PUT-1' r = app.delete('/foo/1') assert r.status_int == 200 assert r.body == b'DELETE-1' r = app.put('/foo/1/2') assert r.status_int == 200 assert r.body == b'PUT-LOOKUP-1-2' r = app.delete('/foo/1/2') assert r.status_int == 200 assert r.body == b'DELETE-LOOKUP-1-2' r = app.get('/foo/1/2') assert r.status_int == 200 assert r.body == b'FINAL-2' r = app.post('/foo/1/2') assert r.status_int == 200 assert r.body == b'POST-2' def test_nested_rest_with_default(self): class FooController(RestController): @expose() def _default(self, *remainder): return "DEFAULT %s" % remainder class RootController(RestController): foo = FooController() app = TestApp(make_app(RootController())) r = app.get('/foo/missing') assert r.status_int == 200 assert r.body == b"DEFAULT missing" def test_dynamic_rest_lookup(self): class BarController(RestController): @expose() def get_all(self): return "BAR" @expose() def put(self): return "PUT_BAR" @expose() def delete(self): return "DELETE_BAR" class BarsController(RestController): @expose() def _lookup(self, id_, *remainder): return BarController(), remainder @expose() def get_all(self): return "BARS" @expose() def post(self): return "POST_BARS" class FooController(RestController): bars = BarsController() @expose() def get_all(self): return "FOO" @expose() def put(self): return "PUT_FOO" @expose() def delete(self): return "DELETE_FOO" class FoosController(RestController): @expose() def _lookup(self, id_, *remainder): return FooController(), remainder @expose() def get_all(self): return "FOOS" @expose() def post(self): return "POST_FOOS" class RootController(RestController): foos = FoosController() app = TestApp(make_app(RootController())) r = app.get('/foos') assert r.status_int == 200 assert r.body == b'FOOS' r = app.post('/foos') assert r.status_int == 200 assert r.body == b'POST_FOOS' r = app.get('/foos/foo') assert r.status_int == 200 assert r.body == b'FOO' r = app.put('/foos/foo') assert r.status_int == 200 assert r.body == b'PUT_FOO' r = app.delete('/foos/foo') assert r.status_int == 200 assert r.body == b'DELETE_FOO' r = app.get('/foos/foo/bars') assert r.status_int == 200 assert r.body == b'BARS' r = app.post('/foos/foo/bars') assert r.status_int == 200 assert r.body == b'POST_BARS' r = app.get('/foos/foo/bars/bar') assert r.status_int == 200 assert r.body == b'BAR' r = app.put('/foos/foo/bars/bar') assert r.status_int == 200 assert r.body == b'PUT_BAR' r = app.delete('/foos/foo/bars/bar') assert r.status_int == 200 assert r.body == b'DELETE_BAR' def test_method_not_allowed_get(self): class ThingsController(RestController): @expose() def put(self, id_, value): response.status = 200 @expose() def delete(self, id_): response.status = 200 app = TestApp(make_app(ThingsController())) r = app.get('/', status=405) assert r.status_int == 405 assert r.headers['Allow'] == 'DELETE, PUT' def test_method_not_allowed_post(self): class ThingsController(RestController): @expose() def get_one(self): return dict() app = TestApp(make_app(ThingsController())) r = app.post('/', {'foo': 'bar'}, status=405) assert r.status_int == 405 assert r.headers['Allow'] == 'GET' def test_method_not_allowed_put(self): class ThingsController(RestController): @expose() def get_one(self): return dict() app = TestApp(make_app(ThingsController())) r = app.put('/123', status=405) assert r.status_int == 405 assert r.headers['Allow'] == 'GET' def test_method_not_allowed_delete(self): class ThingsController(RestController): @expose() def get_one(self): return dict() app = TestApp(make_app(ThingsController())) r = app.delete('/123', status=405) assert r.status_int == 405 assert r.headers['Allow'] == 'GET' def test_proper_allow_header_multiple_gets(self): class ThingsController(RestController): @expose() def get_all(self): return dict() @expose() def get(self): return dict() app = TestApp(make_app(ThingsController())) r = app.put('/123', status=405) assert r.status_int == 405 assert r.headers['Allow'] == 'GET' @unittest.skipIf(sys.maxunicode <= 65536, 'narrow python build with UCS-2') def test_rest_with_utf8_uri(self): class FooController(RestController): key = chr(0x1F330) data = {key: 'Success!'} @expose() def get_one(self, id_): return self.data[id_] @expose() def get_all(self): return "Hello, World!" @expose() def put(self, id_, value): return self.data[id_] @expose() def delete(self, id_): return self.data[id_] class RootController(RestController): foo = FooController() app = TestApp(make_app(RootController())) r = app.get('/foo/%F0%9F%8C%B0') assert r.status_int == 200 assert r.body == b'Success!' r = app.put('/foo/%F0%9F%8C%B0', {'value': 'pecans'}) assert r.status_int == 200 assert r.body == b'Success!' r = app.delete('/foo/%F0%9F%8C%B0') assert r.status_int == 200 assert r.body == b'Success!' r = app.get('/foo/') assert r.status_int == 200 assert r.body == b'Hello, World!' def test_rest_with_utf8_endpoint(self): class ChildController(object): @expose() def index(self): return 'Hello, World!' class FooController(RestController): pass # okay, so it's technically a chestnut, but close enough... setattr(FooController, '🌰', ChildController()) class RootController(RestController): foo = FooController() app = TestApp(make_app(RootController())) r = app.get('/foo/%F0%9F%8C%B0/') assert r.status_int == 200 assert r.body == b'Hello, World!' class TestExplicitRoute(PecanTestCase): def test_alternate_route(self): class RootController(RestController): @expose(route='some-path') def get_all(self): return "Hello, World!" self.assertRaises( ValueError, RootController ) pecan-1.5.1/pecan/tests/test_scaffolds.py000066400000000000000000000121051445453044500204200ustar00rootroot00000000000000import os import shutil import sys import tempfile import unittest from io import StringIO from pecan.tests import PecanTestCase class TestPecanScaffold(PecanTestCase): def test_normalize_pkg_name(self): from pecan.scaffolds import PecanScaffold s = PecanScaffold() assert s.normalize_pkg_name('sam') == 'sam' assert s.normalize_pkg_name('sam1') == 'sam1' assert s.normalize_pkg_name('sam_') == 'sam_' assert s.normalize_pkg_name('Sam') == 'sam' assert s.normalize_pkg_name('SAM') == 'sam' assert s.normalize_pkg_name('sam ') == 'sam' assert s.normalize_pkg_name(' sam') == 'sam' assert s.normalize_pkg_name('sam$') == 'sam' assert s.normalize_pkg_name('sam-sam') == 'samsam' class TestScaffoldUtils(PecanTestCase): def setUp(self): super(TestScaffoldUtils, self).setUp() self.scaffold_destination = tempfile.mkdtemp() self.out = sys.stdout sys.stdout = StringIO() def tearDown(self): shutil.rmtree(self.scaffold_destination) sys.stdout = self.out def test_copy_dir(self): from pecan.scaffolds import PecanScaffold class SimpleScaffold(PecanScaffold): _scaffold_dir = ('pecan', os.path.join( 'tests', 'scaffold_fixtures', 'simple' )) SimpleScaffold().copy_to(os.path.join( self.scaffold_destination, 'someapp' ), out_=StringIO()) assert os.path.isfile(os.path.join( self.scaffold_destination, 'someapp', 'foo' )) assert os.path.isfile(os.path.join( self.scaffold_destination, 'someapp', 'bar', 'spam.txt' )) with open(os.path.join( self.scaffold_destination, 'someapp', 'foo' ), 'r') as f: assert f.read().strip() == 'YAR' def test_destination_directory_levels_deep(self): from pecan.scaffolds import copy_dir f = StringIO() copy_dir( ( 'pecan', os.path.join('tests', 'scaffold_fixtures', 'simple') ), os.path.join(self.scaffold_destination, 'some', 'app'), {}, out_=f ) assert os.path.isfile(os.path.join( self.scaffold_destination, 'some', 'app', 'foo') ) assert os.path.isfile(os.path.join( self.scaffold_destination, 'some', 'app', 'bar', 'spam.txt') ) with open(os.path.join( self.scaffold_destination, 'some', 'app', 'foo' ), 'r') as f: assert f.read().strip() == 'YAR' with open(os.path.join( self.scaffold_destination, 'some', 'app', 'bar', 'spam.txt' ), 'r') as f: assert f.read().strip() == 'Pecan' def test_destination_directory_already_exists(self): from pecan.scaffolds import copy_dir f = StringIO() copy_dir( ( 'pecan', os.path.join('tests', 'scaffold_fixtures', 'simple') ), os.path.join(self.scaffold_destination), {}, out_=f ) assert 'already exists' in f.getvalue() def test_copy_dir_with_filename_substitution(self): from pecan.scaffolds import copy_dir copy_dir( ( 'pecan', os.path.join('tests', 'scaffold_fixtures', 'file_sub') ), os.path.join( self.scaffold_destination, 'someapp' ), {'package': 'thingy'}, out_=StringIO() ) assert os.path.isfile(os.path.join( self.scaffold_destination, 'someapp', 'foo_thingy') ) assert os.path.isfile(os.path.join( self.scaffold_destination, 'someapp', 'bar_thingy', 'spam.txt') ) with open(os.path.join( self.scaffold_destination, 'someapp', 'foo_thingy' ), 'r') as f: assert f.read().strip() == 'YAR' with open(os.path.join( self.scaffold_destination, 'someapp', 'bar_thingy', 'spam.txt' ), 'r') as f: assert f.read().strip() == 'Pecan' def test_copy_dir_with_file_content_substitution(self): from pecan.scaffolds import copy_dir copy_dir( ( 'pecan', os.path.join('tests', 'scaffold_fixtures', 'content_sub'), ), os.path.join( self.scaffold_destination, 'someapp' ), {'package': 'thingy'}, out_=StringIO() ) assert os.path.isfile(os.path.join( self.scaffold_destination, 'someapp', 'foo') ) assert os.path.isfile(os.path.join( self.scaffold_destination, 'someapp', 'bar', 'spam.txt') ) with open(os.path.join( self.scaffold_destination, 'someapp', 'foo' ), 'r') as f: assert f.read().strip() == 'YAR thingy' with open(os.path.join( self.scaffold_destination, 'someapp', 'bar', 'spam.txt' ), 'r') as f: assert f.read().strip() == 'Pecan thingy' pecan-1.5.1/pecan/tests/test_secure.py000066400000000000000000000425001445453044500177440ustar00rootroot00000000000000import sys import unittest from webtest import TestApp from pecan import expose, make_app from pecan.secure import secure, unlocked, SecureController from pecan.tests import PecanTestCase try: set() except: from sets import Set as set class TestSecure(PecanTestCase): def test_simple_secure(self): authorized = False class SecretController(SecureController): @expose() def index(self): return 'Index' @expose() @unlocked def allowed(self): return 'Allowed!' @classmethod def check_permissions(cls): return authorized class RootController(object): @expose() def index(self): return 'Hello, World!' @expose() @secure(lambda: False) def locked(self): return 'No dice!' @expose() @secure(lambda: True) def unlocked(self): return 'Sure thing' secret = SecretController() app = TestApp(make_app( RootController(), debug=True, static_root='tests/static' )) response = app.get('/') assert response.status_int == 200 assert response.body == b'Hello, World!' response = app.get('/unlocked') assert response.status_int == 200 assert response.body == b'Sure thing' response = app.get('/locked', expect_errors=True) assert response.status_int == 401 response = app.get('/secret/', expect_errors=True) assert response.status_int == 401 response = app.get('/secret/allowed') assert response.status_int == 200 assert response.body == b'Allowed!' def test_unlocked_attribute(self): class AuthorizedSubController(object): @expose() def index(self): return 'Index' @expose() def allowed(self): return 'Allowed!' class SecretController(SecureController): @expose() def index(self): return 'Index' @expose() @unlocked def allowed(self): return 'Allowed!' authorized = unlocked(AuthorizedSubController()) class RootController(object): @expose() def index(self): return 'Hello, World!' @expose() @secure(lambda: False) def locked(self): return 'No dice!' @expose() @secure(lambda: True) def unlocked(self): return 'Sure thing' secret = SecretController() app = TestApp(make_app( RootController(), debug=True, static_root='tests/static' )) response = app.get('/') assert response.status_int == 200 assert response.body == b'Hello, World!' response = app.get('/unlocked') assert response.status_int == 200 assert response.body == b'Sure thing' response = app.get('/locked', expect_errors=True) assert response.status_int == 401 response = app.get('/secret/', expect_errors=True) assert response.status_int == 401 response = app.get('/secret/allowed') assert response.status_int == 200 assert response.body == b'Allowed!' response = app.get('/secret/authorized/') assert response.status_int == 200 assert response.body == b'Index' response = app.get('/secret/authorized/allowed') assert response.status_int == 200 assert response.body == b'Allowed!' def test_secure_attribute(self): authorized = False class SubController(object): @expose() def index(self): return 'Hello from sub!' class RootController(object): @expose() def index(self): return 'Hello from root!' sub = secure(SubController(), lambda: authorized) app = TestApp(make_app(RootController())) response = app.get('/') assert response.status_int == 200 assert response.body == b'Hello from root!' response = app.get('/sub/', expect_errors=True) assert response.status_int == 401 authorized = True response = app.get('/sub/') assert response.status_int == 200 assert response.body == b'Hello from sub!' def test_secured_generic_controller(self): authorized = False class RootController(object): @classmethod def check_permissions(cls): return authorized @expose(generic=True) def index(self): return 'Index' @secure('check_permissions') @index.when(method='POST') def index_post(self): return 'I should not be allowed' @secure('check_permissions') @expose(generic=True) def secret(self): return 'I should not be allowed' app = TestApp(make_app( RootController(), debug=True, static_root='tests/static' )) response = app.get('/') assert response.status_int == 200 response = app.post('/', expect_errors=True) assert response.status_int == 401 response = app.get('/secret/', expect_errors=True) assert response.status_int == 401 def test_secured_generic_controller_lambda(self): authorized = False class RootController(object): @expose(generic=True) def index(self): return 'Index' @secure(lambda: authorized) @index.when(method='POST') def index_post(self): return 'I should not be allowed' @secure(lambda: authorized) @expose(generic=True) def secret(self): return 'I should not be allowed' app = TestApp(make_app( RootController(), debug=True, static_root='tests/static' )) response = app.get('/') assert response.status_int == 200 response = app.post('/', expect_errors=True) assert response.status_int == 401 response = app.get('/secret/', expect_errors=True) assert response.status_int == 401 def test_secured_generic_controller_secure_attribute(self): authorized = False class SecureController(object): @expose(generic=True) def index(self): return 'I should not be allowed' @index.when(method='POST') def index_post(self): return 'I should not be allowed' @expose(generic=True) def secret(self): return 'I should not be allowed' class RootController(object): sub = secure(SecureController(), lambda: authorized) app = TestApp(make_app( RootController(), debug=True, static_root='tests/static' )) response = app.get('/sub/', expect_errors=True) assert response.status_int == 401 response = app.post('/sub/', expect_errors=True) assert response.status_int == 401 response = app.get('/sub/secret/', expect_errors=True) assert response.status_int == 401 def test_secured_generic_controller_secure_attribute_with_unlocked(self): class RootController(SecureController): @unlocked @expose(generic=True) def index(self): return 'Unlocked!' @unlocked @index.when(method='POST') def index_post(self): return 'Unlocked!' @expose(generic=True) def secret(self): return 'I should not be allowed' app = TestApp(make_app( RootController(), debug=True, static_root='tests/static' )) response = app.get('/') assert response.status_int == 200 response = app.post('/') assert response.status_int == 200 response = app.get('/secret/', expect_errors=True) assert response.status_int == 401 def test_state_attribute(self): from pecan.secure import Any, Protected assert repr(Any) == '' assert bool(Any) is False assert repr(Protected) == '' assert bool(Protected) is True def test_secure_obj_only_failure(self): class Foo(object): pass try: secure(Foo()) except Exception as e: assert isinstance(e, TypeError) class TestObjectPathSecurity(PecanTestCase): def setUp(self): super(TestObjectPathSecurity, self).setUp() permissions_checked = set() class DeepSecretController(SecureController): authorized = False @expose() @unlocked def _lookup(self, someID, *remainder): if someID == 'notfound': return None return SubController(someID), remainder @expose() def index(self): return 'Deep Secret' @classmethod def check_permissions(cls): permissions_checked.add('deepsecret') return cls.authorized class SubController(object): def __init__(self, myID): self.myID = myID @expose() def index(self): return 'Index %s' % self.myID deepsecret = DeepSecretController() class SecretController(SecureController): authorized = False independent_authorization = False @expose() def _lookup(self, someID, *remainder): if someID == 'notfound': return None elif someID == 'lookup_wrapped': return self.wrapped, remainder return SubController(someID), remainder @secure('independent_check_permissions') @expose() def independent(self): return 'Independent Security' wrapped = secure( SubController('wrapped'), 'independent_check_permissions' ) @classmethod def check_permissions(cls): permissions_checked.add('secretcontroller') return cls.authorized @classmethod def independent_check_permissions(cls): permissions_checked.add('independent') return cls.independent_authorization class NotSecretController(object): @expose() def _lookup(self, someID, *remainder): if someID == 'notfound': return None return SubController(someID), remainder unlocked = unlocked(SubController('unlocked')) class RootController(object): secret = SecretController() notsecret = NotSecretController() self.deepsecret_cls = DeepSecretController self.secret_cls = SecretController self.permissions_checked = permissions_checked self.app = TestApp(make_app( RootController(), debug=True, static_root='tests/static' )) def tearDown(self): self.permissions_checked.clear() self.secret_cls.authorized = False self.deepsecret_cls.authorized = False def test_sub_of_both_not_secret(self): response = self.app.get('/notsecret/hi/') assert response.status_int == 200 assert response.body == b'Index hi' def test_protected_lookup(self): response = self.app.get('/secret/hi/', expect_errors=True) assert response.status_int == 401 self.secret_cls.authorized = True response = self.app.get('/secret/hi/') assert response.status_int == 200 assert response.body == b'Index hi' assert 'secretcontroller' in self.permissions_checked def test_secured_notfound_lookup(self): response = self.app.get('/secret/notfound/', expect_errors=True) assert response.status_int == 404 def test_secret_through_lookup(self): response = self.app.get( '/notsecret/hi/deepsecret/', expect_errors=True ) assert response.status_int == 401 def test_layered_protection(self): response = self.app.get('/secret/hi/deepsecret/', expect_errors=True) assert response.status_int == 401 assert 'secretcontroller' in self.permissions_checked self.secret_cls.authorized = True response = self.app.get('/secret/hi/deepsecret/', expect_errors=True) assert response.status_int == 401 assert 'secretcontroller' in self.permissions_checked assert 'deepsecret' in self.permissions_checked self.deepsecret_cls.authorized = True response = self.app.get('/secret/hi/deepsecret/') assert response.status_int == 200 assert response.body == b'Deep Secret' assert 'secretcontroller' in self.permissions_checked assert 'deepsecret' in self.permissions_checked def test_cyclical_protection(self): self.secret_cls.authorized = True self.deepsecret_cls.authorized = True response = self.app.get('/secret/1/deepsecret/2/deepsecret/') assert response.status_int == 200 assert response.body == b'Deep Secret' assert 'secretcontroller' in self.permissions_checked assert 'deepsecret' in self.permissions_checked def test_unlocked_lookup(self): response = self.app.get('/notsecret/1/deepsecret/2/') assert response.status_int == 200 assert response.body == b'Index 2' assert 'deepsecret' not in self.permissions_checked response = self.app.get( '/notsecret/1/deepsecret/notfound/', expect_errors=True ) assert response.status_int == 404 assert 'deepsecret' not in self.permissions_checked def test_mixed_protection(self): self.secret_cls.authorized = True response = self.app.get( '/secret/1/deepsecret/notfound/', expect_errors=True ) assert response.status_int == 404 assert 'secretcontroller' in self.permissions_checked assert 'deepsecret' not in self.permissions_checked def test_independent_check_failure(self): response = self.app.get('/secret/independent/', expect_errors=True) assert response.status_int == 401 assert len(self.permissions_checked) == 1 assert 'independent' in self.permissions_checked def test_independent_check_success(self): self.secret_cls.independent_authorization = True response = self.app.get('/secret/independent') assert response.status_int == 200 assert response.body == b'Independent Security' assert len(self.permissions_checked) == 1 assert 'independent' in self.permissions_checked def test_wrapped_attribute_failure(self): self.secret_cls.independent_authorization = False response = self.app.get('/secret/wrapped/', expect_errors=True) assert response.status_int == 401 assert len(self.permissions_checked) == 1 assert 'independent' in self.permissions_checked def test_wrapped_attribute_success(self): self.secret_cls.independent_authorization = True response = self.app.get('/secret/wrapped/') assert response.status_int == 200 assert response.body == b'Index wrapped' assert len(self.permissions_checked) == 1 assert 'independent' in self.permissions_checked def test_lookup_to_wrapped_attribute_on_self(self): self.secret_cls.authorized = True self.secret_cls.independent_authorization = True response = self.app.get('/secret/lookup_wrapped/') assert response.status_int == 200 assert response.body == b'Index wrapped' assert len(self.permissions_checked) == 2 assert 'independent' in self.permissions_checked assert 'secretcontroller' in self.permissions_checked def test_unlocked_attribute_in_insecure(self): response = self.app.get('/notsecret/unlocked/') assert response.status_int == 200 assert response.body == b'Index unlocked' class SecureControllerSharedPermissionsRegression(PecanTestCase): """Regression tests for https://github.com/dreamhost/pecan/issues/131""" def setUp(self): super(SecureControllerSharedPermissionsRegression, self).setUp() class Parent(object): @expose() def index(self): return 'hello' class UnsecuredChild(Parent): pass class SecureChild(Parent, SecureController): @classmethod def check_permissions(cls): return False class RootController(object): secured = SecureChild() unsecured = UnsecuredChild() self.app = TestApp(make_app(RootController())) def test_inherited_security(self): assert self.app.get('/secured/', status=401).status_int == 401 assert self.app.get('/unsecured/').status_int == 200 pecan-1.5.1/pecan/tests/test_templating.py000066400000000000000000000026151445453044500206250ustar00rootroot00000000000000import tempfile from pecan.templating import RendererFactory, format_line_context from pecan.tests import PecanTestCase class TestTemplate(PecanTestCase): def setUp(self): super(TestTemplate, self).setUp() self.rf = RendererFactory() def test_available(self): self.assertTrue(self.rf.available('json')) self.assertFalse(self.rf.available('badrenderer')) def test_create_bad(self): self.assertEqual(self.rf.get('doesnotexist', '/'), None) def test_extra_vars(self): extra_vars = self.rf.extra_vars self.assertEqual(extra_vars.make_ns({}), {}) extra_vars.update({'foo': 1}) self.assertEqual(extra_vars.make_ns({}), {'foo': 1}) def test_update_extra_vars(self): extra_vars = self.rf.extra_vars extra_vars.update({'foo': 1}) self.assertEqual(extra_vars.make_ns({'bar': 2}), {'foo': 1, 'bar': 2}) self.assertEqual(extra_vars.make_ns({'foo': 2}), {'foo': 2}) class TestTemplateLineFormat(PecanTestCase): def setUp(self): super(TestTemplateLineFormat, self).setUp() self.f = tempfile.NamedTemporaryFile() def tearDown(self): del self.f def test_format_line_context(self): for i in range(11): self.f.write(b'Testing Line %d\n' % i) self.f.flush() assert format_line_context(self.f.name, 0).count('Testing Line') == 10 pecan-1.5.1/pecan/tests/test_util.py000066400000000000000000000102171445453044500174330ustar00rootroot00000000000000import functools import unittest from pecan import expose from pecan import util from pecan.compat import getargspec class TestArgSpec(unittest.TestCase): @property def controller(self): class RootController(object): @expose() def index(self, a, b, c=1, *args, **kwargs): return 'Hello, World!' @staticmethod @expose() def static_index(a, b, c=1, *args, **kwargs): return 'Hello, World!' return RootController() def test_no_decorator(self): expected = getargspec(self.controller.index.__func__) actual = util.getargspec(self.controller.index.__func__) assert expected == actual expected = getargspec(self.controller.static_index) actual = util.getargspec(self.controller.static_index) assert expected == actual def test_simple_decorator(self): def dec(f): return f expected = getargspec(self.controller.index.__func__) actual = util.getargspec(dec(self.controller.index.__func__)) assert expected == actual expected = getargspec(self.controller.static_index) actual = util.getargspec(dec(self.controller.static_index)) assert expected == actual def test_simple_wrapper(self): def dec(f): @functools.wraps(f) def wrapped(*a, **kw): return f(*a, **kw) return wrapped expected = getargspec(self.controller.index.__func__) actual = util.getargspec(dec(self.controller.index.__func__)) assert expected == actual expected = getargspec(self.controller.static_index) actual = util.getargspec(dec(self.controller.static_index)) assert expected == actual def test_multiple_decorators(self): def dec(f): @functools.wraps(f) def wrapped(*a, **kw): return f(*a, **kw) return wrapped expected = getargspec(self.controller.index.__func__) actual = util.getargspec(dec(dec(dec(self.controller.index.__func__)))) assert expected == actual expected = getargspec(self.controller.static_index) actual = util.getargspec(dec(dec(dec( self.controller.static_index)))) assert expected == actual def test_decorator_with_args(self): def dec(flag): def inner(f): @functools.wraps(f) def wrapped(*a, **kw): return f(*a, **kw) return wrapped return inner expected = getargspec(self.controller.index.__func__) actual = util.getargspec(dec(True)(self.controller.index.__func__)) assert expected == actual expected = getargspec(self.controller.static_index) actual = util.getargspec(dec(True)( self.controller.static_index)) assert expected == actual def test_nested_cells(self): def before(handler): def deco(f): def wrapped(*args, **kwargs): if callable(handler): handler() return f(*args, **kwargs) return wrapped return deco class RootController(object): @expose() @before(lambda: True) def index(self, a, b, c): return 'Hello, World!' argspec = util._cfg(RootController.index)['argspec'] assert argspec.args == ['self', 'a', 'b', 'c'] def test_class_based_decorator(self): class deco(object): def __init__(self, arg): self.arg = arg def __call__(self, f): @functools.wraps(f) def wrapper(*args, **kw): assert self.arg == '12345' return f(*args, **kw) return wrapper class RootController(object): @expose() @deco('12345') def index(self, a, b, c): return 'Hello, World!' argspec = util._cfg(RootController.index)['argspec'] assert argspec.args == ['self', 'a', 'b', 'c'] pecan-1.5.1/pecan/util.py000066400000000000000000000027321445453044500152350ustar00rootroot00000000000000from pecan.compat import getargspec as _getargspec def iscontroller(obj): return getattr(obj, 'exposed', False) def getargspec(method): """ Drill through layers of decorators attempting to locate the actual argspec for a method. """ argspec = _getargspec(method) args = argspec[0] if args and args[0] == 'self': return argspec if hasattr(method, '__func__'): method = method.__func__ func_closure = method.__closure__ # NOTE(sileht): if the closure is None we cannot look deeper, # so return actual argspec, this occurs when the method # is static for example. if not func_closure: return argspec closure = None # In the case of deeply nested decorators (with arguments), it's possible # that there are several callables in scope; Take a best guess and go # with the one that looks most like a pecan controller function # (has a __code__ object, and 'self' is the first argument) func_closure = filter( lambda c: ( callable(c.cell_contents) and hasattr(c.cell_contents, '__code__') ), func_closure ) func_closure = sorted( func_closure, key=lambda c: 'self' in c.cell_contents.__code__.co_varnames, reverse=True ) closure = func_closure[0] method = closure.cell_contents return getargspec(method) def _cfg(f): if not hasattr(f, '_pecan'): f._pecan = {} return f._pecan pecan-1.5.1/requirements.txt000066400000000000000000000000601445453044500160740ustar00rootroot00000000000000WebOb>=1.8 Mako>=0.4.0 setuptools logutils>=0.3 pecan-1.5.1/setup.cfg000066400000000000000000000002401445453044500144310ustar00rootroot00000000000000[nosetests] match=^test where=pecan nocapture=1 cover-package=pecan cover-erase=1 [tool:pytest] norecursedirs = +package+ config_fixtures docs .git *.egg .tox pecan-1.5.1/setup.py000066400000000000000000000061661445453044500143370ustar00rootroot00000000000000import sys import platform from setuptools import setup, find_packages version = '1.5.1' # # determine requirements # with open('requirements.txt') as reqs: requirements = [ line for line in reqs.read().split('\n') if (line and not line.startswith('-')) ] with open('test-requirements.txt') as reqs: test_requirements = [ line for line in reqs.read().split('\n') if (line and not line.startswith('-')) ] try: from functools import singledispatch # noqa except: # # This was introduced in Python 3.4 - the singledispatch package contains # a backported replacement for 2.6 through 3.4 # requirements.append('singledispatch') try: from collections import OrderedDict except: requirements.append('ordereddict') tests_require = requirements + test_requirements if sys.version_info < (3, 0): # These don't support Python3 yet - don't run their tests if platform.python_implementation() != 'PyPy': # Kajiki is not pypy-compatible tests_require += ['Kajiki'] tests_require += ['Genshi'] else: # Genshi added Python3 support in 0.7 tests_require += ['Genshi>=0.7'] # # call setup # setup( name='pecan', version=version, description="A WSGI object-dispatching web framework, designed to be " "lean and fast, with few dependencies.", long_description=None, classifiers=[ 'Development Status :: 5 - Production/Stable', 'Environment :: Console', 'Environment :: Web Environment', 'Intended Audience :: Developers', 'Intended Audience :: System Administrators', 'License :: OSI Approved :: BSD License', 'Operating System :: MacOS :: MacOS X', 'Operating System :: Microsoft :: Windows', 'Operating System :: POSIX', 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Topic :: Internet :: WWW/HTTP :: WSGI', 'Topic :: Software Development :: Libraries :: Application Frameworks' ], keywords='web framework wsgi object-dispatch http', author='Jonathan LaCour', author_email='info@pecanpy.org', url='http://github.com/pecan/pecan', license='BSD', packages=find_packages(exclude=['ez_setup', 'examples', 'tests']), include_package_data=True, zip_safe=False, install_requires=requirements, tests_require=tests_require, test_suite='pecan', entry_points=""" [pecan.command] serve = pecan.commands:ServeCommand shell = pecan.commands:ShellCommand create = pecan.commands:CreateCommand [pecan.scaffold] base = pecan.scaffolds:BaseScaffold rest-api = pecan.scaffolds:RestAPIScaffold [console_scripts] pecan = pecan.commands:CommandRunner.handle_command_line gunicorn_pecan = pecan.commands.serve:gunicorn_run """ ) pecan-1.5.1/test-requirements.txt000066400000000000000000000001361445453044500170550ustar00rootroot00000000000000gunicorn Jinja2<3 # >= 3 not compatible py35 pep8 sqlalchemy uwsgi virtualenv WebTest>=1.3.1 pecan-1.5.1/tox.ini000066400000000000000000000021421445453044500141260ustar00rootroot00000000000000[tox] envlist = py36,py37,py38,py39,scaffolds,sqlalchemy-1.4,sqlalchemy-2,pep8 [testenv] deps = sqlalchemy > 1.4 commands={envpython} -m pip freeze {envpython} setup.py test -v {posargs} [testenv:sqlalchemy-1.4] deps = sqlalchemy < 2 commands={envpython} -m pip freeze {envpython} setup.py test -v {posargs} [testenv:sqlalchemy-2] deps = sqlalchemy >= 2 commands={envpython} -m pip freeze {envpython} setup.py test -v {posargs} [testenv:scaffolds] deps = -r{toxinidir}/test-requirements.txt changedir={envdir}/tmp commands=pecan create testing123 {envpython} testing123/setup.py install {envpython} testing123/setup.py test -q pep8 --repeat --show-source testing123/setup.py testing123/testing123 {envpython} -m pip freeze {envpython} {toxinidir}/pecan/tests/scaffold_builder.py [testenv:pep8] deps = pep8 commands = pep8 --repeat --show-source pecan setup.py --ignore=E402 # Generic environment for running commands like packaging [testenv:venv] commands={posargs} [testenv:docs] deps = sphinx commands = python setup.py build_sphinx