Skip to content
This repository has been archived by the owner on Feb 25, 2022. It is now read-only.

Instantiate event hooks on each request/response #475

Merged
merged 3 commits into from
Apr 19, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apistar/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from apistar.server import App, ASyncApp, Component, Include, Route
from apistar.test import TestClient

__version__ = '0.5.3'
__version__ = '0.5.4'
__all__ = [
'App', 'ASyncApp', 'Client', 'Component', 'Document', 'Section', 'Link', 'Field',
'Route', 'Include', 'TestClient', 'http'
Expand Down
106 changes: 53 additions & 53 deletions apistar/server/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,20 @@ def __init__(self,
msg = 'components must be a list of instances of Component.'
assert all([isinstance(component, Component) for component in components]), msg
if event_hooks:
msg = 'event_hooks must be a list of instances, not classes.'
assert not any([isinstance(event_hook, type) for event_hook in event_hooks]), msg
msg = 'event_hooks must be a list.'
assert isinstance(event_hooks, (list, tuple)), msg

routes = routes + self.include_extra_routes(schema_url, static_url)
self.init_document(routes)
self.init_router(routes)
self.init_templates(template_dir)
self.init_staticfiles(static_url, static_dir)
self.init_injector(components)
self.init_hooks(event_hooks)
self.debug = False
self.event_hooks = event_hooks

# Ensure event hooks can all be instantiated.
self.get_event_hooks()

def include_extra_routes(self, schema_url=None, static_url=None):
extra_routes = []
Expand Down Expand Up @@ -101,30 +104,33 @@ def init_injector(self, components=None):
}
self.injector = Injector(components, initial_components)

def init_hooks(self, event_hooks=None):
if event_hooks is None:
event_hooks = []
def get_event_hooks(self):
event_hooks = []
for hook in self.event_hooks or []:
if isinstance(hook, type):
# New style usage, instantiate hooks on requests.
event_hooks.append(hook())
else:
# Old style usage, to be deprecated on the next version bump.
event_hooks.append(hook)

self.on_request_functions = [
on_request = [
hook.on_request for hook in event_hooks
if hasattr(hook, 'on_request')
]

self.on_response_functions = [self.render_response] + [
hook.on_response for hook in reversed(event_hooks)
if hasattr(hook, 'on_response')
] + [self.finalize_wsgi]

self.on_exception_functions = [self.exception_handler] + [
on_response = [
hook.on_response for hook in reversed(event_hooks)
if hasattr(hook, 'on_response')
] + [self.finalize_wsgi]
]

self.on_error_functions = [
on_error = [
hook.on_error for hook in reversed(event_hooks)
if hasattr(hook, 'on_error')
]

return on_request, on_response, on_error

def reverse_url(self, name: str, **params):
return self.router.reverse_url(name, **params)

Expand Down Expand Up @@ -178,6 +184,12 @@ def __call__(self, environ, start_response):
}
method = environ['REQUEST_METHOD'].upper()
path = environ['PATH_INFO']

if self.event_hooks is None:
on_request, on_response, on_error = [], [], []
else:
on_request, on_response, on_error = self.get_event_hooks()

try:
route, path_params = self.router.lookup(path, method)
state['route'] = route
Expand All @@ -186,22 +198,25 @@ def __call__(self, environ, start_response):
funcs = [route.handler]
else:
funcs = (
self.on_request_functions +
[route.handler] +
self.on_response_functions
on_request +
[route.handler, self.render_response] +
on_response +
[self.finalize_wsgi]
)
return self.injector.run(funcs, state)
except Exception as exc:
try:
state['exc'] = exc
funcs = self.on_exception_functions
funcs = (
[self.exception_handler] +
on_response +
[self.finalize_wsgi]
)
return self.injector.run(funcs, state)
except Exception as inner_exc:
try:
state['exc'] = inner_exc
funcs = self.on_error_functions
if funcs:
self.injector.run(funcs, state)
self.injector.run(on_error, state)
finally:
funcs = [self.error_handler, self.finalize_wsgi]
return self.injector.run(funcs, state)
Expand Down Expand Up @@ -241,30 +256,6 @@ def init_injector(self, components=None):
}
self.injector = ASyncInjector(components, initial_components)

def init_hooks(self, event_hooks=None):
if event_hooks is None:
event_hooks = []

self.on_request_functions = [
hook.on_request for hook in event_hooks
if hasattr(hook, 'on_request')
]

self.on_response_functions = [self.render_response] + [
hook.on_response for hook in reversed(event_hooks)
if hasattr(hook, 'on_response')
] + [self.finalize_asgi]

self.on_exception_functions = [self.exception_handler] + [
hook.on_response for hook in reversed(event_hooks)
if hasattr(hook, 'on_response')
] + [self.finalize_asgi]

self.on_error_functions = [
hook.on_error for hook in reversed(event_hooks)
if hasattr(hook, 'on_error')
]

def init_staticfiles(self, static_url: str, static_dir: str=None):
if not static_dir:
self.statics = None
Expand All @@ -284,6 +275,12 @@ async def asgi_callable(receive, send):
}
method = scope['method']
path = scope['path']

