Merge branch 'master' of github.com:pecan/pecan
This commit is contained in:
		@@ -0,0 +1,139 @@
 | 
			
		||||
.. _rest:
 | 
			
		||||
 | 
			
		||||
REST Controller
 | 
			
		||||
===============
 | 
			
		||||
 | 
			
		||||
If you need to write controllers to interact with objects, using the 
 | 
			
		||||
``RestController`` may help speed things up. It follows the Representational 
 | 
			
		||||
State Transfer Protocol, also known as REST, by routing the standard HTTP 
 | 
			
		||||
verbs of GET, POST, PUT, and DELETE to individual methods::
 | 
			
		||||
 | 
			
		||||
    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, the ``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 ``RestController`` uses the de-facto standard ``?_method=`` query 
 | 
			
		||||
string hack to work around the lack of PUT/DELETE support in current browsers.
 | 
			
		||||
 | 
			
		||||
The ``RestController`` still supports the ``index``, ``_default``, and 
 | 
			
		||||
``_lookup`` routing overrides. If you need to override ``_route``, however, 
 | 
			
		||||
make sure to call ``RestController._route`` at the end of your custom 
 | 
			
		||||
``_route`` method so that the REST routing described above still occurs.
 | 
			
		||||
 | 
			
		||||
Nesting
 | 
			
		||||
-------
 | 
			
		||||
 | 
			
		||||
``RestController`` instances can be nested so that child resources get 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`` would call ``BooksController.get`` with an 
 | 
			
		||||
``author_id`` of 1 and ``id`` of 2.
 | 
			
		||||
 | 
			
		||||
To determine which arguments are associated with the parent resource, Pecan 
 | 
			
		||||
looks at the ``get_one`` or ``get`` method signatures, in that order, in the 
 | 
			
		||||
parent controller. If the parent resource takes a variable number of arguments, 
 | 
			
		||||
Pecan will hand it everything up to the child resource controller name (e.g., 
 | 
			
		||||
``books`` in the above example).
 | 
			
		||||
 | 
			
		||||
Custom Actions
 | 
			
		||||
--------------
 | 
			
		||||
 | 
			
		||||
In addition to the default methods defined above, you can add additional 
 | 
			
		||||
behaviors to a ``RestController`` by defining a special ``_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()
 | 
			
		||||
 | 
			
		||||
Additional method names are the keys in the dictionary. The values are lists 
 | 
			
		||||
of valid HTTP verbs for those custom actions, including PUT and DELETE.
 | 
			
		||||
 
 | 
			
		||||
@@ -218,72 +218,3 @@ requests ``/hello.json``. The second tells the templating engine to use
 | 
			
		||||
tells Pecan to use the html_template.mako when the client requests
 | 
			
		||||
``/hello.html``. If the client requests ``/hello``, Pecan will use the
 | 
			
		||||
text/html template.
 | 
			
		||||
 | 
			
		||||
.. _advanced_routing:
 | 
			
		||||
 | 
			
		||||
Advanced Routing
 | 
			
		||||
================
 | 
			
		||||
Pecan offers a few ways to control and extend routing.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.. _restcontroller:
 | 
			
		||||
 | 
			
		||||
RestController
 | 
			
		||||
