Repository: marshmallow-code/webargs

Issue:
support for webapp2 framework?
Hi, just wondering if you planned on supporting the webapp2 framework which is using standard webob requests objects. This would make this library usable on appengine with more frameworks and would be awesome.
Thanks!



Discussion:
I do not have plans to add support for a webapp2 parser in the near future.

That said, new parsers are relatively simple to create. You would only need to create a `Parser` class that implements the methods for parsing webob requests. See the existing parsers for examples.


Resolving patch:
diff --git a/examples/webapp2_example.py b/examples/webapp2_example.py
new file mode 100755
--- /dev/null
+++ b/examples/webapp2_example.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""A Hello, World! example using Webapp2 in a Google App Engine environment
+
+Run the app:
+
+    $ python webapp2_example.py
+
+Try the following with httpie (a cURL-like utility, http://httpie.org):
+
+    $ pip install httpie
+    $ http GET :5001/hello
+    $ http GET :5001/hello name==Ada
+    $ http POST :5001/hello_dict name=awesome
+    $ http POST :5001/hello_dict
+"""
+
+import webapp2
+
+from webargs import Arg
+from webargs.webapp2parser import use_args, use_kwargs
+
+hello_args = {
+    'name': Arg(str, default='World')
+}
+
+
+class MainPage(webapp2.RequestHandler):
+
+    @use_args(hello_args)
+    def get_args(self, args):
+        # args is a dict of parsed items from hello_args
+        self.response.write('Hello, {name}!'.format(name=args['name']))
+
+    @use_kwargs(hello_args)
+    def get_kwargs(self, name=None):
+        self.response.write('Hello, {name}!'.format(name=name))
+
+app = webapp2.WSGIApplication([
+    webapp2.Route(r'/hello', MainPage, handler_method='get_args'),
+    webapp2.Route(r'/hello_dict', MainPage, handler_method='get_kwargs'),
+], debug=True)
+
+
+if __name__ == '__main__':
+    from wsgiref.simple_server import make_server
+    httpd = make_server('', 5001, app)
+    print "Serving on port 5001..."
+    httpd.serve_forever()
diff --git a/webargs/webapp2parser.py b/webargs/webapp2parser.py
new file mode 100644
--- /dev/null
+++ b/webargs/webapp2parser.py
@@ -0,0 +1,78 @@
+# -*- coding: utf-8 -*-
+"""Webapp2 request argument parsing module.
+
+Example: ::
+
+    import webapp2
+
+    from webargs import Arg
+    from webargs.webobparser import use_args
+
+    hello_args = {
+        'name': Arg(str, default='World')
+    }
+
+    class MainPage(webapp2.RequestHandler):
+        @use_args(hello_args)
+        def get_args(self, args):
+            self.response.write('Hello, {name}!'.format(name=args['name']))
+        @use_args(hello_args, as_kwargs=True)
+        def get_kwargs(self, name=None):
+            self.response.write('Hello, {name}!'.format(name=name))
+
+    app = webapp2.WSGIApplication([
+        webapp2.Route(r'/hello', MainPage, handler_method='get_args'),
+        webapp2.Route(r'/hello_dict', MainPage, handler_method='get_kwargs'),
+    ], debug=True)
+"""
+import logging
+
+
+from webargs import core
+import webapp2
+import webapp2_extras.json
+import webob.multidict
+
+logger = logging.getLogger(__name__)
+
+
+class Webapp2Parser(core.Parser):
+    """webapp2 request argument parser."""
+
+    def parse_json(self, req, name, arg):
+        """Pull a json value from the request."""
+        try:
+            json_data = webapp2_extras.json.decode(req.body)
+            return core.get_value(json_data, name, arg.multiple)
+        except ValueError:
+            return core.Missing
+
+    def parse_querystring(self, req, name, arg):
+        """Pull a querystring value from the request."""
+        return core.get_value(req.GET, name, arg.multiple)
+
+    def parse_form(self, req, name, arg):
+        """Pull a form value from the request."""
+        return core.get_value(req.POST, name, arg.multiple)
+
+    def parse_cookies(self, req, name, arg):
+        """Pull the value from the cookiejar."""
+        return core.get_value(req.cookies, name, arg.multiple)
+
+    def parse_headers(self, req, name, arg):
+        """Pull a value from the header data."""
+        return core.get_value(req.headers, name, arg.multiple)
+
+    def parse_files(self, req, name, arg):
+        """Pull a file from the request."""
+        files = ((k, v) for k, v in req.POST.items() if hasattr(v, 'file'))
+        return core.get_value(webob.multidict.MultiDict(files), name, arg.multiple)
+
+    def parse(self, argmap, req=None, locations=None, validate=None, force_all=False):
+        """Wrap :meth:`core.Parser.parse` to inject the active :class:`webapp2.Request` in"""
+        req = req or webapp2.get_request()
+        return super(Webapp2Parser, self).parse(argmap, req, locations, validate, force_all)
+
+parser = Webapp2Parser()
+use_args = parser.use_args
+use_kwargs = parser.use_kwargs


Test patch:
diff --git a/tests/test_webapp2parser.py b/tests/test_webapp2parser.py
new file mode 100644
--- /dev/null
+++ b/tests/test_webapp2parser.py
@@ -0,0 +1,123 @@
+# -*- coding: utf-8 -*-
+"""Tests for the webapp2 parser"""
+import urllib
+import json
+
+import pytest
+from webargs.core import PY2
+from .compat import text_type
+from webargs import Arg, ValidationError
+
+pytestmark = pytest.mark.skipif(not PY2, reason='webapp2 is only compatible with Python 2')
+
+import webtest
+
+if PY2:
+    # everything should be skipped via `pytestmark` here so it is OK
+    import webapp2
+    from webargs.webapp2parser import parser
+
+hello_args = {
+    'name': Arg(text_type, default='World'),
+}
+
+hello_multiple = {
+    'name': Arg(multiple=True),
+}
+
+hello_validate = {
+    'num': Arg(int, validate=lambda n: n != 3, error="Houston, we've had a problem."),
+}
+
+
+def test_parse_querystring_args():
+    request = webapp2.Request.blank('/echo?name=Fred')
+    assert parser.parse(hello_args, req=request) == {'name': 'Fred'}
+
+
+def test_parse_querystring_multiple():
+    expected = {'name': ['steve', 'Loria']}
+    request = webapp2.Request.blank('/echomulti?name=steve&name=Loria')
+    assert parser.parse(hello_multiple, req=request) == expected
+
+
+def test_parse_form():
+    expected = {'name': 'Joe'}
+    request = webapp2.Request.blank('/echo', POST=expected)
+    assert parser.parse(hello_args, req=request) == expected
+
+
+def test_parse_form_multiple():
+    expected = {'name': ['steve', 'Loria']}
+    request = webapp2.Request.blank('/echo', POST=urllib.urlencode(expected, doseq=True))
+    assert parser.parse(hello_multiple, req=request) == expected
+
+
+def test_parsing_form_default():
+    request = webapp2.Request.blank('/echo', POST='')
+    assert parser.parse(hello_args, req=request) == {'name': 'World'}
+
+
+def test_parse_json():
+    expected = {'name': 'Fred'}
+    request = webapp2.Request.blank('/echo', POST=json.dumps(expected), headers={'content-type': 'application/json'})
+    assert parser.parse(hello_args, req=request) == expected
+
+
+def test_parse_json_default():
+    request = webapp2.Request.blank('/echo', POST='', headers={'content-type': 'application/json'})
+    assert parser.parse(hello_args, req=request) == {'name': 'World'}
+
+
+def test_parsing_cookies():
+    # whitespace is not valid in a cookie name or value per RFC 6265
+    # http://tools.ietf.org/html/rfc6265#section-4.1.1
+    expected = {'name': 'Jean-LucPicard'}
+    response = webapp2.Response()
+    response.set_cookie('name', expected['name'])
+    request = webapp2.Request.blank('/', headers={'Cookie': response.headers['Set-Cookie']})
+    assert parser.parse(hello_args, req=request, locations=('cookies',)) == expected
+
+
+def test_parsing_headers():
+    expected = {'name': 'Fred'}
+    request = webapp2.Request.blank('/', headers=expected)
+    assert parser.parse(hello_args, req=request, locations=('headers',)) == expected
+
+
+def test_parse_files():
+    """Test parsing file upload using WebTest since I don't know how to mock that using a webob.Request"""
+    class Handler(webapp2.RequestHandler):
+        @parser.use_args({'myfile': Arg(multiple=True)}, locations=('files',))
+        def post(self, args):
+            self.response.content_type = 'application/json'
+            _value = lambda f: f.getvalue().decode('utf-8')
+            data = dict((i.filename, _value(i.file)) for i in args['myfile'])
+            self.response.write(json.dumps(data))
+    app = webapp2.WSGIApplication([('/', Handler)])
+    testapp = webtest.TestApp(app)
+    payload = [('myfile', 'baz.txt', b'bar'), ('myfile', 'moo.txt', b'zoo')]
+    res = testapp.post('/', upload_files=payload)
+    assert res.json == {'baz.txt': 'bar', 'moo.txt': 'zoo'}
+
+
+def test_exception_on_validation_error():
+    request = webapp2.Request.blank('/', POST={'num': '3'})
+    with pytest.raises(ValidationError):
+        parser.parse(hello_validate, req=request)
+
+
+def test_validation_error_with_message():
+    request = webapp2.Request.blank('/', POST={'num': '3'})
+    with pytest.raises(ValidationError) as exc:
+        parser.parse(hello_validate, req=request)
+        assert "Houston, we've had a problem." in exc.value
+
+
+def test_default_app_request():
+    """Test that parser.parse uses the request from webapp2.get_request() if no request is passed"""
+    expected = {'name': 'Joe'}
+    request = webapp2.Request.blank('/echo', POST=expected)
+    app = webapp2.WSGIApplication([])
+    app.set_globals(app, request)
+    assert parser.parse(hello_args) == expected