if self.event_hooks is None:
on_request, on_response, on_error = [], [], []
else:
on_request, on_response, on_error = self.get_event_hooks()

try:
route, path_params = self.router.lookup(path, method)
state['route'] = route
Expand All @@ -292,22 +289,25 @@ async def asgi_callable(receive, send):
funcs = [route.handler]
else:
funcs = (
self.on_request_functions +
[route.handler] +
self.on_response_functions
on_request +
[route.handler, self.render_response] +
on_response +
[self.finalize_asgi]
)
await self.injector.run_async(funcs, state)
except Exception as exc:
try:
state['exc'] = exc
funcs = self.on_exception_functions
funcs = (
[self.exception_handler] +
on_response +
[self.finalize_asgi]
)
await self.injector.run_async(funcs, state)
except Exception as inner_exc:
try:
state['exc'] = inner_exc
funcs = self.on_error_functions
if funcs:
await self.injector.run(funcs, state)
await self.injector.run(on_error, state)
finally:
funcs = [self.error_handler, self.finalize_asgi]
await self.injector.run(funcs, state)
Expand Down
4 changes: 4 additions & 0 deletions apistar/server/injector.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ def run(self, funcs, state):
try:
steps = self.resolver_cache[funcs]
except KeyError:
if not funcs:
return
steps = self.resolve_functions(funcs)
self.resolver_cache[funcs] = steps

Expand All @@ -116,6 +118,8 @@ async def run_async(self, funcs, state):
try:
steps = self.resolver_cache[funcs]
except KeyError:
if not funcs:
return
steps = self.resolve_functions(funcs)
self.resolver_cache[funcs] = steps

Expand Down
49 changes: 37 additions & 12 deletions docs/api-guide/event-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ class CustomHeadersHook():
def on_response(self, response: http.Response):
response.headers['x-custom'] = 'Ran on_response()'

event_hooks = [CustomHeadersHook()]
event_hooks = [CustomHeadersHook]

app = App(routes=routes, event_hooks=event_hooks)
```
Expand All @@ -36,24 +36,49 @@ For these cases, any installed `on_error` event hooks will be run. This event ho
allows you to monitor or log exceptions.

It's recommend that use of `on_error` event hooks should be kept to a minimum,
dealing only with whatever error monitoring is required. Any exceptions thrown
in one of these handlers will result in the raw exception being raised to the
webserver with no further error handling taking place.
dealing only with whatever error monitoring is required.

An example of error handling:
## Using event hooks

Event hooks can pull in components such as the request, the returned response,
or the exception that resulted in a response. For example:

```python
class ErrorHandlingHook():
class ErrorHandlingHook:
def on_response(self, response: http.Response, exc: Exception):
if exc is None:
print("Response OK")
print("Handler returned a response")
else:
print("Handled exception")
print("Exception handler returned a response")

def on_error(self, response: http.Response):
print("Unhandled error")
print("An unhandled error was raised")

event_hooks = [ErrorHandlingHook()]

app = App(routes=routes, event_hooks=event_hooks)
app = App(routes=routes, event_hooks=[ErrorHandlingHook])
```

Event hooks are instantiated at the start of every request/response cycle, and can
store state between the request and response events.

```python
class TimingHook:
def on_request(self):
self.started = time.time()

def on_response(self):
duration = time.time() - self.started
print("Response returned in %0.6f seconds." % duration)


app = App(routes=routes, event_hooks=[TimingHook])
```

## Ordering of event hooks

The `on_request` hooks are run in the order that their classes are included.

The `on_response` and `on_exception` hooks are run in the reverse order that their classes are included.

This behaviour ensures that event hooks run in a similar manner to stack-based middleware,
with the each event hook wrapping everything that comes after it.
4 changes: 4 additions & 0 deletions docs/topics/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Version 0.5.x

### 0.5.4

* Event hooks instantiated on each request/response cycle.

### 0.5.3

* Support multipart and urlencoded content in `RequestData`.
Expand Down
10 changes: 6 additions & 4 deletions tests/test_event_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@


class CustomResponseHeader():
def on_request(self):
self.message = 'Ran hooks'

def on_response(self, response: http.Response):
response.headers['Custom'] = 'Ran on_response'
return response
response.headers['Custom'] = self.message

def on_error(self):
global ON_ERROR
Expand All @@ -28,7 +30,7 @@ def error():
Route('/error', method='GET', handler=error),
]

event_hooks = [CustomResponseHeader()]
event_hooks = [CustomResponseHeader]

app = App(routes=routes, event_hooks=event_hooks)

Expand All @@ -38,7 +40,7 @@ def error():
def test_on_response():
response = client.get('/hello')
assert response.status_code == 200
assert response.headers['Custom'] == 'Ran on_response'
assert response.headers['Custom'] == 'Ran hooks'


def test_on_error():
Expand Down