from collections import namedtuple
from copy import deepcopy
from itertools import chain
from tangled.decorators import register_action
from tangled.util import fully_qualified_name
from tangled.web.const import ALL_HTTP_METHODS
[docs]def config(content_type, **kwargs):
"""Decorator for configuring resources methods.
When used on a resource class, the class level configuration will be
applied to all methods.
Example::
class MyResource:
@config('text/html', template='my_resource.mako')
def GET(self):
pass
Example of defaults and overrides::
@config('*/*', status=303, response_attrs={'location': '/'})
class MyResource:
@config('*/*', status=302)
@config('text/html', status=None, response_attrs={})
def GET(self):
pass
"""
if 'redirect' in kwargs:
kwargs['status'] = kwargs.pop('redirect')
if not kwargs:
raise TypeError('@config requires at least one keyword arg')
def wrapper(wrapped):
def action(app):
# This is here so the app won't start if any of the args
# passed to @config are invalid. Otherwise, the invalid args
# wouldn't be detected until a request is made to a resource
# that was decorated with invalid args. NOTE: We can't check
# *everything* pre-emptively here, but we do what we can.
if isinstance(wrapped, type):
Config(app, 'GET', content_type, **kwargs)
elif wrapped.__name__ in ALL_HTTP_METHODS:
Config(app, wrapped.__name__, content_type, **kwargs)
differentiator = fully_qualified_name(wrapped), content_type
if app.contains(config, differentiator):
app.get(config, differentiator).update(kwargs)
else:
app.register(config, kwargs, differentiator)
register_action(wrapped, action, 'tangled.web')
return wrapped
return wrapper
_fields = ('methods', 'content_type', 'name', 'default', 'required')
ConfigArg = type('ConfigArg', (namedtuple('ConfigArg', _fields),), {})
Field = type('Field', (ConfigArg,), {})
RepresentationArg = type('RepresentationArg', (ConfigArg,), {})
class Config:
def __init__(self, app, request_method, content_type, **kwargs):
self.__dict__['request_method'] = request_method
self.__dict__['content_type'] = content_type
self.__dict__['representation_args'] = {}
def _get_args(arg_type):
all_items = []
items = app.get(arg_type, (request_method, '*/*'))
if items:
all_items.extend(items.values())
if content_type != '*/*':
items = app.get(arg_type, (request_method, content_type))
if items:
all_items.extend(items.values())
return all_items
_fields = _get_args(Field)
_args = _get_args(RepresentationArg)
self._field_names = set(f.name for f in _fields)
self._arg_names = set(a.name for a in _args)
kwargs = deepcopy(kwargs)
for config_arg in chain(_fields, _args):
name = config_arg.name
if config_arg.required and name not in kwargs:
raise TypeError('Missing resource config arg: {}'.format(name))
else:
default = config_arg.default
default = default() if callable(default) else deepcopy(default)
setattr(self, name, default)
for name, value in kwargs.items():
setattr(self, name, value)
@classmethod
def for_resource(cls, app, resource, request_method, content_type,
resource_method=None, include_defaults=True):
"""Get :class:`Config` for resource, method, & content type.
This collects all the relevant config set via ``@config`` and
combines it with the default config. Default config is set when
``@config`` args are added via :meth:`add_config_field` and
:meth:`add_representation_arg`.
Returns an info structure populated with class level defaults
for */* plus method level info for ``content_type``.
If the resource method name differs from the request method
name, pass ``resource_method`` so the method level config can be
found.
Typically, this wouldn't be used directly; usually
:meth:`Request.resource_config` would be used to get the
info for the resource associated with the current request.
"""
kwargs = cls.get_resource_args(
app, resource, request_method, content_type, resource_method, include_defaults)
return cls(app, request_method, content_type, **kwargs)
@classmethod
def get_resource_args(cls, app, resource, request_method, content_type, resource_method=None,
include_defaults=False):
"""Get config args for resource, method, & content type.
This fetches the args for a resource, method, and content type
from the app registry. Those args can then be used to construct
a :class:`Config` instance.
By default, only args for the specified content type will be
included.
.. note:: This is intended primarily for internal use.
"""
resource_cls = resource.__class__
resource_method = resource_method or request_method
resource_method = getattr(resource_cls, resource_method)
cls_name = fully_qualified_name(resource_cls)
meth_name = fully_qualified_name(resource_method)
kwargs = {}
if include_defaults:
kwargs.update(app.get(config, (cls_name, '*/*'), default={}))
kwargs.update(app.get(config, (cls_name, content_type), default={}))
if include_defaults:
kwargs.update(app.get(config, (meth_name, '*/*'), default={}))
kwargs.update(app.get(config, (meth_name, content_type), default={}))
return kwargs
def __setattr__(self, name, value):
if name.startswith('_') or name in self._field_names:
super().__setattr__(name, value)
elif name in self._arg_names:
self.representation_args[name] = value
else:
raise TypeError(
"can't set {} on {}".format(name, self.__class__))
def __repr__(self):
items = []
for name in sorted(self.__dict__):
if not name.startswith('_'):
value = self.__dict__[name]
items.append('{name}={value}'.format_map(locals()))
items = ', '.join(items)
return '{self.__class__.__name__}({items}'.format_map(locals())