application.py |
|
---|---|
Primary WSGI application and framework entry point. |
|
Imports |
from __future__ import unicode_literals
import logging
import logging.config
from inspect import isroutine, isfunction, ismethod, getcallargs
from webob.exc import HTTPException, HTTPNotFound
from marrow.package.loader import load
from .context import Context
from .dispatch import WebDispatchers
from .extension import WebExtensions
from .view import WebViews
from ..ext.base import BaseExtension
if __debug__:
from .util import safe_name
|
Module Globals |
|
A standard Python logger object. |
log = __import__('logging').getLogger(__name__)
|
WSGI Application |
|
The WebCore WSGI application. This glues together a few components:
|
class Application(object):
__slots__ = (
'config', # Application configuration.
'feature', # Feature tag announcement; populated by the `provides` of active extensions.
'__context', # Application context instance.
'RequestContext', # Per-request context class.
'__call__', # WSGI request handler. Dynamically assigned.
)
|
Construct the initial ApplicationContext, populate, and prepare the WSGI stack. No actions other than configuration should happen during construction. Current configuration is limited to three arguments:
|
def __init__(self, root, **config):
self.config = self._configure(config) # Prepare the configuration.
if __debug__:
log.debug("Preparing WebCore application.")
if isfunction(root): # We need to armour against this turning into a bound method of the context.
root = staticmethod(root)
|
This construts a basic |
context = self.__context = Context(app=self, root=root)._promote('ApplicationContext')
|
These can't really be deferred to extensions themselves, for fairly obvious chicken/egg reasons. |
exts = context.extension = WebExtensions(context) # Load extension registry and prepare callbacks.
context.dispatch = WebDispatchers(context) # Load dispatch registry.
context.view = WebViews(context) # Load the view registry.
|
Execute extension startup callbacks; this is the appropriate time to attach descriptors to the context. |
for ext in exts.signal.start: ext(context)
|
At this point the context should have been populated with any descriptor protocol additions. Promote the
|
self.RequestContext = context._promote('RequestContext', instantiate=False)
|
Handle WSGI middleware wrapping by extensions and point our call at the result. |
app = self.application
for ext in exts.signal.middleware: app = ext(context, app)
self.__call__ = app
if __debug__: # Mostly useful for timing calculations.
log.debug("WebCore application prepared.")
|
Prepare the incoming configuration and ensure certain expected values are present. For example, this ensures BaseExtension is included in the extension list, and populates the logging config. |
def _configure(self, config):
config = config or dict()
|
We really need this to be there. |
if 'extensions' not in config: config['extensions'] = list()
|
Always make sure the BaseExtension is present since request/response objects are handy. |
config['extensions'].insert(0, BaseExtension())
|
Tests are skipped on these as we have no particular need to test Python's own logging mechanism. |
level = config.get('logging', {}).get('level', None)
if level: # pragma: no cover
logging.basicConfig(level=getattr(logging, level.upper()))
elif 'logging' in config: # pragma: no cover
logging.config.dictConfig(config['logging'])
return config
|
This is impractical to test due to the blocking nature of starting a web server interface. Individual adapters are hand-tested for basic operation prior to release. Initiate a web server service to serve this application. You can always use the Application instance as a bare WSGI application, of course. This method is provided as a convienence. Pass in the name of the service you wish to use, and any additional configuration options appropriate for that
service. Almost all services accept |
def serve(self, service='auto', **options): # pragma: no cover
service = load(service, 'web.server') # We don't bother with a full registry for these one-time lookups.
try:
service(self, **options)
except KeyboardInterrupt: # We catch this as SIG_TERM or ^C are basically the only ways to stop most servers.
pass
|
Notify extensions that the service has returned and we are exiting. |
for ext in self.__context.extension.signal.stop: ext(self.__context)
|
Extract usable args and kwargs from the given request object. Candidate for promotion to an extension callback. Two approaches come to mind:
We currently go for the latter without merging or PHP-ness. TODO: Investigate WebOb's request.urlvars and request.urlargs: https://github.com/Pylons/webob/blob/master/webob/request.py#L566-L646 |
def _extract_arguments(self, request):
args = list(request.remainder)
if args and args[0] == '': del args[0] # Trim the result of a leading `/`.
|
def process_kwargs(src):
kwargs = dict()
for name_, value in src.items():
if name_ in kwargs and not isinstance(kwargs[name_], list):
kwargs[name_] = [kwargs[name_], value]
elif name_ in kwargs:
kwargs[name_].append(value)
else:
kwargs[name_] = value
return kwargs
kwargs = process_kwargs(request.GET)
kwargs.update(process_kwargs(request.POST))
return args, kwargs
if __debug__: # This method only exists in development. Really; thus the use of name mangling.
|
|
def __validate_arguments(self, context, endpoint, bound, args, kwargs):
try:
if callable(endpoint) and not isroutine(endpoint):
|
|
Callable Instance |
getcallargs(endpoint.__call__, *args, **kwargs)
elif bound:
|
Instance Method |
getcallargs(endpoint, *args, **kwargs)
else:
|
Unbound Method or Function |
getcallargs(endpoint, context, *args, **kwargs)
except TypeError as e:
|
If the argument specification doesn't match, the handler can't process this request. This is one policy. Another possibility is more computationally expensive and would pass only valid arguments, silently dropping invalid ones. This can be implemented as a mutate handler. |
log.error(str(e), extra=dict(
request = id(context),
endpoint = safe_name(endpoint),
endpoint_args = args,
endpoint_kw = kwargs,
))
return HTTPNotFound("Incorrect endpoint arguments: " + str(e))
|
def _execute_endpoint(self, context, endpoint, signals):
if not callable(endpoint):
|
|
Endpoints don't have to be functions. They can instead point to what a function would return for view lookup. |
if __debug__:
log.debug("Static endpoint located.", extra=dict(
request = id(context),
endpoint = repr(endpoint),
))
|
Use the result directly, as if it were the result of calling a function or method. |
return endpoint
|
Our endpoint API states that endpoints recieve as positional parameters all remaining path elements, and as keyword arguments a combination of GET and POST variables with POST taking precedence. |
args, kwargs = self._extract_arguments(context.request)
|
Instance methods were handed the context at class construction time via dispatch.
The |
bound = not isroutine(endpoint) or (ismethod(endpoint) and getattr(endpoint, '__self__', None) is not None)
|
Allow argument transformation; args and kwargs can be manipulated inline. |
for ext in signals.mutate: ext(context, endpoint, args, kwargs)
if __debug__:
log.debug("Callable endpoint located.", extra=dict(
request = id(context),
endpoint = safe_name(endpoint),
endpoint_args = args,
endpoint_kw = kwargs
))
|
Make sure the handler can actually accept these arguments when running in development mode. Passing invalid arguments would 500 Internal Server Error on us due to the TypeError exception bubbling up. |
result = self.__validate_arguments(context, endpoint, bound, args, kwargs)
if result: # Bail if validation returned any truthy value.
return result
try:
|
Actually call the endpoint. |
if bound:
result = endpoint(*args, **kwargs)
else:
result = endpoint(context, *args, **kwargs)
except HTTPException as e:
result = e
|
Execute return value transformation callbacks. |
for ext in signals.transform: result = ext(context, endpoint, result)
return result
|
Process a single WSGI request/response cycle. This is the WSGI handler for WebCore. Depending on the presence of extensions providing WSGI middleware,
the Most apps won't utilize middleware, the extension interface is preferred for most operations in WebCore. They allow for code injection at various intermediary steps in the processing of a request and response. |
def application(self, environ, start_response):
context = environ['wc.context'] = self.RequestContext(environ=environ)
signals = context.extension.signal
|
Announce the start of a request cycle. This executes |
for ext in signals.pre: ext(context)
|
Identify the endpoint for this request. |
is_endpoint, handler = context.dispatch(context, context.root, context.environ['PATH_INFO'])
|
If no endpoint could be resolved, that's a 404. |
if not is_endpoint:
|
We can't just set the handler to the exception class or instance, because both are callable. An endpoint that returns a 404 Not Found error on dispatch failure. |
def handler(_ctx, *args, **kw): # Yes, this works. We're just assigning this code obj. to a label. :3
return HTTPNotFound("Dispatch failed." if __debug__ else None) # Not an exceptional event, so don't raise.
result = self._execute_endpoint(context, handler, signals) # Process the endpoint.
if __debug__:
log.debug("Result prepared, identifying view handler.", extra=dict(
request = id(context),
result = repr(result)
))
|
Identify a view capable of handling this result. |
for view in context.view(result):
if view(context, result): break
else:
|
We've run off the bottom of the list of possible views. |
raise TypeError("No view could be found to handle: " + repr(type(result)))
if __debug__:
log.debug("View identified, populating response.", extra=dict(
request = id(context),
view = repr(view),
))
for ext in signals.after: ext(context)
return context.response.conditional_response_app(environ, start_response)
|