Compare commits
No commits in common. "master" and "0.3" have entirely different histories.
|
@ -1,5 +0,0 @@
|
||||||
/build/
|
|
||||||
/dist/
|
|
||||||
*.egg-info/
|
|
||||||
__pycache__/
|
|
||||||
*.py[co]
|
|
|
@ -49,7 +49,7 @@ copyright = u'2011-2015 Dustin C. Hatch'
|
||||||
# built documents.
|
# built documents.
|
||||||
#
|
#
|
||||||
# The short X.Y version.
|
# The short X.Y version.
|
||||||
version = '1.0'
|
version = '0.2'
|
||||||
# The full version, including alpha/beta/rc tags.
|
# The full version, including alpha/beta/rc tags.
|
||||||
release = version
|
release = version
|
||||||
|
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
==========
|
|
||||||
milla.vary
|
|
||||||
==========
|
|
||||||
|
|
||||||
.. automodule:: milla.vary
|
|
||||||
:members:
|
|
2
setup.py
2
setup.py
|
@ -15,7 +15,7 @@ if sys.version_info < (2, 7):
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='Milla',
|
name='Milla',
|
||||||
version='1.0',
|
version='0.3',
|
||||||
description='Lightweight WSGI framework for web applications',
|
description='Lightweight WSGI framework for web applications',
|
||||||
long_description='''\
|
long_description='''\
|
||||||
Milla is a simple WSGI framework for Python web applications. It is mostly
|
Milla is a simple WSGI framework for Python web applications. It is mostly
|
||||||
|
|
130
src/milla/app.py
130
src/milla/app.py
|
@ -1,4 +1,4 @@
|
||||||
# Copyright 2011, 2012, 2014-2016 Dustin C. Hatch
|
# Copyright 2011, 2012, 2014, 2015 Dustin C. Hatch
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
# you may not use this file except in compliance with the License.
|
# you may not use this file except in compliance with the License.
|
||||||
|
@ -13,57 +13,48 @@
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
'''Module milla.app
|
'''Module milla.app
|
||||||
|
|
||||||
The :py:class:`BaseApplication` class is the core of the Milla
|
Please give me a docstring!
|
||||||
framework. This class implements a WSGI application that dispatches
|
|
||||||
requests to callables based on their URL-path.
|
|
||||||
|
|
||||||
Most applications can use :py:class:`Application` directly, without
|
|
||||||
creating a sub-class. For advanced use, applications can define a
|
|
||||||
sub-class of :py:class:`BaseApplication` and customize the behavior of
|
|
||||||
the framework.
|
|
||||||
|
|
||||||
:Created: Mar 26, 2011
|
:Created: Mar 26, 2011
|
||||||
:Author: dustin
|
:Author: dustin
|
||||||
|
:Updated: $Date$
|
||||||
|
:Updater: $Author$
|
||||||
'''
|
'''
|
||||||
|
|
||||||
from milla import util
|
|
||||||
from milla.controllers import FaviconController
|
from milla.controllers import FaviconController
|
||||||
|
from milla.util import asbool
|
||||||
from webob.exc import HTTPNotFound, WSGIHTTPException, HTTPMethodNotAllowed
|
from webob.exc import HTTPNotFound, WSGIHTTPException, HTTPMethodNotAllowed
|
||||||
import milla.dispatch.traversal
|
import milla.dispatch.traversal
|
||||||
import os
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = ['Application']
|
||||||
'Application',
|
|
||||||
'BaseApplication',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
class Application(object):
|
||||||
# In Python 2, it could be a str or a unicode object
|
'''Represents a Milla web application
|
||||||
_string = basestring
|
|
||||||
except NameError:
|
|
||||||
# In Python 3, we are only interested in str objects
|
|
||||||
_string = str
|
|
||||||
|
|
||||||
|
Constructing an ``Application`` instance needs a dispatcher, or
|
||||||
|
alternatively, a root object that will be passed to a new
|
||||||
|
:py:class:`milla.dispatch.traversal.Traverser`.
|
||||||
|
|
||||||
class BaseApplication(object):
|
:param obj: An object implementing the dispatcher protocol, or an
|
||||||
'''Base class for Milla applications
|
object to be used as the root for a Traverser
|
||||||
|
|
||||||
This class can be used by applications that need to customize the
|
``Application`` instances are WSGI applications.
|
||||||
behavior of the framework. In most cases, :py:class:`Application`
|
|
||||||
instances can be created directly and a sublcass is not necessary.
|
.. py:attribute:: config
|
||||||
|
|
||||||
|
A mapping of configuration settings. For each request, the
|
||||||
|
configuration is copied and assigned to ``request.config``.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
DEFAULT_CONFIG = {}
|
def __init__(self, obj):
|
||||||
|
if not hasattr(obj, 'resolve'):
|
||||||
#: Enable HTTP method emulation for POST requests?
|
# Object is not a dispatcher, but the root object for traversal
|
||||||
post_method_emulation = True
|
obj = milla.dispatch.traversal.Traverser(obj)
|
||||||
|
self.dispatcher = obj
|
||||||
def __init__(self):
|
self.config = {'milla.favicon': True}
|
||||||
self.config = self.DEFAULT_CONFIG.copy()
|
|
||||||
self.dispatcher = None
|
|
||||||
|
|
||||||
def __call__(self, environ, start_response):
|
def __call__(self, environ, start_response):
|
||||||
start_response = StartResponseWrapper(start_response)
|
start_response = StartResponseWrapper(start_response)
|
||||||
|
@ -75,11 +66,6 @@ class BaseApplication(object):
|
||||||
allowed_methods = self._find_attr(func, 'allowed_methods')
|
allowed_methods = self._find_attr(func, 'allowed_methods')
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
allowed_methods = milla.DEFAULT_METHODS
|
allowed_methods = milla.DEFAULT_METHODS
|
||||||
if request.method == 'HEAD':
|
|
||||||
request.real_method = 'HEAD'
|
|
||||||
request.method = 'GET'
|
|
||||||
else:
|
|
||||||
request.real_method = request.method
|
|
||||||
if request.method not in allowed_methods:
|
if request.method not in allowed_methods:
|
||||||
allow_header = {'Allow': ', '.join(allowed_methods)}
|
allow_header = {'Allow': ', '.join(allowed_methods)}
|
||||||
if request.method == 'OPTIONS':
|
if request.method == 'OPTIONS':
|
||||||
|
@ -101,10 +87,20 @@ class BaseApplication(object):
|
||||||
|
|
||||||
# The callable might have returned just a string, which is OK,
|
# The callable might have returned just a string, which is OK,
|
||||||
# but we need to wrap it in a Response object
|
# but we need to wrap it in a Response object
|
||||||
|
try:
|
||||||
|
# In Python 2, it could be a str or a unicode object
|
||||||
|
_string = basestring
|
||||||
|
except NameError:
|
||||||
|
# In Python 3, we are only interested in str objects
|
||||||
|
_string = str
|
||||||
if isinstance(response, _string) or not response:
|
if isinstance(response, _string) or not response:
|
||||||
response = request.ResponseClass(response)
|
response = request.ResponseClass(response)
|
||||||
|
|
||||||
return response(environ, start_response)
|
if environ['REQUEST_METHOD'] == 'HEAD':
|
||||||
|
start_response(response.status, response.headerlist)
|
||||||
|
return ''
|
||||||
|
else:
|
||||||
|
return response(environ, start_response)
|
||||||
|
|
||||||
def _call_after(self, func):
|
def _call_after(self, func):
|
||||||
try:
|
try:
|
||||||
|
@ -131,23 +127,6 @@ class BaseApplication(object):
|
||||||
return self._find_attr(obj.func, attr)
|
return self._find_attr(obj.func, attr)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def update_config(self, filename):
|
|
||||||
'''Update application configuration from a file
|
|
||||||
|
|
||||||
:param filename: Path to configuration file
|
|
||||||
|
|
||||||
This method will update the application configuration using
|
|
||||||
the values found in the specified configuration file. If the
|
|
||||||
specified file does not exist or is not accessible, no
|
|
||||||
changes will be made.
|
|
||||||
|
|
||||||
The configuration file will be read using
|
|
||||||
:py:func:`milla.util.read_config`.
|
|
||||||
'''
|
|
||||||
|
|
||||||
if filename and os.access(filename, os.R_OK):
|
|
||||||
self.config.update(util.read_config(filename))
|
|
||||||
|
|
||||||
def make_request(self, environ):
|
def make_request(self, environ):
|
||||||
'''Create a :py:class:`~milla.Request` from a WSGI environment
|
'''Create a :py:class:`~milla.Request` from a WSGI environment
|
||||||
|
|
||||||
|
@ -160,9 +139,8 @@ class BaseApplication(object):
|
||||||
# Sometimes, hacky applications will try to "emulate" some HTTP
|
# Sometimes, hacky applications will try to "emulate" some HTTP
|
||||||
# methods like PUT or DELETE by specifying an _method parameter
|
# methods like PUT or DELETE by specifying an _method parameter
|
||||||
# in a POST request.
|
# in a POST request.
|
||||||
if self.post_method_emulation:
|
if request.method == 'POST' and '_method' in request.POST:
|
||||||
if request.method == 'POST' and '_method' in request.POST:
|
request.method = request.POST.pop('_method')
|
||||||
request.method = request.POST.pop('_method')
|
|
||||||
return request
|
return request
|
||||||
|
|
||||||
def resolve_path(self, path_info):
|
def resolve_path(self, path_info):
|
||||||
|
@ -184,7 +162,7 @@ class BaseApplication(object):
|
||||||
return self.dispatcher.resolve(path_info)
|
return self.dispatcher.resolve(path_info)
|
||||||
except milla.dispatch.UnresolvedPath:
|
except milla.dispatch.UnresolvedPath:
|
||||||
if (path_info == '/favicon.ico' and
|
if (path_info == '/favicon.ico' and
|
||||||
util.asbool(self.config.get('milla.favicon'))):
|
asbool(self.config.get('milla.favicon'))):
|
||||||
return FaviconController()
|
return FaviconController()
|
||||||
else:
|
else:
|
||||||
return path_not_found
|
return path_not_found
|
||||||
|
@ -204,36 +182,6 @@ class BaseApplication(object):
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
class Application(BaseApplication):
|
|
||||||
'''Represents a Milla web application
|
|
||||||
|
|
||||||
Constructing an ``Application`` instance needs a dispatcher, or
|
|
||||||
alternatively, a root object that will be passed to a new
|
|
||||||
:py:class:`milla.dispatch.traversal.Traverser`.
|
|
||||||
|
|
||||||
:param obj: An object implementing the dispatcher protocol, or an
|
|
||||||
object to be used as the root for a Traverser
|
|
||||||
|
|
||||||
``Application`` instances are WSGI applications.
|
|
||||||
|
|
||||||
.. py:attribute:: config
|
|
||||||
|
|
||||||
A mapping of configuration settings. For each request, the
|
|
||||||
configuration is copied and assigned to ``request.config``.
|
|
||||||
'''
|
|
||||||
|
|
||||||
DEFAULT_CONFIG = {
|
|
||||||
'milla.favicon': True,
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, obj):
|
|
||||||
super(Application, self).__init__()
|
|
||||||
if not hasattr(obj, 'resolve'):
|
|
||||||
# Object is not a dispatcher, but the root object for traversal
|
|
||||||
obj = milla.dispatch.traversal.Traverser(obj)
|
|
||||||
self.dispatcher = obj
|
|
||||||
|
|
||||||
|
|
||||||
class StartResponseWrapper():
|
class StartResponseWrapper():
|
||||||
|
|
||||||
def __init__(self, start_response):
|
def __init__(self, start_response):
|
||||||
|
|
|
@ -20,18 +20,13 @@
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from milla.auth import permissions
|
from milla.auth import permissions
|
||||||
import milla.auth
|
import milla.auth
|
||||||
import warnings
|
|
||||||
try:
|
try:
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pkg_resources = None
|
pkg_resources = None
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = ['auth_required', 'require_perms']
|
||||||
'auth_required',
|
|
||||||
'require_perms',
|
|
||||||
'validate_request',
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
VALIDATOR_EP_GROUP = 'milla.request_validator'
|
VALIDATOR_EP_GROUP = 'milla.request_validator'
|
||||||
|
@ -47,33 +42,6 @@ def _find_request(*args, **kwargs):
|
||||||
|
|
||||||
|
|
||||||
def _validate_request(func, requirement, *args, **kwargs):
|
def _validate_request(func, requirement, *args, **kwargs):
|
||||||
warnings.warn(
|
|
||||||
'_validate_request is deprecated; use validate_request instead',
|
|
||||||
DeprecationWarning,
|
|
||||||
stacklevel=2,
|
|
||||||
)
|
|
||||||
validate_request(func, requirement, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def validate_request(func, requirement, *args, **kwargs):
|
|
||||||
'''Validate a request meets a given requirement
|
|
||||||
|
|
||||||
:param func: Decorated callable
|
|
||||||
:param requirement: A requirement that the request must meet in
|
|
||||||
order to be considered valid, as specified by the request
|
|
||||||
validator used by the application. This is usally a sub-class of
|
|
||||||
:py:class:`~milla.auth.permissions.PermissionRequirement`, or
|
|
||||||
some other class that has a ``check`` method that accepts a
|
|
||||||
:py:class:`~milla.Request` object as its only argument.
|
|
||||||
:param args: Positional arguments to pass through to the decorated
|
|
||||||
callable
|
|
||||||
:param kwargs: Keyword arguments to pass through to the decorated
|
|
||||||
callable
|
|
||||||
|
|
||||||
This is a helper function used by :py:func:`auth_required` and
|
|
||||||
:py:func:`require_perms` that can be used by other request
|
|
||||||
decorators as well.
|
|
||||||
'''
|
|
||||||
request = _find_request(*args, **kwargs)
|
request = _find_request(*args, **kwargs)
|
||||||
|
|
||||||
rv = request.config.get('request_validator', 'default')
|
rv = request.config.get('request_validator', 'default')
|
||||||
|
@ -135,7 +103,7 @@ def auth_required(func):
|
||||||
|
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
return validate_request(func, None, *args, **kwargs)
|
return _validate_request(func, None, *args, **kwargs)
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
@ -186,5 +154,5 @@ class require_perms(object):
|
||||||
def __call__(self, func):
|
def __call__(self, func):
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
return validate_request(func, self.requirement, *args, **kwargs)
|
return _validate_request(func, self.requirement, *args, **kwargs)
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
|
@ -104,8 +104,15 @@ class HTTPVerbController(Controller):
|
||||||
def GET(self, request):
|
def GET(self, request):
|
||||||
return 'Hello, world!'
|
return 'Hello, world!'
|
||||||
|
|
||||||
|
HEAD = GET
|
||||||
|
|
||||||
def POST(self, request):
|
def POST(self, request):
|
||||||
return 'Thanks!'
|
return 'Thanks!'
|
||||||
|
|
||||||
|
This example also allows ``HEAD`` requests, by processing them as
|
||||||
|
``GET`` requests. *Milla* handles this correctly, as it does not
|
||||||
|
send a response body for ``HEAD`` requests, even if the controller
|
||||||
|
callable returns one.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
def __call__(self, request, *args, **kwargs):
|
def __call__(self, request, *args, **kwargs):
|
||||||
|
|
|
@ -1,181 +0,0 @@
|
||||||
# Copyright 2016 Dustin C. Hatch
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
'''Multi-format response handling
|
|
||||||
|
|
||||||
:Created: Jul 1, 2016
|
|
||||||
:Author: dustin
|
|
||||||
'''
|
|
||||||
import milla
|
|
||||||
import collections
|
|
||||||
import inspect
|
|
||||||
import functools
|
|
||||||
|
|
||||||
|
|
||||||
class renders(object):
|
|
||||||
'''Mark a method as a renderer for one or more media types
|
|
||||||
|
|
||||||
:param content_types: Internet media types supported by the
|
|
||||||
renderer
|
|
||||||
'''
|
|
||||||
|
|
||||||
def __init__(self, *content_types):
|
|
||||||
self.content_types = content_types
|
|
||||||
|
|
||||||
def __call__(self, func):
|
|
||||||
func.renders = self.content_types
|
|
||||||
return func
|
|
||||||
|
|
||||||
|
|
||||||
def default_renderer(func):
|
|
||||||
'''Mark a :py:class:`VariedResponseMixin` renderer as default'''
|
|
||||||
|
|
||||||
func.default_renderer = True
|
|
||||||
return func
|
|
||||||
|
|
||||||
|
|
||||||
class VariedResponseMeta(type):
|
|
||||||
|
|
||||||
def __new__(mcs, name, bases, attrs):
|
|
||||||
cls = type.__new__(mcs, name, bases, attrs)
|
|
||||||
cls.renderers = {}
|
|
||||||
cls.default_type = None
|
|
||||||
for attr in attrs.values():
|
|
||||||
if not isinstance(attr, collections.Callable):
|
|
||||||
continue
|
|
||||||
if hasattr(attr, 'renders'):
|
|
||||||
for content_type in attr.renders:
|
|
||||||
cls.renderers[content_type] = attr
|
|
||||||
if getattr(attr, 'default_renderer', False):
|
|
||||||
cls.default_type = attr.renders[0]
|
|
||||||
return cls
|
|
||||||
|
|
||||||
|
|
||||||
_VariedResponseBase = VariedResponseMeta(
|
|
||||||
'_VariedResponseBase', (milla.Response,), {})
|
|
||||||
|
|
||||||
|
|
||||||
class VariedResponseBase(_VariedResponseBase):
|
|
||||||
'''Base class for responses with variable representations
|
|
||||||
|
|
||||||
In many cases, a a response can be represented in more than one
|
|
||||||
format (e.g. HTML, JSON, XML, etc.). This class can be used to
|
|
||||||
present the correct format based on the value of the ``Accept``
|
|
||||||
header in the request.
|
|
||||||
|
|
||||||
To use this class, create a subclass with a method to render each
|
|
||||||
supported representation format. The render methods must have
|
|
||||||
a ``renders`` attribute that contains a sequence of Internet media
|
|
||||||
(MIME) types the renderer is capable of producing. The
|
|
||||||
:py:func:`renders` decorator can be used to set this attribute.
|
|
||||||
|
|
||||||
Each renderer must take at least one argument, which is the context
|
|
||||||
data passed to :py:meth:`set_payload`. Additional arguments are
|
|
||||||
allowed, but they must be passed through :py:meth:`set_payload` as
|
|
||||||
keyword arguments.
|
|
||||||
|
|
||||||
If the ``Accept`` header of the request does not specify a media
|
|
||||||
type supported by any renderer, :py:exc:`~webob.exc.NotAcceptable`
|
|
||||||
will be raised. To avoid this, select a renderer as the "default"
|
|
||||||
by setting its `default_renderer` attribute to ``True`` (e.g. with
|
|
||||||
:py:func:`default_renderer`). This renderer will be used for all
|
|
||||||
requests unless a more appropriate renderer is available.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
class VariedResponse(Response, VariedResponse):
|
|
||||||
|
|
||||||
@default_renderer
|
|
||||||
@renders('text/html')
|
|
||||||
def render_html(self, context, template):
|
|
||||||
self.body = render_jinja_template(template, context)
|
|
||||||
|
|
||||||
@renders('application/json')
|
|
||||||
def render_json(self, context):
|
|
||||||
self.body = json.dumps(context)
|
|
||||||
|
|
||||||
The custom response class can be set as the default by extending the
|
|
||||||
:py:meth:`~milla.app.BaseApplication.make_request` method. For
|
|
||||||
example:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
class Application(milla.app.Application):
|
|
||||||
|
|
||||||
def make_request(self, environ):
|
|
||||||
request = super(Application, self).make_request(environ)
|
|
||||||
request.ResponseClass = VariedResponse.for_request(request)
|
|
||||||
return request
|
|
||||||
'''
|
|
||||||
|
|
||||||
def __init__(self, request, *args, **kwargs):
|
|
||||||
super(VariedResponseBase, self).__init__(*args, **kwargs)
|
|
||||||
self.request = request
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def for_request(cls, request):
|
|
||||||
return functools.partial(cls, request)
|
|
||||||
|
|
||||||
def set_payload(self, context, **kwargs):
|
|
||||||
'''Set the response payload using the most appropriate renderer
|
|
||||||
|
|
||||||
:param context: The data to pass to the renderer
|
|
||||||
:param kwargs: Additional keyword arguments to pass to the
|
|
||||||
renderer
|
|
||||||
|
|
||||||
This method will determine the most appropriate representation
|
|
||||||
format for the response based on the ``Accept`` header in the
|
|
||||||
request and delegate to the method that can render that format.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
def controller(request):
|
|
||||||
response = VariedResponse.for_request(request)
|
|
||||||
response.set_payload(
|
|
||||||
{'hello': 'world'},
|
|
||||||
template='hello.html',
|
|
||||||
)
|
|
||||||
return response
|
|
||||||
|
|
||||||
In this example, the context is ``{'hello': 'world'}``. This
|
|
||||||
will be passed as the first argument to any renderer. If the
|
|
||||||
selected renderer accepts a ``template`` argument,
|
|
||||||
``'hello.html'`` will be passed as well.
|
|
||||||
'''
|
|
||||||
|
|
||||||
if not self.vary:
|
|
||||||
self.vary = ['Accept']
|
|
||||||
elif 'accept' not in (v.lower() for v in self.vary):
|
|
||||||
self.vary = self.vary + ('Accept',)
|
|
||||||
|
|
||||||
offer_types = self.renderers.keys()
|
|
||||||
match = self.request.accept.best_match(offer_types, self.default_type)
|
|
||||||
if match is None:
|
|
||||||
raise milla.HTTPNotAcceptable
|
|
||||||
renderer = self.renderers[match]
|
|
||||||
kwargs = _filter_kwargs(renderer, kwargs)
|
|
||||||
renderer(self, context, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def _filter_kwargs(func, kwargs):
|
|
||||||
if hasattr(inspect, 'signature'): # Python 3
|
|
||||||
sig = inspect.signature(func)
|
|
||||||
accepted = (p.name for p in sig.parameters.values()
|
|
||||||
if p.kind == inspect.Parameter.POSITIONAL_OR_KEYWORD)
|
|
||||||
else: # Python 2
|
|
||||||
accepted = inspect.getargspec(func)[0]
|
|
||||||
return dict((k, kwargs[k]) for k in accepted if k in kwargs)
|
|
|
@ -1,220 +0,0 @@
|
||||||
from milla import vary
|
|
||||||
import collections
|
|
||||||
import functools
|
|
||||||
import milla
|
|
||||||
import nose.tools
|
|
||||||
import sys
|
|
||||||
try:
|
|
||||||
from unittest import mock
|
|
||||||
except ImportError:
|
|
||||||
import mock
|
|
||||||
|
|
||||||
|
|
||||||
PY2 = sys.version_info[0] == 2
|
|
||||||
PY3 = sys.version_info[0] == 3
|
|
||||||
|
|
||||||
def test_renders_decorator():
|
|
||||||
'''renders modifies and returns the decorated object'''
|
|
||||||
|
|
||||||
def func():
|
|
||||||
pass
|
|
||||||
|
|
||||||
func2 = vary.renders('text/html')(func)
|
|
||||||
|
|
||||||
assert func2 is func
|
|
||||||
assert 'text/html' in func.renders
|
|
||||||
|
|
||||||
|
|
||||||
def test_default_renderer_decorator():
|
|
||||||
'''default_renderer modifies and returns the decorated object'''
|
|
||||||
|
|
||||||
def func():
|
|
||||||
pass
|
|
||||||
|
|
||||||
func2 = vary.default_renderer(func)
|
|
||||||
|
|
||||||
assert func2 is func
|
|
||||||
assert func.default_renderer
|
|
||||||
|
|
||||||
|
|
||||||
def test_variedresponsemeta_renderers():
|
|
||||||
'''VariedResponseMeta adds renderers dict to implementation classes'''
|
|
||||||
|
|
||||||
TestClass = vary.VariedResponseMeta('TestClass', (object,), {})
|
|
||||||
|
|
||||||
assert isinstance(TestClass.renderers, collections.Mapping)
|
|
||||||
|
|
||||||
|
|
||||||
def test_variedresponsemeta_default_renderer():
|
|
||||||
'''VariedResponseMeta adds default_type to implementation classes'''
|
|
||||||
|
|
||||||
TestClass = vary.VariedResponseMeta('TestClass', (object,), {})
|
|
||||||
|
|
||||||
assert TestClass.default_type is None
|
|
||||||
|
|
||||||
|
|
||||||
def test_variedresponsemeta_renders():
|
|
||||||
'''Test VariedResponseMeta implementation class renderers population'''
|
|
||||||
|
|
||||||
VariedResponse = vary.VariedResponseMeta('VariedResponse', (object,), {})
|
|
||||||
|
|
||||||
class TestClass(VariedResponse):
|
|
||||||
|
|
||||||
@vary.renders('text/html')
|
|
||||||
def render_html(self, context):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
if PY2:
|
|
||||||
want_func = TestClass.render_html.__func__
|
|
||||||
else:
|
|
||||||
want_func = TestClass.render_html
|
|
||||||
assert TestClass.renderers['text/html'] is want_func
|
|
||||||
|
|
||||||
|
|
||||||
def test_variedresponsemeta_default_renderer():
|
|
||||||
'''Test VariedResponseMeta implementation class sets default type'''
|
|
||||||
|
|
||||||
VariedResponse = vary.VariedResponseMeta('VariedResponse', (object,), {})
|
|
||||||
|
|
||||||
class TestClass(VariedResponse):
|
|
||||||
|
|
||||||
@vary.default_renderer
|
|
||||||
@vary.renders('text/html')
|
|
||||||
def render_html(self, context):
|
|
||||||
pass
|
|
||||||
|
|
||||||
assert TestClass.default_type == 'text/html'
|
|
||||||
|
|
||||||
|
|
||||||
def test_variedresponsebase_init_super():
|
|
||||||
'''VariedResponseBase.__init__ calls Response.__init__'''
|
|
||||||
|
|
||||||
request = milla.Request.blank('http://localhost/')
|
|
||||||
with mock.patch.object(milla.Response, '__init__') as init:
|
|
||||||
vary.VariedResponseBase(request, 'a', b='c')
|
|
||||||
|
|
||||||
assert init.called_with('a', b='c')
|
|
||||||
|
|
||||||
|
|
||||||
def test_variedresponsebase_for_request():
|
|
||||||
'''VariedResponseBase.for_request returns a partial'''
|
|
||||||
|
|
||||||
request = milla.Request.blank('http://localhost/')
|
|
||||||
klass = vary.VariedResponseBase.for_request(request)
|
|
||||||
assert isinstance(klass, functools.partial), klass
|
|
||||||
|
|
||||||
|
|
||||||
def test_variedresponsebase_set_payload_set_vary():
|
|
||||||
'''VariedResponseBase.set_payload sets the Vary response header'''
|
|
||||||
|
|
||||||
def render_html(response, context):
|
|
||||||
pass
|
|
||||||
|
|
||||||
request = milla.Request.blank('http://localhost/')
|
|
||||||
response = vary.VariedResponseBase(request)
|
|
||||||
response.renderers['text/html'] = render_html
|
|
||||||
response.set_payload({})
|
|
||||||
|
|
||||||
assert response.headers['Vary'] == 'Accept'
|
|
||||||
|
|
||||||
|
|
||||||
def test_variedresponsebase_set_payload_add_vary():
|
|
||||||
'''VariedResponseBase.set_payload adds to the Vary response header'''
|
|
||||||
|
|
||||||
def render_html(response, context):
|
|
||||||
pass
|
|
||||||
|
|
||||||
request = milla.Request.blank('http://localhost/')
|
|
||||||
response = vary.VariedResponseBase(request)
|
|
||||||
response.renderers['text/html'] = render_html
|
|
||||||
response.vary = ('Cookie',)
|
|
||||||
response.set_payload({})
|
|
||||||
|
|
||||||
assert response.headers['Vary'] == 'Cookie, Accept'
|
|
||||||
|
|
||||||
|
|
||||||
def test_variedresponsebase_set_payload_match():
|
|
||||||
'''VariedResponseBase.set_payload calls the matching renderer'''
|
|
||||||
|
|
||||||
class State(object):
|
|
||||||
html_called = False
|
|
||||||
json_called = False
|
|
||||||
|
|
||||||
def render_html(response, state):
|
|
||||||
state.html_called = True
|
|
||||||
|
|
||||||
render_html.renders = ('text/html',)
|
|
||||||
|
|
||||||
def render_json(response, state):
|
|
||||||
state.json_called = True
|
|
||||||
|
|
||||||
render_json.renders = ('application/json',)
|
|
||||||
|
|
||||||
def check_type(accept, attr):
|
|
||||||
request = milla.Request.blank('http://localhost/')
|
|
||||||
request.accept = accept
|
|
||||||
response = vary.VariedResponseBase(request)
|
|
||||||
response.renderers = {
|
|
||||||
'text/html': render_html,
|
|
||||||
'application/json': render_json,
|
|
||||||
}
|
|
||||||
state = State()
|
|
||||||
response.set_payload(state)
|
|
||||||
|
|
||||||
assert getattr(state, attr)
|
|
||||||
|
|
||||||
tests = [
|
|
||||||
('text/html', 'html_called'),
|
|
||||||
('application/json', 'json_called'),
|
|
||||||
]
|
|
||||||
for accept, attr in tests:
|
|
||||||
yield check_type, accept, attr
|
|
||||||
|
|
||||||
|
|
||||||
@nose.tools.raises(milla.HTTPNotAcceptable)
|
|
||||||
def test_variedresponsebase_set_payload_not_acceptable():
|
|
||||||
'''VariedResponseBase.set_payload raises HTTPNotAcceptable'''
|
|
||||||
|
|
||||||
def render_html(response, context):
|
|
||||||
pass
|
|
||||||
|
|
||||||
request = milla.Request.blank('http://localhost/')
|
|
||||||
request.accept = 'text/plain'
|
|
||||||
response = vary.VariedResponseBase(request)
|
|
||||||
response.renderers['text/html'] = render_html
|
|
||||||
response.set_payload({})
|
|
||||||
|
|
||||||
|
|
||||||
def test_variedresponsebase_set_payload_default_format():
|
|
||||||
'''VariedResponseBase.set_payload falls back to the default renderer'''
|
|
||||||
|
|
||||||
class State(object):
|
|
||||||
called = False
|
|
||||||
|
|
||||||
state = State()
|
|
||||||
|
|
||||||
def render_html(response, context):
|
|
||||||
state.called = True
|
|
||||||
|
|
||||||
request = milla.Request.blank('http://localhost/')
|
|
||||||
request.accept = 'text/plain'
|
|
||||||
response = vary.VariedResponseBase(request)
|
|
||||||
response.renderers['text/html'] = render_html
|
|
||||||
response.default_type = 'text/html'
|
|
||||||
ctx = {}
|
|
||||||
response.set_payload(ctx)
|
|
||||||
|
|
||||||
assert state.called
|
|
||||||
|
|
||||||
|
|
||||||
def test_variedresponsebase_set_payload_renderer_unknown_kwargs():
|
|
||||||
'''VariedResponseBase.set_payload ignores unknown keyword arguments'''
|
|
||||||
|
|
||||||
def render_html(response, context):
|
|
||||||
pass
|
|
||||||
|
|
||||||
request = milla.Request.blank('http://localhost/')
|
|
||||||
response = vary.VariedResponseBase(request)
|
|
||||||
response.renderers['text/html'] = render_html
|
|
||||||
response.set_payload({}, foo='bar')
|
|
Loading…
Reference in New Issue