Commits

Anonymous committed 96e7457

Add methods req.make_body_seekable and req.copy_body
Add **kw to Request.__init__, so all attributes can be set. This is particularly useful with Request.blank
Fix an error message string interpolation

Comments (0)

Files changed (3)

 * Added in ``sorted`` function for backward compatibility with Python
   2.3.
 
+* Allow keyword arguments to :class:`webob.Request`, which assign
+  attributes (possibly overwriting values in the environment).
+
+* Added methods :method:`webob.Request.make_body_seekable` and
+  :method:`webob.Request.copy_body`, which make it easier to share a
+  request body among different consuming applications, doing something
+  like `req.make_body_seekable(); req.body_file.seek(0)``
+
 0.9.1
 -----
 

tests/test_request.py

     assert req.decode_param_names
     assert u'\u1000' in req.GET.keys()
     assert req.GET[u'\u1000'] == 'x'
+
+class UnseekableInput(object):
+    def __init__(self, data):
+        self.data = data
+        self.pos = 0
+    def read(self, size=-1):
+        if size == -1:
+            t = self.data[self.pos:]
+            self.pos = len(self.data)
+            return t
+        else:
+            assert self.pos + size <= len(self.data), (
+                "Attempt to read past end (length=%s, position=%s, reading %s bytes)"
+                % (len(self.data), self.pos, size))
+            t = self.data[self.pos:self.pos+size]
+            self.pos += size
+            return t
+
+def test_copy():
+    req = Request.blank('/', method='POST', body='some text', request_body_tempfile_limit=1)
+    old_body_file = req.body_file
+    req.copy_body()
+    assert req.body_file is not old_body_file
+    req = Request.blank('/', method='POST', body_file=UnseekableInput('0123456789'), content_length=10)
+    assert not hasattr(req.body_file, 'seek')
+    old_body_file = req.body_file
+    req.make_body_seekable()
+    assert req.body_file is not old_body_file
+    assert req.body == '0123456789'
+    old_body_file = req.body_file
+    req.make_body_seekable()
+    assert req.body_file is old_body_file
+    
+    

webob/__init__.py

     request_body_tempfile_limit = 10*1024
 
     def __init__(self, environ=None, environ_getter=None, charset=NoDefault, unicode_errors=NoDefault,
-                 decode_param_names=NoDefault):
+                 decode_param_names=NoDefault, **kw):
         if environ is None and environ_getter is None:
             raise TypeError(
                 "You must provide one of environ or environ_getter")
             self.__dict__['unicode_errors'] = unicode_errors
         if decode_param_names is not NoDefault:
             self.__dict__['decode_param_names'] = decode_param_names
+        for name, value in kw.items():
+            if not hasattr(self.__class__, name):
+                raise TypeError(
+                    "Unexpected keyword: %s=%r" % name, value)
+            setattr(self, name, value)
 
     def __setattr__(self, attr, value, DEFAULT=[]):
         ## FIXME: I don't know why I need this guard (though experimentation says I do)
         This only does a shallow copy, except of wsgi.input
         """
         env = self.environ.copy()
-        data = self.body
-        tempfile_limit = self.request_body_tempfile_limit
-        if tempfile_limit and len(data) > tempfile_limit:
-            fileobj = tempfile.TemporaryFile()
-            fileobj.write(data)
-            fileobj.seek(0)
-        else:
-            fileobj = StringIO(data)
-        env['wsgi.input'] = fileobj
-        return self.__class__(env)
+        new_req = self.__class__(env)
+        new_req.copy_body()
+        return new_req
 
     def copy_get(self):
         """
         env['REQUEST_METHOD'] = 'GET'
         return self.__class__(env)
 
+    def make_body_seekable(self):
+        """
+        This forces ``environ['wsgi.input']`` to be seekable.  That
+        is, if it doesn't have a `seek` method already, the content is
+        copied into a StringIO or temporary file.
+
+        The choice to copy to StringIO is made from
+        ``self.request_body_tempfile_limit``
+        """
+        input = self.body_file
+        if hasattr(input, 'seek'):
+            # It has a seek method, so we don't need to do anything
+            return
+        self.copy_body()
+
+    def copy_body(self):
+        """
+        Copies the body, in cases where it might be shared with
+        another request object and that is not desired.
+
+        This copies the body in-place, either into a StringIO object
+        or a temporary file.
+        """
+        length = self.content_length
+        if length == 0:
+            # No real need to copy this, but of course it is free
+            self.body_file = StringIO('')
+            return
+        tempfile_limit = self.request_body_tempfile_limit
+        body = None
+        input = self.body_file
+        if length == -1:
+            body = self.body
+            length = len(body)
+            self.content_length = length
+        if tempfile_limit and length > tempfile_limit:
+            fileobj = tempfile.TemporaryFile()
+            if body is None:
+                while length:
+                    data = input.read(min(length, 4096))
+                    fileobj.write(data)
+                    length -= len(data)
+            else:
+                fileobj.write(body)
+            fileobj.seek(0)
+        else:
+            if body is None:
+                body = input.read(length)
+            fileobj = StringIO(body)
+        self.body_file = fileobj
+
     def remove_conditional_headers(self, remove_encoding=True, remove_range=True,
                                         remove_match=True, remove_modified=True):
         """
             if not hasattr(self.__class__, name):
                 # Not a basic attribute
                 raise TypeError(
-                    "Unexpected keyword: %s=%r in %r" % (name, value))
+                    "Unexpected keyword: %s=%r" % (name, value))
             setattr(self, name, value)
 
     def __repr__(self):