--------------
 | 
			
		||||
    A Decorated Controller that dispatches in a RESTful Manner.
 | 
			
		||||
 | 
			
		||||
    This controller was designed to follow Representational State Transfer protocol, also known as REST.
 | 
			
		||||
    The goal of this controller method is to provide the developer a way to map
 | 
			
		||||
    RESTful URLS to controller methods directly, while still allowing Normal Object Dispatch to occur.
 | 
			
		||||
 | 
			
		||||
    Here is a brief rundown of the methods which are called on dispatch along with an example URL.
 | 
			
		||||
 | 
			
		||||
    +-----------------+--------------------------------------------------------------+--------------------------------------------+
 | 
			
		||||
    | Method          | Description                                                  | Example Method(s) / URL(s)                 |
 | 
			
		||||
    +=================+==============================================================+============================================+
 | 
			
		||||
    | get_one         | Display one record.                                          | GET /movies/1                              |
 | 
			
		||||
    +-----------------+--------------------------------------------------------------+--------------------------------------------+
 | 
			
		||||
    | get_all         | Display all records in a resource.                           | GET /movies/                               |
 | 
			
		||||
    +-----------------+--------------------------------------------------------------+--------------------------------------------+
 | 
			
		||||
    | get             | A combo of get_one and get_all.                              | GET /movies/                               |
 | 
			
		||||
    |                 |                                                              +--------------------------------------------+
 | 
			
		||||
    |                 |                                                              | GET /movies/1                              |
 | 
			
		||||
    +-----------------+--------------------------------------------------------------+--------------------------------------------+
 | 
			
		||||
    | new             | Display a page to prompt the User for resource creation.     | GET /movies/new                            |
 | 
			
		||||
    +-----------------+--------------------------------------------------------------+--------------------------------------------+
 | 
			
		||||
    | edit            | Display a page to prompt the User for resource modification. |  GET /movies/1/edit                        |
 | 
			
		||||
    +-----------------+--------------------------------------------------------------+--------------------------------------------+
 | 
			
		||||
    | post            | Create a new record.                                         | POST /movies/                              |
 | 
			
		||||
    +-----------------+--------------------------------------------------------------+--------------------------------------------+
 | 
			
		||||
    | put             | Update an existing record.                                   | POST /movies/1?_method=PUT                 |
 | 
			
		||||
    |                 |                                                              +--------------------------------------------+
 | 
			
		||||
    |                 |                                                              | PUT /movies/1                              |
 | 
			
		||||
    +-----------------+--------------------------------------------------------------+--------------------------------------------+
 | 
			
		||||
    | post_delete     | Delete an existing record.                                   | POST /movies/1?_method=DELETE              |
 | 
			
		||||
    |                 |                                                              +--------------------------------------------+
 | 
			
		||||
    |                 |                                                              | DELETE /movies/1                           |
 | 
			
		||||
    +-----------------+--------------------------------------------------------------+--------------------------------------------+
 | 
			
		||||
    | get_delete      | Display a delete Confirmation page.                          | GET /movies/1/delete                       |
 | 
			
		||||
    +-----------------+--------------------------------------------------------------+--------------------------------------------+
 | 
			
		||||
    | delete          | A combination of post_delete and get_delete.                 | GET /movies/delete                         |
 | 
			
		||||
    |                 |                                                              +--------------------------------------------+
 | 
			
		||||
    |                 |                                                              | DELETE /movies/1                           |
 | 
			
		||||
    |                 |                                                              +--------------------------------------------+
 | 
			
		||||
    |                 |                                                              | DELETE /movies/                            |
 | 
			
		||||
    |                 |                                                              +--------------------------------------------+
 | 
			
		||||
    |                 |                                                              | POST /movies/1/delete                      |
 | 
			
		||||
    |                 |                                                              +--------------------------------------------+
 | 
			
		||||
    |                 |                                                              | POST /movies/delete                        |
 | 
			
		||||
    +-----------------+--------------------------------------------------------------+--------------------------------------------+
 | 
			
		||||
 | 
			
		||||
    You may note the ?_method on some of the URLs.  This is basically a hack because exiting browsers
 | 
			
		||||
    do not support the PUT and DELETE methods.  Just note that if you decide to use a this resource with a web browser,
 | 
			
		||||
    you will likely have to add a _method as a hidden field in your forms for these items.  Also note that RestController differs
 | 
			
		||||
    from a base Pecan controller in that it offers no index, default, or lookup.  It is intended primarily for  resource management.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.. _note:
 | 
			
		||||
    The following items are still pending:
 | 
			
		||||
 | 
			
		||||
     * Hooks
 | 
			
		||||
     * Security
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@ Structure
 | 
			
		||||
---------
 | 
			
		||||
This guide assumes that you have all your tests in a ``tests`` directory. If
 | 
			
		||||
you have created a project from the ``base`` project template that Pecan
 | 
			
		||||
provides you should already have this directory with a few tests.
 | 
			
		||||
provides, you should already have this directory with a few tests.
 | 
			
		||||
 | 
			
		||||
The template project uses UnitTest-type tests and some of those tests use
 | 
			
		||||
WebTest. We will describe how they work in the next section.
 | 
			
		||||
@@ -37,34 +37,54 @@ This is how running those tests with ``py.test`` would look like::
 | 
			
		||||
 | 
			
		||||
Configuration and Testing
 | 
			
		||||
-------------------------
 | 
			
		||||
When running tests, you would want to avoid as much as possible setting up test
 | 
			
		||||
