The `Application` class is now a sub-class of `BaseApplication`.
Applications that require more control of the framework's behavior can
extend `BaseApplication` instead of `Application`. The `BaseApplication`
class also provides some new features:
* `BaseApplication.setup_routes` is always called when an instance is
created, and can be used to configure the request dispatcher
* `BaseApplication.update_config` updates the application configuration
from a configuration file
The major difference between `BaseApplication` and `Application` is that
the latter requires a request dispatcher, while the former does not.
This pattern allows simple applications to construct a `Router` or root
object and then initialize the `Application` without needing to define a
sub-class.
This commit breaks up the `Application.__call__` method into smaller methods
that can be overridden by subclasses. These methods allow customization of
various steps of the request/response handling process:
* `make_request`: Create the `Request` object from the WSGI environment
dictionary. The default implementation creates a `milla.Request` object,
copies the application configuration to its `config` attribute, and handles
"emulated" HTTP methods from POST data.
* `resolve_path`: Locates a controller callable from the given path info. The
default implementation calls the `resolve` method on the application's
`dispatcher` attribute. If `UnresolvePath` is raised, it returns a callable
that raises `HTTPNotFound`.
* `handle_error`: Called inside the exception handler when a controller
callable raises an exception. The method should return a callable WSGI
application (such as a `Response` or `WSGIHTTPException` object). To access
the exception that was raised, use the `sys.exc_info` function. The default
implementation returns the exception if it is an instance of
`WSGIHTTPException`, or re-raises the exception otherwise. This allows
middleware applications to handle the exception, if desired.
Using the `milla.app.Application._find_attr` method to find the
`allowed_methods` of returned controller callables allows the attribute to be
defined e.g. on the class for instance methods, etc.
Some WSGI servers, e.g. Werkzeug, unconditionally attempt to iterate over the
application response, even for HEAD requests. If `None` is returned, then the
server will crash in this case, because it is not iterable. This commit alters
the behavior of `milla.Application` to return an empty string, which is
iterable and has the same effect of not sending a body.
If a controller callable returns a string, it needs to be wrapped in a
Response object. To determine if this is the case, the Application tests to
see if the returned object is an instance of `basestring`. Since `basestring`
doesn't exist in Python 3, only `str` is a valid return type.
Unfortunately, my way of testing whether the `basestring` type existed was
flawed. Instead of raising `NameError` when it doesn't exist,
`UnboundLocalError` (a subclass `NameError`) is *always* raised. Since the
exception handler sets `basestring` equal to `str` assuming this is Python 3,
most of the time this isn't a problem. If, however, the controller returns a
`unicode` object in Python 2, the `isinstance` call returns `False`, so the
response is not wrapped in a Response object.
Rather than try to reassign the `basestring` name, now we just use `_string`,
which will either be `basestring` (in Python 2) or `str` (in Python 3).
Apparently, the unit tests didn't cover this case...
For convenience, a `Controller` class now exists from which all classes wanting to implement these methods should descend. This will allow sane cooperative multiple inheritance.
* Created a dispatcher protocol for classes to implement for path resolution
* Implemented a traversal dispatcher
* Updated the router to implement the dispatcher protocol
* Added unit tests for both dispatchers
* Replaced the Controller class with an Application class