diff --git a/docs/source/configuration.rst b/docs/source/configuration.rst index 05c4df4..cf005b5 100644 --- a/docs/source/configuration.rst +++ b/docs/source/configuration.rst @@ -2,11 +2,10 @@ Configuration ============= -Pecan is very easy to configure. As long as you follow certain conventions; +Pecan is very easy to configure. As long as you follow certain conventions, using, setting and dealing with configuration should be very intuitive. -Python files is what the framework uses to get the values from configuration -files. These files need to specify the values in a key/value way (Python +Pecan configuration files are pure Python. These files need to specify the values in a key/value way (Python dictionaries) or if you need simple one-way values you can also specify them as direct variables (more on that below). @@ -48,12 +47,14 @@ Things like debug mode, Root Controller and possible Hooks, should be specified here. This is what is used when the framework is wrapping your application into a valid WSGI app. -A typical application configuration would look like this:: +A typical application configuration might look like this:: app = { - 'root' : RootController(), - 'static_root' : 'public', - 'template_path' : 'project/templates', + 'root' : 'project.controllers.root.RootController', + 'modules' : ['project'], + 'static_root' : '%(confdir)s/public', + 'template_path' : '%(confdir)s/project/templates', + 'reload' : True, 'debug' : True } @@ -62,18 +63,22 @@ Let's look at each value and what it means: **app** is a reserved variable name for the configuration, so make sure you are not overriding, otherwise you will get default values. -**root** Needs the Root Controller of your application. Remember that you are -passing an object instance, so you'll need to import it at the top of the file. -In the example configuration, this would look something like:: +**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`). - from myproject.controllers.root import RootController +**static_root** Points to the directory where your static files live (relative +to the project root). -**static_root** Points to the directory where your static files live. +**template_path** Points to the directory where your template files live +(relative to the project root). -**template_path** Points to the directory where your template files live. +**reload** - When ``True``, ``pecan serve`` will listen for file changes and +restare your app (especially useful for development). -**debug** Enables ``WebError`` to have full tracebacks in the browser (this is -OFF by default). +**debug** Enables ``WebError`` to have display tracebacks in the browser +(**IMPORTANT**: Make sure this is *always* set to ``False`` in production +environments). .. _server_configuration: @@ -81,7 +86,7 @@ OFF by default). Server Configuration -------------------- Pecan provides some defaults. Change these to alter the host and port your -WSGI app is served on.:: +WSGI app is served on:: server = { 'port' : '8080', @@ -92,23 +97,18 @@ WSGI app is served on.:: Accessing Configuration at Runtime ---------------------------------- -You can access any configuration values at runtime via ``pecan.conf``. +You can access any configuration value at runtime via ``pecan.conf``. This includes custom, application and server-specific values. -Below is an example on how to access those values from your application:: - -Custom and Single Values ------------------------- -There might be times when you do not need a dictionary, but instead a simple -value. For example, if you needed to specify a global administrator, you could +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 `pecan.conf` like:: +And it would be accessible in `pecan.conf` as:: - >>>> from pecan import conf - >>>> conf.administrator + >>> from pecan import conf + >>> conf.administrator 'foo_bar_user' diff --git a/docs/source/installation.rst b/docs/source/installation.rst index b829012..2f43623 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -13,7 +13,7 @@ Pecan can be isolated from other packages is best practice. To get started with an environment for Pecan, create a new `virtual environment `_:: - virtualenv --no-site-packages pecan-env + virtualenv pecan-env cd pecan-env source bin/activate @@ -31,12 +31,16 @@ After a lot of output, you should have Pecan successfully installed. Development (Unstable) Version ------------------------------ If you want to run the development version of Pecan you will -need to install git and clone the repo from github:: +need to install git and clone the repo from GitHub:: - git clone https://github.com/pecan/pecan.git + git clone https://github.com/dreamhost/pecan.git If your virtual environment is still activated, call ``setup.py`` to install the development version:: cd pecan python setup.py develop + +...alternatively, you can also install from GitHub directly with ``pip``:: + + pip install -e git://github.com/dreamhost/pecan.git#egg=pecan diff --git a/docs/source/quick_start.rst b/docs/source/quick_start.rst index 13c93d6..63d2538 100644 --- a/docs/source/quick_start.rst +++ b/docs/source/quick_start.rst @@ -114,11 +114,7 @@ Simple Configuration -------------------- For ease of use, Pecan configuration files are pure Python. -This is how your default configuration file should look:: - - from test_project.controllers.root import RootController - - import test_project +This is how your default (generated) configuration file should look:: # Server Specific Configurations server = { @@ -128,8 +124,8 @@ This is how your default configuration file should look:: # Pecan Application Configurations app = { - 'root' : RootController(), - 'modules' : [test_project], + 'root' : 'test_project.controllers.root.RootController', + 'modules' : ['test_project'], 'static_root' : '%(confdir)s/public', 'template_path' : '%(confdir)s/test_project/templates', 'reload': True, @@ -162,9 +158,10 @@ Root Controller --------------- The Root Controller is the root of your application. -This is how it looks in the project template:: +This is how it looks in the project template +(``test_project.controllers.root.RootController``):: - from pecan import expose, request + from pecan import expose from formencode import Schema, validators as v from webob.exc import status_map @@ -175,13 +172,23 @@ This is how it looks in the project template:: class RootController(object): - @expose('index.html') - def index(self, name='', age=''): - return dict(errors=request.validation_errors, name=name, age=age) + + @expose( + generic = True, + template = 'index.html' + ) + def index(self): + return dict() - @expose('success.html', schema=SampleForm(), error_handler='index') - def handle_form(self, name, age): - return dict(name=name, age=age) + @index.when( + method = 'POST', + template = 'success.html', + schema = SampleForm(), + error_handler = '/index', + htmlfill = dict(auto_insert_errors = True, prefix_error = False) + ) + def index_post(self, name, age): + return dict(name=name) @expose('error.html') def error(self, status): @@ -193,34 +200,32 @@ This is how it looks in the project template:: return dict(status=status, message=message) +You can specify additional classes and methods if you need to do so, but for +now we have an *index* and *index_post* method. -You can specify additional classes if you need to do so, but for now we have an -*index* and *handle_form* method. - -**index**: is *exposed* via the decorator ``@expose`` (which in turn uses the +**def index**: is *exposed* via the decorator ``@expose`` (which in turn uses the ``index.html`` template) at the root of the application (http://127.0.0.1:8080/), -so anything that hits the root of your application will touch this method. +so any HTTP GET that hits the root of your application (/) will be routed to +this method. Notice that the index method returns a dictionary - this dictionary is used as a namespace to render the specified template (``index.html``) into HTML. -Since we are performing form validation and want to pass any errors we might -get to the template, we set ``errors`` to receive form validation errors that -may exist in ``request.validation_errors``. - - -**handle_form**: receives 2 arguments (*name* and *age*) that are validated +**def index_post**: receives 2 arguments (*name* and *age*) that are validated through the *SampleForm* schema. +``method`` has been set to 'POST', so HTTP POSTs to the application root (in +our example, form submissions) will be routed to this method. + The ``error_handler`` has been set to index. This means that when errors are raised, they will be sent to the index controller and rendered through its template. -**error**: Finally, we have the error controller that allows your application to +**def error**: Finally, we have the error controller that allows your application to display custom pages for certain HTTP errors (404, etc...). Application Interaction ----------------------- If you still have your application running and you visit it in your browser, -you should see a page with some information about Pecan and a form so you can +you should see a page with some information about Pecan and the form so you can play a bit. diff --git a/docs/source/routing.rst b/docs/source/routing.rst index 5b502b9..9fc0dd5 100644 --- a/docs/source/routing.rst +++ b/docs/source/routing.rst @@ -3,11 +3,13 @@ Routing ======= -When a user requests a Pecan-powered page how does Pecan know which -controller to use? Pecan uses a method known as object-dispatch to map an +When a user requests a certain URL in your app, how does Pecan know which +controller to route to? Pecan uses a method known as **object-dispatch** to map an HTTP request to a controller. Object-dispatch begins by splitting the path into a list of components and then walking an object path, starting at -the root controller. Let's look at a simple bookstore application: +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: :: diff --git a/pecan/commands/base.py b/pecan/commands/base.py index b630274..63e2efd 100644 --- a/pecan/commands/base.py +++ b/pecan/commands/base.py @@ -35,7 +35,7 @@ class Command(paste_command.Command): 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__')] + return config.app.modules def import_module(self, package, name): parent = __import__(package, fromlist=[name]) diff --git a/pecan/core.py b/pecan/core.py index 2dc5a38..a01d40f 100644 --- a/pecan/core.py +++ b/pecan/core.py @@ -188,7 +188,8 @@ class Pecan(object): ''' Creates a Pecan application instance, which is a WSGI application. - :param root: The root controller object. + :param root: A string representing a root controller object (e.g., + "myapp.controller.root.RootController") :param default_renderer: The default rendering engine to use. Defaults to mako. :param template_path: The default relative path to use for templates. Defaults to 'templates'. :param hooks: A list of Pecan hook objects to use for this application. @@ -197,12 +198,40 @@ class Pecan(object): :param force_canonical: A boolean indicating if this project should require canonical URLs. ''' + if isinstance(root, basestring): + root = self.__translate_root__(root) + self.root = root self.renderers = RendererFactory(custom_renderers, extra_template_vars) self.default_renderer = default_renderer self.hooks = hooks self.template_path = template_path self.force_canonical = force_canonical + + 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:] + + try: + module = __import__(name, fromlist=fromlist) + kallable = getattr(module, parts[-1]) + assert hasattr(kallable, '__call__'), "%s does not represent a callable class or function." % item + return kallable() + except AttributeError, e: + raise ImportError('No item named %s' % item) + + raise ImportError('No item named %s' % item) def route(self, node, path): ''' diff --git a/pecan/deploy.py b/pecan/deploy.py index 36218d1..bd7f9d8 100644 --- a/pecan/deploy.py +++ b/pecan/deploy.py @@ -4,7 +4,7 @@ def deploy(config_module_or_path): set_config(config_module_or_path) for module in getattr(conf.app, 'modules'): try: - module_app = import_module('%s.app' % module.__name__) + module_app = import_module('%s.app' % module) if hasattr(module_app, 'setup_app'): return module_app.setup_app(conf) except ImportError: diff --git a/pecan/templates/project/config.py_tmpl b/pecan/templates/project/config.py_tmpl index 424e017..3ef9c49 100644 --- a/pecan/templates/project/config.py_tmpl +++ b/pecan/templates/project/config.py_tmpl @@ -1,7 +1,3 @@ -from ${package}.controllers.root import RootController - -import ${package} - # Server Specific Configurations server = { 'port' : '8080', @@ -10,8 +6,8 @@ server = { # Pecan Application Configurations app = { - 'root' : RootController(), - 'modules' : [${package}], + 'root' : '${package}.controllers.root.RootController', + 'modules' : ['${package}'], 'static_root' : '%(confdir)s/public', 'template_path' : '%(confdir)s/${package}/templates', 'reload' : True, diff --git a/pecan/tests/test_base.py b/pecan/tests/test_base.py index 772d9f1..ae68603 100644 --- a/pecan/tests/test_base.py +++ b/pecan/tests/test_base.py @@ -11,6 +11,9 @@ from pecan.decorators import accept_noncanonical import os +class SampleRootController(object): pass + + class TestBase(TestCase): def test_simple_app(self): @@ -31,6 +34,10 @@ class TestBase(TestCase): r = app.get('/index.html') assert r.status_int == 200 assert r.body == 'Hello, World!' + + def test_controller_lookup_by_string_path(self): + app = Pecan('pecan.tests.test_base.SampleRootController') + assert app.root and isinstance(app.root, SampleRootController) def test_object_dispatch(self): class SubSubController(object): diff --git a/pecan/tests/test_config/sample_apps/sample_app_config.py b/pecan/tests/test_config/sample_apps/sample_app_config.py index 63b8d7f..9e01138 100644 --- a/pecan/tests/test_config/sample_apps/sample_app_config.py +++ b/pecan/tests/test_config/sample_apps/sample_app_config.py @@ -1,7 +1,7 @@ import sample_app app = { - 'modules': [sample_app] + 'modules': ['sample_app'] } foo = { diff --git a/pecan/tests/test_config/sample_apps/sample_app_config_missing.py b/pecan/tests/test_config/sample_apps/sample_app_config_missing.py index 77dae46..caf3929 100644 --- a/pecan/tests/test_config/sample_apps/sample_app_config_missing.py +++ b/pecan/tests/test_config/sample_apps/sample_app_config_missing.py @@ -1,7 +1,7 @@ import sample_app_missing app = { - 'modules': [sample_app_missing] + 'modules': ['sample_app_missing'] } foo = { diff --git a/pecan/tests/test_deploy.py b/pecan/tests/test_deploy.py index da1784b..2717727 100644 --- a/pecan/tests/test_deploy.py +++ b/pecan/tests/test_deploy.py @@ -16,7 +16,7 @@ class TestDeploy(TestCase): def test_module_lookup(self): """ 1. A config file has: - app { 'modules': [valid_module] } + app { 'modules': ['valid_module'] } 2. The module, `valid_module` has an app.py that defines a `def setup.py` """ test_config_file = os.path.join(os.path.dirname(__file__), 'test_config', 'sample_apps', 'sample_app_config.py') @@ -25,7 +25,7 @@ class TestDeploy(TestCase): def test_module_lookup_find_best_match(self): """ 1. A config file has: - app { 'modules': [invalid_module, valid_module] } + app { 'modules': ['invalid_module', 'valid_module'] } 2. The module, `valid_module` has an app.py that defines a `def setup_app` """ test_config_file = os.path.join(os.path.dirname(__file__), 'test_config', 'sample_apps', 'sample_app_config.py') @@ -34,7 +34,7 @@ class TestDeploy(TestCase): def test_missing_app_file_lookup(self): """ 1. A config file has: - app { 'modules': [valid_module] } + app { 'modules': ['valid_module'] } 2. The module has no `app.py` file. """ test_config_file = os.path.join(os.path.dirname(__file__), 'test_config', 'sample_apps', 'sample_app_config_missing.py') @@ -47,7 +47,7 @@ class TestDeploy(TestCase): def test_missing_setup_app(self): """ 1. A config file has: - app { 'modules': [valid_module] } + app { 'modules': ['valid_module'] } 2. The module, `valid_module` has an `app.py` that contains no `def setup_app` """ test_config_file = os.path.join(os.path.dirname(__file__), 'test_config', 'sample_apps', 'sample_app_config_missing_app.py')