cases by creating a Pecan app on each instance. To avoid this, you need to
 | 
			
		||||
create a proper test configuration file and load it at setup time.
 | 
			
		||||
When you create a new project using the ``base`` project template, Pecan adds 
 | 
			
		||||
a reference to its ``py.test`` plugin to your project's ``setup.cfg`` file. 
 | 
			
		||||
This handles loading your Pecan configuration and setting up your app as 
 | 
			
		||||
defined by your project's ``app.py`` file.
 | 
			
		||||
 | 
			
		||||
To do this, you need to know the absolute path for your configuration file and 
 | 
			
		||||
then call ``set_config`` with it. A typical ``setUp`` method would look like::
 | 
			
		||||
If you've created your own project without using Pecan's template, you can 
 | 
			
		||||
load the plugin yourself by adding this to your ``setup.cfg`` file::
 | 
			
		||||
 | 
			
		||||
    [pytest]
 | 
			
		||||
    addopts = -p pecan.testing --with-config=./config.py
 | 
			
		||||
 | 
			
		||||
Alternatively, you can just pass those options to ``py.test`` directly.
 | 
			
		||||
 | 
			
		||||
By default, Pecan's testing plugin assumes you will be using the ``config.py`` 
 | 
			
		||||
configuration file to run your tests. To change which configuration file gets 
 | 
			
		||||
used once, run ``py.test`` with the `--with-config` option. To make the change 
 | 
			
		||||
permanent, modify that option in the `addopts` setting of your ``setup.cfg`` 
 | 
			
		||||
file.
 | 
			
		||||
 | 
			
		||||
Pecan's ``py.test`` plugin exposes two new variables in the ``py.test`` 
 | 
			
		||||
namespace: ``temp_dir`` and ``wsgi_app``.
 | 
			
		||||
 | 
			
		||||
``py.test.temp_dir`` is a temporary directory that you can use for your tests. 
 | 
			
		||||
It's created at startup and deleted after all tests have completed. When using 
 | 
			
		||||
locally distributed testing with py.test, this is guaranteed to be shared by 
 | 
			
		||||
each test process. This is useful if you need to create some initial resource 
 | 
			
		||||
(e.g., a database template) that is later copied by each test. If you're using 
 | 
			
		||||
remotely distributed testing, the directory won't be shared across nodes.
 | 
			
		||||
 | 
			
		||||
``py.test.wsgi_app`` is your Pecan app loaded and configured per your project's 
 | 
			
		||||
``app.py`` file. In your test's ``setUp`` method, you would wrap this with 
 | 
			
		||||
