Skip to content

Commit

Permalink
Changed the __setattr__ and __getattr__ behavior of Request instances.
Browse files Browse the repository at this point in the history
New attributes are now added to the environ dictionary as 'bottle.request.env.<name>' values.
This has several advantages over __dict__:
  - Instances can be reused without leaking request context (needed for LocalRequest).
  - It is now impossible to overwrite existing attributes.
  - Middleware and plugins can set and access these attributes.
  - We can use __slots__.
  • Loading branch information
defnull committed Apr 4, 2012
1 parent 8cbd14c commit a21d716
Show file tree
Hide file tree
Showing 2 changed files with 39 additions and 28 deletions.
43 changes: 25 additions & 18 deletions bottle.py
Original file line number Diff line number Diff line change
Expand Up @@ -897,7 +897,12 @@ def __call__(self, environ, start_response):

class BaseRequest(object):
""" A wrapper for WSGI environment dictionaries that adds a lot of
convenient access methods and properties. Most of them are read-only."""
convenient access methods and properties. Most of them are read-only.
Adding new attributes to a request actually adds them to the environ
dictionary (as 'bottle.request.ext.<name>'). This is the recommended
way to store and access request-specific data.
"""

__slots__ = ('environ')

Expand All @@ -906,17 +911,17 @@ class BaseRequest(object):
#: Maximum number pr GET or POST parameters per request
MAX_PARAMS = 100

def __init__(self, environ):
def __init__(self, environ=None):
""" Wrap a WSGI environ dictionary. """
#: The wrapped WSGI environ dictionary. This is the only real attribute.
#: All other attributes actually are read-only properties.
self.environ = environ
environ['bottle.request'] = self
self.environ = {} if environ is None else environ
self.environ['bottle.request'] = self

@DictProperty('environ', 'bottle.app', read_only=True)
def app(self):
''' Bottle application handling this request. '''
raise AttributeError('This request is not connected to an application.')
raise RuntimeError('This request is not connected to an application.')

@property
def path(self):
Expand Down Expand Up @@ -1216,15 +1221,22 @@ def __setitem__(self, key, value):
for key in todelete:
self.environ.pop('bottle.request.'+key, None)

def __repr__(self):
return '<%s: %s %s>' % (self.__class__.__name__, self.method, self.url)

def __getattr__(self, name):
''' Search in self.environ for additional user defined attributes. '''
try:
var = self.environ['bottle.request.ext.%s'%name]
return var.__get__(self) if hasattr(var, '__get__') else var
except KeyError:
raise AttributeError('Custom attribute %r not found.' % name)
raise AttributeError('Attribute %r not defined.' % name)

def __setattr__(self, name, value):
if name == 'environ': return object.__setattr__(self, name, value)
self.environ['bottle.request.ext.%s'%name] = value


def __repr__(self):
return '<%s: %s %s>' % (self.__class__.__name__, self.method, self.url)


def _hkey(s):
Expand Down Expand Up @@ -1472,22 +1484,18 @@ def __repr__(self):
#: attributes.
_lctx = threading.local()

def local_property(name, doc=None):

return property(
lambda self: getattr(_lctx, name),
lambda self, value: setattr(_lctx, name, value),
lambda self: delattr(_lctx, name),
doc or ('Thread-local property stored in :data:`_lctx.%s` ' % name)
)
def local_property(name):
return property(lambda self: getattr(_lctx, name),
lambda self, value: setattr(_lctx, name, value),
lambda self: delattr(_lctx, name),
'Thread-local property stored in :data:`_lctx.%s`' % name)

class LocalRequest(BaseRequest):
''' A thread-local subclass of :class:`BaseRequest` with a different
set of attribues for each thread. There is usually only one global
instance of this class (:data:`request`). If accessed during a
request/response cycle, this instance always refers to the *current*
request (even on a multithreaded server). '''
def __init__(self): pass
bind = BaseRequest.__init__
environ = local_property('request_environ')

Expand All @@ -1498,7 +1506,6 @@ class LocalResponse(BaseResponse):
instance of this class (:data:`response`). Its attributes are used
to build the HTTP response at the end of the request/response cycle.
'''
def __init__(self): pass
bind = BaseResponse.__init__
_status_line = local_property('response_status_line')
_status_code = local_property('response_status_code')
Expand Down
24 changes: 14 additions & 10 deletions test/test_environ.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@
import threading
import base64

from bottle import BaseRequest, BaseResponse
from bottle import BaseRequest, BaseResponse, LocalRequest

class TestRequest(unittest.TestCase):

def test_app(self):
e = {}
r = BaseRequest(e)
self.assertRaises(AttributeError, lambda: r.app)
self.assertRaises(RuntimeError, lambda: r.app)
e.update({'bottle.app': 5})
self.assertEqual(r.app, 5)

Expand Down Expand Up @@ -395,14 +395,18 @@ def test_maxparam(self):
finally:
BaseRequest.MAX_PARAMS = old_value

def test_no_additional_attributes(self):
r = BaseRequest({})
r.environ = {}
self.assertRaises(AttributeError, lambda: setattr(r, 'foo', 5))
r.environ['bottle.request.ext.foo'] = 5
self.assertEquals(r.foo, 5)
r.environ['bottle.request.ext.e'] = property(lambda self: self.environ)
self.assertEquals(r.e, r.environ)
def test_user_defined_attributes(self):
for cls in (BaseRequest, LocalRequest):
r = cls()

# New attributes go to the environ dict.
r.foo = 'somevalue'
self.assertEqual(r.foo, 'somevalue')
self.assertTrue('somevalue' in r.environ.values())

# Unknown attributes raise AttributeError.
self.assertRaises(AttributeError, getattr, r, 'somevalue')



class TestResponse(unittest.TestCase):
Expand Down

0 comments on commit a21d716

Please sign in to comment.