``TestApp``::
 | 
			
		||||
 | 
			
		||||
    from unittest import TestCase
 | 
			
		||||
    from webtest import TestApp
 | 
			
		||||
 | 
			
		||||
    import py.test
 | 
			
		||||
 | 
			
		||||
    class TestRootController(TestCase):
 | 
			
		||||
    
 | 
			
		||||
        def setUp(self):
 | 
			
		||||
        config_path = '/path/to/test_config.py'
 | 
			
		||||
        pecan.set_config(config_path)
 | 
			
		||||
 | 
			
		||||
        self.app = TestApp(
 | 
			
		||||
            make_app(
 | 
			
		||||
                config.app.root
 | 
			
		||||
                template_path   = config.app.template_path
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
As you can see, we are loading the configuration file into Pecan first and then
 | 
			
		||||
creating a Pecan application with it. Any interaction after ``setUp`` will be
 | 
			
		||||
exactly as if your application was really running via an HTTP server.
 | 
			
		||||
            self.app = TestApp(py.test.wsgi_app)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Using WebTest with a UnitTest
 | 
			
		||||
-----------------------------
 | 
			
		||||
Once you have a ``setUp`` method with your Pecan configuration loaded you have
 | 
			
		||||
a wealth of actions provided within the test class to interact with your Pecan
 | 
			
		||||
Once you have a ``setUp`` method with your ``TestApp`` created, you have a 
 | 
			
		||||
wealth of actions provided within the test class to interact with your Pecan
 | 
			
		||||
application::
 | 
			
		||||
 | 
			
		||||
 *  POST   => self.app.post
 | 
			
		||||
@@ -72,19 +92,18 @@ application::
 | 
			
		||||
 *  DELETE => self.app.delete
 | 
			
		||||
 *  PUT    => self.app.put
 | 
			
		||||
 | 
			
		||||
For example, if I wanted to assert that I can get the root of my application,
 | 
			
		||||
I would probably do something similar to this::
 | 
			
		||||
For example, if you want to assert that you can get to the root of your 
 | 
			
		||||
application, you could do something similar to this::
 | 
			
		||||
 | 
			
		||||
    response = self.app.get('/')
 | 
			
		||||
    assert response.status_int == 200
 | 
			
		||||
 | 
			
		||||
If you are expecting error responses from your application, you should make
 | 
			
		||||
sure that you pass the `expect_errors` flag and set it to True::
 | 
			
		||||
If you are expecting error responses from your application, make sure to pass 
 | 
			
		||||
`expect_errors=True`::
 | 
			
		||||
 | 
			
		||||
    response = self.app.get('/url/does/not/exist', expect_errors=True)
 | 
			
		||||
    assert response.status_int == 404
 | 
			
		||||
 | 
			
		||||
If you would like to dig in to more examples in how to test and verify more
 | 
			
		||||
actions, make sure you take a look at the 
 | 
			
		||||
actions, take a look at the 
 | 
			
		||||
`WebTest documentation <http://pythonpaste.org/webtest/>`_
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -32,36 +32,31 @@ class Command(paste_command.Command):
 | 
			
		||||
            ex.args[0] = self.parser.error(ex.args[0])
 | 
			
		||||
            raise
 | 
			
		||||
    
 | 
			
		||||
    def can_import(self, name):
 | 
			
		||||
        try:
 | 
			
		||||
            __import__(name)
 | 
			
		||||
            return True
 | 
			
		||||
        except ImportError:
 | 
			
		||||
            return False
 | 
			
		||||
    
 | 
			
		||||
    def get_package_names(self, config):
 | 
			
		||||
        if not hasattr(config.app, 'modules'):
 | 
			
		||||
            return []
 | 
			
		||||
        return [module.__name__ for module in config.app.modules if hasattr(module, '__name__')]
 | 
			
		||||
    
 | 
			
		||||
    def import_module(self, package, name):
 | 
			
		||||
        parent = __import__(package, fromlist=[name])
 | 
			
		||||
        return getattr(parent, name, None)
 | 
			
		||||
    
 | 
			
		||||
    def load_configuration(self, name):
 | 
			
		||||
        set_config(name)
 | 
			
		||||
        return _runtime_conf
 | 
			
		||||
    
 | 
			
		||||
    def load_app(self, config):
 | 
			
		||||
        for package_name in self.get_package_names(config):
 | 
			
		||||
            module_name = '%s.app' % package_name
 | 
			
		||||
            if self.can_import(module_name):
 | 
			
		||||
                module = sys.modules[module_name]
 | 
			
		||||
            module = self.import_module(package_name, 'app')
 | 
			
		||||
            if hasattr(module, 'setup_app'):
 | 
			
		||||
                return module.setup_app(config)
 | 
			
		||||
        raise paste_command.BadCommand('No app.setup_app found in any of the configured app.modules')
 | 
			
		||||
        raise paste_command.BadCommand('No app.setup_app found in any app modules')
 | 
			
		||||
    
 | 
			
		||||
    def load_model(self, config):
 | 
			
		||||
        for package_name in self.get_package_names(config):
 | 
			
		||||
            module_name = '%s.model' % package_name
 | 
			
		||||
            if self.can_import(module_name):
 | 
			
		||||
                return sys.modules[module_name]
 | 
			
		||||
            module = self.import_module(package_name, 'model')
 | 
			
		||||
            if module:
 | 
			
		||||
                return module
 | 
			
		||||
        return None
 | 
			
		||||
    
 | 
			
		||||
    def logging_file_config(self, config_file):
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,7 @@ class TestConfigApp(TestCase):
 | 
			
		||||
        assert len(config.app['modules']) == 1
 | 
			
		||||
        
 | 
			
		||||
    def test_app_static_root(self):
 | 
			
		||||
        assert config.app['static_root'] == 'public'
 | 
			
		||||
        assert 'public' in config.app['static_root']
 | 
			
		||||
        
 | 
			
		||||
    def test_app_template_path(self):
 | 
			
		||||
        assert 'templates' in config.app['template_path']
 | 
			
		||||
 
 | 
			
		||||
@@ -1,20 +1,13 @@
 | 
			
		||||
from unittest import TestCase
 | 
			
		||||
from webtest import TestApp
 | 
			
		||||
from pecan import make_app
 | 
			
		||||
 | 
			
		||||
from ${package}.controllers.root import RootController
 | 
			
		||||
import py.test
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class TestRootController(TestCase):
 | 
			
		||||
    
 | 
			
		||||
    def setUp(self):
 | 
			
		||||
 | 
			
		||||
        self.app = TestApp(
 | 
			
		||||
            make_app(
 | 
			
		||||
                RootController(),
 | 
			
		||||
                template_path = '${package}/templates'
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
        self.app = TestApp(py.test.wsgi_app)
 | 
			
		||||
    
 | 
			
		||||
    def test_get(self):
 | 
			
		||||
        response = self.app.get('/')
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								pecan/templates/project/setup.cfg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								pecan/templates/project/setup.cfg
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
			
		||||
[pytest]
 | 
			
		||||
addopts = -p pecan.testing --with-config=./config.py
 | 
			
		||||
							
								
								
									
										219
									
								
								pecan/testing.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										219
									
								
								pecan/testing.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,219 @@
 | 
			
		||||
"""
 | 
			
		||||
Plugin for py.test that sets up the app.
 | 
			
		||||
 | 
			
		||||
App configuration inspired by the Pylons nose equivalent:
 | 
			
		||||
https://github.com/Pylons/pylons/blob/master/pylons/test.py
 | 
			
		||||
 | 
			
		||||
Handling of multiprocessing inspired by pytest-cov.
 | 
			
		||||
"""
 | 
			
		||||
from pecan import conf, set_config
 | 
			
		||||
from tempfile import mkdtemp
 | 
			
		||||
 | 
			
		||||
import py
 | 
			
		||||
import py.test
 | 
			
		||||
import os
 | 
			
		||||
import shutil
 | 
			
		||||
import socket
 | 
			
		||||
import sys
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def pytest_addoption(parser):
 | 
			
		||||
    """
 | 
			
		||||
    Adds the custom "with-config" option to take in the config file.
 | 
			
		||||
    """
 | 
			
		||||
    group = parser.getgroup('pecan')
 | 
			
		||||
    group._addoption('--with-config', 
 | 
			
		||||
                     dest='config_file', 
 | 
			
		||||
                     metavar='path', 
 | 
			
		||||
                     default='./test.py',
 | 
			
		||||
                     action='store', 
 | 
			
		||||
                     type='string', 
 | 
			
		||||
                     help='configuration file for pecan tests')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def pytest_configure(config):
 | 
			
		||||
    """
 | 
			
		||||
    Loads the Pecan plugin if using a configuration file.
 | 
			
		||||
    """
 | 
			
		||||
    if config.getvalue('config_file'):
 | 
			
		||||
        config.pluginmanager.register(PecanPlugin(config), '_pecan')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PecanPlugin(object):
 | 
			
		||||
    """
 | 
			
		||||
    Plugin for a Pecan application. Sets up and tears down the 
 | 
			
		||||
    WSGI application based on the configuration and session type.
 | 
			
		||||
    """
 | 
			
		||||
    
 | 
			
		||||
    def __init__(self, config):
 | 
			
		||||
        self.config = config
 | 
			
		||||
        self.impl = None
 | 
			
		||||
    
 | 
			
		||||
    def pytest_namespace(self):
 | 
			
		||||
        """
 | 
			
		||||
        Add the session variables to the namespace.
 | 
			
		||||
        """
 | 
			
		||||
        return {
 | 
			
		||||
            'temp_dir': None,
 | 
			
		||||
            'wsgi_app': None
 | 
			
		||||
        }
 | 
			
		||||
    
 | 
			
		||||
    def pytest_sessionstart(self, session):
 | 
			
		||||
        """
 | 
			
		||||
        Set up the testing session.
 | 
			
		||||
        """
 | 
			
		||||
        self.impl = PecanPluginImpl.create_from_session(session)
 | 
			
		||||
        self.impl.sessionstart(session)
 | 
			
		||||
    
 | 
			
		||||
    def pytest_configure_node(self, node):
 | 
			
		||||
        """
 | 
			
		||||
        Configures a new slave node.
 | 
			
		||||
        """
 | 
			
		||||
        if self.impl:
 | 
			
		||||
            self.impl.configure_node(node)
 | 
			
		||||
    
 | 
			
		||||
    def pytest_testnodedown(self, node, error):
 | 
			
		||||
        """
 | 
			
		||||
        Tears down an exiting node.
 | 
			
		||||
        """
 | 
			
		||||
        if self.impl:
 | 
			
		||||
            self.impl.testnodedown(node, error)
 | 
			
		||||
    
 | 
			
		||||
    def pytest_sessionfinish(self, session, exitstatus):
 | 
			
		||||
        """
 | 
			
		||||
        Cleans up the testing session.
 | 
			
		||||
        """
 | 
			
		||||
        if self.impl:
 | 
			
		||||
            self.impl.sessionfinish(session, exitstatus)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PecanPluginImpl(object):
 | 
			
		||||
    """
 | 
			
		||||
    Actual implementation of the Pecan plugin. This ensures the proper 
 | 
			
		||||
    environment is configured for each session type.
 | 
			
		||||
    """
 | 
			
		||||
    
 | 
			
		||||
    def __init__(self, config):
 | 
			
		||||
        self.config = config
 | 
			
		||||
        self.log = py.log.Producer('pecan-%s' % self.name)
 | 
			
		||||
        if not config.option.debug:
 | 
			
		||||
            py.log.setconsumer(self.log._keywords, None)
 | 
			
		||||
        self.log('Created %s instance' % self.__class__.__name__)
 | 
			
		||||
    
 | 
			
		||||
    @property
 | 
			
		||||
    def name(self):
 | 
			
		||||
        return 'main'
 | 
			
		||||
    
 | 
			
		||||
    def _setup_app(self):
 | 
			
		||||
        self.log('Invoking setup_app')
 | 
			
		||||
        path = os.getcwd()
 | 
			
		||||
        if path not in sys.path:
 | 
			
		||||
            sys.path.insert(0, path)
 | 
			
		||||
        set_config(self.config.getvalue('config_file'))
 | 
			
		||||
        py.test.wsgi_app = self._load_app(conf)
 | 
			
		||||
    
 | 
			
		||||
    def _get_package_names(self, config):
 | 
			
		||||
        if not hasattr(config.app, 'modules'):
 | 
			
		||||
            return []
 | 
			
		||||
        return [module.__name__ for module in config.app.modules if hasattr(module, '__name__')]
 | 
			
		||||
    
 | 
			
		||||
    def _can_import(self, name):
 | 
			
		||||
        try:
 | 
			
		||||
            __import__(name)
 | 
			
		||||
            return True
 | 
			
		||||
        except ImportError:
 | 
			
		||||
            return False
 | 
			
		||||
    
 | 
			
		||||
    def _load_app(self, config):
 | 
			
		||||
        for package_name in self._get_package_names(config):
 | 
			
		||||
            module_name = '%s.app' % package_name
 | 
			
		||||
            if self._can_import(module_name):
 | 
			
		||||
                module = sys.modules[module_name]
 | 
			
		||||
                if hasattr(module, 'setup_app'):
 | 
			
		||||
                    return module.setup_app(config)
 | 
			
		||||
        raise RuntimeError('No app.setup_app found in any of the configured app.modules')
 | 
			
		||||
    
 | 
			
		||||
    def _create_temp_directory(self):
 | 
			
		||||
        temp_dir = mkdtemp()
 | 
			
		||||
        self.log('Created temporary directory %s' % temp_dir)
 | 
			
		||||
        py.test.temp_dir = temp_dir
 | 
			
		||||
    
 | 
			
		||||
    def _delete_temp_directory(self):
 | 
			
		||||
        if py.test.temp_dir and os.path.exists(py.test.temp_dir):
 | 
			
		||||
            self.log('Removing temporary directory %s' % py.test.temp_dir)
 | 
			
		||||
            shutil.rmtree(py.test.temp_dir)
 | 
			
		||||
    
 | 
			
		||||
    def sessionstart(self, session):
 | 
			
		||||
        self.log('Starting session')
 | 
			
		||||
        self._setup_app()
 | 
			
		||||
        self._create_temp_directory()
 | 
			
		||||
    
 | 
			
		||||
    def configure_node(self, node):
 | 
			
		||||
        pass
 | 
			
		||||
    
 | 
			
		||||
    def testnodedown(self, node, error):
 | 
			
		||||
        pass
 | 
			
		||||
    
 | 
			
		||||
    def sessionfinish(self, session, exitstatus):
 | 
			
		||||
        self.log('Stopping session')
 | 
			
		||||
        self._delete_temp_directory()
 | 
			
		||||
    
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def create_from_session(session):
 | 
			
		||||
        if session.config.option.dist != 'no':
 | 
			
		||||
            impl_cls = MasterPecanPluginImpl
 | 
			
		||||
        elif getattr(session.config, 'slaveinput', {}).get('slaveid'):
 | 
			
		||||
            impl_cls = SlavePecanPluginImpl
 | 
			
		||||
        else:
 | 
			
		||||
            impl_cls = PecanPluginImpl
 | 
			
		||||
        return impl_cls(session.config)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class MasterPecanPluginImpl(PecanPluginImpl):
 | 
			
		||||
    """
 | 
			
		||||
    Plugin implementation for distributed master.
 | 
			
		||||
    """
 | 
			
		||||
    
 | 
			
		||||
    def sessionstart(self, session):
 | 
			
		||||
        self.log('Starting master session')
 | 
			
		||||
        self._setup_app()
 | 
			
		||||
        self._create_temp_directory()
 | 
			
		||||
    
 | 
			
		||||
    def configure_node(self, node):
 | 
			
		||||
        self.log('Configuring slave node %s' % node.gateway.id)
 | 
			
		||||
        node.slaveinput['pecan_master_host'] = socket.gethostname()
 | 
			
		||||
        node.slaveinput['pecan_temp_dir'] = py.test.temp_dir
 | 
			
		||||
    
 | 
			
		||||
    def sessionfinish(self, session, exitstatus):
 | 
			
		||||
        self.log('Stopping master session')
 | 
			
		||||
        self._delete_temp_directory()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class SlavePecanPluginImpl(PecanPluginImpl):
 | 
			
		||||
    """
 | 
			
		||||
    Plugin implementation for distributed slaves.
 | 
			
		||||
    """
 | 
			
		||||
    
 | 
			
		||||
    @property
 | 
			
		||||
    def name(self):
 | 
			
		||||
        return self.config.slaveinput['slaveid']
 | 
			
		||||
    
 | 
			
		||||
    def _is_collocated(self, session):
 | 
			
		||||
        return (socket.gethostname() == session.config.slaveinput['pecan_master_host'])
 | 
			
		||||
    
 | 
			
		||||
    def _set_temp_directory(self, session):
 | 
			
		||||
        self.log('Setting temporary directory to %s' % session.config.slaveinput['pecan_temp_dir'])
 | 
			
		||||
        py.test.temp_dir = session.config.slaveinput['pecan_temp_dir']
 | 
			
		||||
    
 | 
			
		||||
    def sessionstart(self, session):
 | 
			
		||||
        self.log('Starting slave session')
 | 
			
		||||
        self._setup_app()
 | 
			
		||||
        if self._is_collocated(session):
 | 
			
		||||
            self._set_temp_directory(session)
 | 
			
		||||
        else:
 | 
			
		||||
            self._create_temp_directory()
 | 
			
		||||
    
 | 
			
		||||
    def sessionfinish(self, session, exitstatus):
 | 
			
		||||
        self.log('Stopping slave session')
 | 
			
		||||
        if not self._is_collocated(session):
 | 
			
		||||
            self._delete_temp_directory()
 | 
			
		||||
							
								
								
									
										7
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										7
									
								
								setup.py
									
									
									
									
									
								
							@@ -5,10 +5,6 @@ version = '0.1'
 | 
			
		||||
#
 | 
			
		||||
# integration with py.test for `python setup.py test`
 | 
			
		||||
#
 | 
			
		||||
tests_require = [
 | 
			
		||||
  "pytest"
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
class PyTest(Command):
 | 
			
		||||
    user_options = []
 | 
			
		||||
    def initialize_options(self):
 | 
			
		||||
@@ -31,7 +27,8 @@ requirements = [
 | 
			
		||||
  "Paste >= 1.7.5.1",
 | 
			
		||||
  "PasteScript >= 1.7.3",
 | 
			
		||||
  "formencode >= 1.2.2",
 | 
			
		||||
  "WebTest >= 1.2.2"
 | 
			
		||||
  "WebTest >= 1.2.2",
 | 
			
		||||
  "pytest >= 2.0.3"
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
try:
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user