The idea is basically to prefer the simple call signature of "resp =
func(req)", as well as allowing HTTPExceptions. There are some other
hooks to fiddle with the internals, though I'm not sure they are
entirely useful. Also notable is wsgiwrap.unwrap, which converts a
WSGI app into a function that takes a request object and returns a
response object. And also a middleware factory in
wsgiwrap.middleware, which makes middleware relatively easier to run
(I got the idea if a middleware wrapper from Sergey's code, but by
unwrapping the application, it makes it easier to write
response-modifying middleware). Huh, "unwrap" is a bad name for this.
This isn't really tested, except to make sure it doesn't have syntax
errors, as I'm really just trying to think about the API. Does this
seem reasonable?
Well, a little context: I've been using no framework for projects
lately, but I keep rewriting this decorator. And always with just
slightly different features and level of completeness (in a bad way).
I want it Right, and it seems time. Also, I noticed Simon Willison
playing with such a minimal framework
(http://github.com/simonw/djng/tree), which got me in the state of
mind. For instance, looking at this code:
http://github.com/simonw/djng/blob/72d8a5aa9088720841321f8e4177d7ceb63f7368/djng/errors.py
here's how you'd do it with my code:
@wsgiwrap.middleware(unwrap_args=dict(raise_errors=True))
def ErrorWrapper(req, app, custom_404=default_404, custom_500=default_500):
try:
return app(req, normalize_exceptions=True)
except webob.exc.HTTPNotFound, resp:
return custom_404(req) if custom_404 else resp
except Exception, e:
return custom_500(req, e)
def default_404(req):
return Response('A 404 error occurred')
def default_500(req, e):
return Response('A 500 error occurred: %r' % e)
Not really that different, but eh. At least it works, which took a
little refactoring to accomplish.
One thing I don't like here is default_500 has that extra e argument,
making @wsgiwrap around it kind of useless. But I guess that's okay.
One of the things I like with this is that you can have functions with
parameters, and while they can't be WSGI apps (for reasonable
reasons), they still look and act similar to everything else.
You actually can turn them into WSGI apps if you do
@wsgiwrap(add_urlvars=True) and then you can set req.urlvars['e'].
Arguably add_urlvars should be set to true by default, except this
means every function needs **kw to avoid TypeError just because
req.urlvars is set at some point. There's code (that at least TG
uses) that safely calls functions in these cases, but the code is
complex so I worry about introducing it (at least by default).
So, in conclusion: thoughts?
--
Ian Bicking | http://blog.ianbicking.org
Yes, and people are talking about this for WSGI 2. But it doesn't
really fix this either, as you are working with the raw WSGI-like
objects (dicts and tuples) instead of request and response objects.
So this has added value over even that.
> That djng micro framework is neat. The idea of reloadable /
> reconfigurable services sounds interesting but I've never needed to
> change a conf setting during runtime myself.
Probably there's some overlap with something like repoze.plugin too.
There's a lot of ways of approaching configuration. I haven't figured
out the right way myself.
Somewhat similar is something like Beaker that pokes things into the
environ; it's essentially implementing a service in the scope of the
request, and using the environ to indicate that.
Contextual works really well for these things, much better than
messing with environ does.
As of ErrorWrapper I would split it into two separate middlewares like this:
def override_errors(app, overrides={}):
@webob_wrap
def wrapped(req):
r = req.get_response(app)
return overrides.get(r.status_int, r)
return wrapped
def default_500(req, exc):
return Response('A 500 error occurred: %r' % exc, status_int=500)
def catch_exc(app, app500=default_500):
@webob_wrap
def wrapped(req):
try:
return req.get_response(app)
except Exception, exc:
return app500(req, exc)
return wrapped
Used like this:
app = catch_exc(app)
app = override_error(app, {500: Response("Everything is OK <_<;;")})
One issue with all of these techniques is they make the interactive
debugger break. paste.registry solves that (but very complexly) and
as far as I know that's the only thing that tries to address the
problem of restoring context. Contextual could address that, but I
don't know if it ever will.
Yes, and the implementation I gave is also very similar. It has some
more complexity in generating the response (handling cases where the
function doesn't return a response object), and I'm not sure if that
is justified. Also None isn't turned into 404, but I have a personal
dislike of unhelpful 404s, so I'm not too psyched about that feature
anyway.
> Anyway, the idea is that a web application is something that takes
> HTTP request and returns HTTP response, which naturally maps to a
> function that takes webob.Request and returns webob.Response or raises
> webob.exc.HTTPException. We need to be able to expose such functions
> as WSGI app, so the obvious solution follows from there.
>
> As I was using such a decorator one thing became obvious: returning a
> webob.Response is a limitation, so the solution is (not as obviously)
> is to return any WSGI app. This might seem strange as WSGI app does
> not exactly maps to HTTP response, but as the wrapper knows the
> request, then (envrion + WSGI app) does map to a HTTP response.
>
> Anyway this works wonderfully and makes composing web apps from
> disparate parts a breeze as well as writing new parts and implementing
> them as really independent reusable components.
Sure, I've used this pattern myself in the past, specifically in
FlatAtomPub (http://svn.pythonpaste.org/Paste/apps/FlatAtomPub/trunk).
Ultimately I wasn't that happy with it, though, because I started
passing around all my arguments as attributes on the request.
> Before I show how the code looks when using this decorator I have to
> mention that the decorator works on methods, which means that if your
> webapp needs configuration or multiple instances, it still can use the
> decorator.
Yes, I think that's just a given.
> Let's have a look paste.evalexception.middleware
> http://svn.pythonpaste.org/Paste/trunk/paste/evalexception/middleware.py
>
> Do have a look at the EvalException class -- it has to serve some
> static content to help error pages look and work properly and it has
> to find the saved traceback based on url. With webob_wrap we can scrap
> huge chunks of code and the remaining will be clearly readable. For
> example have a look at `media` and `mochikit' methods -- the entire
> thing can be replaced with:
>
> media = urlparser.StaticURLParser(os.path.join(os.path.dirname(__file__),
> 'media'))
> mochikit = urlparser.StaticURLParser(os.path.join(os.path.dirname(__file__),
> 'mochikit'))
Well, also evalexception is kind of a mess, the result of exploratory
coding that was never fully refactored. So... not a great comparison
;) But yes, it fits the pattern.
My decorators invert this, you use self.subapp(req) or
self.subapp.wsgi_app(environ, start_response). I think this is better
for most cases. I think it's much more obvious when you do:
return self.subapp(req)
instead of simply:
return self.subapp
Of course you can do "return self.subapp.wsgi_app", but I doubt people
will do that much given the simpler technique of calling the method.
> As of middleware, for most uses webob_wrap is enough, for example:
>
> def gzip_middleware(app):
> @webob_wrap
> def wrapped(req):
> resp = req.get_response(app)
> if 'gzip' in req.accept_encoding:
> resp.encode_content()
> return resp
> return wrapped
>
>
> Or, if more verbosely:
>
> class GZipMiddleware(object):
> def __init__(self, app, types=('text/plain',)):
> self.app = app
> self.types = types
>
> @webob_wrap
> def __call__(self, req):
> resp = req.get_response(app)
> if 'gzip' in req.accept_encoding and resp.content_type in self.types:
> resp.encode_content()
> return resp
With wsgiwrap.unwrap, I think the situation is improved -- the
application you are wrapping feels more like "internal" code, even if
it is an arbitrary WSGI application.
> And some closing notes:
> * Not only you can easily embed foreign code into you apps, you can
> directly mesh your code with other apps (paste URLMap etc)
> * Even if anyone wants more full-featured decorator, such as
> extracting form values and passing them as arguments webob_wrap would
> serve as a sound foundation for that, one doesn't need to make
> everything one giant decorator. Also, I'd advice against using the
> argument extraction described above: req.GET['arg'] works just fine.
> * This is not a framework and not even a beginning of one --
> frameworks imply inversion of control at some point and this is just a
> decorator (or a bunch of). Frameworks always take their price for the
> provided convenience, @webob_wrap doesn't.
While I've put more features than is perhaps necessary into one class,
I think for the most part the code I give accomplishes the same
things. Is there something specific that you think webob_wrap does
that wsgiwrap does worse?
I think that saying that it "breaks" the debugger is a bit of
exaggeration. Sure things change as the exception propagates, but then
we could say that "finally" blocks break it too. I never really had a
problem with debugging because of it, at least it's very clear when
the services are being used so you know you can't rely on them being
the same when exploring the traceback.
Well, that breaks WSGI doesn't it? I'm with PJE on the issue of adding
new keys to environ in middleware and applications, so when my apps
need more parameters I implement them as classes or closures. This has
the added benefit of them now being easily separable from the rest of
the app. For example a forum topic would be a ForumTopic(id) which is
a self-contained WSGI app that you can use anywhere at all.
> Well, also evalexception is kind of a mess, the result of exploratory
> coding that was never fully refactored. So... not a great comparison
> ;) But yes, it fits the pattern.
Well, that's true, but it also shows how evalexception was trying to
do the most obvious thing with request dispatching and WSGI was
standing in the way, and with webob_wrap the very same approach simply
works and it's still all wsgi.
>> One thing that used to bother me was is that the wrapped functions
>> loose their signature becoming WSGI apps -- now if you want to change
>> their return value you don't call them directly but use
>> req.get_response(self.subapp) for example. But it's better that way --
>> as those apps might return something other than webob.Response and
>> using get_response gives us the guarantee that we get webob.Response.
>> Also, if you really need to get to the wrapped function you can use
>> self.subapp.func(req).
>
> My decorators invert this, you use self.subapp(req) or
> self.subapp.wsgi_app(environ, start_response). I think this is better
> for most cases. I think it's much more obvious when you do:
>
> return self.subapp(req)
>
> instead of simply:
>
> return self.subapp
>
> Of course you can do "return self.subapp.wsgi_app", but I doubt people
> will do that much given the simpler technique of calling the method.
Well, the main difference between our approaches here, I think, is
that I'm trying to make as few assumptions as possible -- when
returning self.subapp the only assumption is that it's a WSGI app.
With your approach it's assumed by default that subapp is not a wsgi
app but a wrapped method, same thing if using self.subapp.wsgi_app --
you can't just replace it with something else.
When using webob_wrap you can use it in just one place and nothing
else will need to know anything about it, not so with wsgiwrap.
>> As of middleware, for most uses webob_wrap is enough, for example:
>>
>> def gzip_middleware(app):
>> @webob_wrap
>> def wrapped(req):
>> resp = req.get_response(app)
>> if 'gzip' in req.accept_encoding:
>> resp.encode_content()
>> return resp
>> return wrapped
>>
>>
>> Or, if more verbosely:
>>
>> class GZipMiddleware(object):
>> def __init__(self, app, types=('text/plain',)):
>> self.app = app
>> self.types = types
>>
>> @webob_wrap
>> def __call__(self, req):
>> resp = req.get_response(app)
>> if 'gzip' in req.accept_encoding and resp.content_type in self.types:
>> resp.encode_content()
>> return resp
I guess I'll add the webob_middleware example as well
@webob_middleware
def gzip_middleware(req, app, types=('text/plain',)):
resp = req.get_response(app)
if 'gzip' in req.accept_encoding and resp.content_type in types:
resp.encode_content()
return resp
> With wsgiwrap.unwrap, I think the situation is improved -- the
> application you are wrapping feels more like "internal" code, even if
> it is an arbitrary WSGI application.
I don't think trying to make everything adapt to some new convention
is the right thing to do -- you adapt external components to it, but
you inevitably write your code to conform to it in the first place --
this means that code is not reusable without more adaptation or
rewrites. That's exactly the problem with frameworks -- reinventing
the wheel in the name of convenience and isolating itself as a result.
If we agree that webob_wrap is close in convenience of use, then I
think it wins on the base of that it makes everything fit by default,
simply by enforcing that every wrapped function and method is a WSGI
app.
Surely sometimes you just want to call self.subapp(req, arg), but then
we don't need no decorators for it -- self.subapp.wsgi_app wouldn't
work anyway, right?
>> And some closing notes:
>> * Not only you can easily embed foreign code into you apps, you can
>> directly mesh your code with other apps (paste URLMap etc)
When we need to delegate the response to an URLMap instance for
example, what would be the code with wsgiwrap, "return
wsgiwrap.unwrap(urlmap)(req)" ?
With webob_wrap it's "return urlmap".
>> * This is not a framework and not even a beginning of one --
>> frameworks imply inversion of control at some point and this is just a
>> decorator (or a bunch of). Frameworks always take their price for the
>> provided convenience, @webob_wrap doesn't.
>
> While I've put more features than is perhaps necessary into one class,
> I think for the most part the code I give accomplishes the same
> things. Is there something specific that you think webob_wrap does
> that wsgiwrap does worse?
I've outlined what I think is it's flaw: introducing another interface
that has no clear benefit and does have a potential of creating yet
one more "walled garden". That's how I see it anyway.
This is great news. I have long been advocating an
application-building approach based on WebOb rather than raw WSGI, and
this looks like a step in the right direction. It's a little
abstract for me at this point (I'm doing well with Pylons), but I hope
to get involved with it later when it solidifies.
The unwrap concept sounds like one of the most important
contributions, but I agree it needs to be renamed: "unwrap" sounds
confusing and perhaps misleading.
I'm not sure about Sergey's assertion that Paste should remain
together rather than smashed into pieces. Partly because I know Ian
and trust his judgement, and he has long been thinking that the Paste
components would do better standalone.
paste.deploy and paste.script are distributed standalone as a
namespace package, and I think that's the worst of either world. They
were split because some users would not use them if they were packaged
with the whole kaboodle, and those users were considered vital to its
success. At the same time, namespace packages add an unnecessary
layer of complexity. For instance, don't they depend on Setuptools?
Which causes problems for people who have trouble installing
Setuptools, or using utilities like Py2exe which don't recognize
Setuptools.
The problem with the Paste components was that they stagnated, and
they couldn't be changed without breaking backward compatibility.
Whereas with separate packages, you just don't install the ones that
are behind the times. WebOb was an incompatible upgrade of
Paste.Request/Response, at it has now gained as much use as Paste
itself.
Users of PasteDeploy and PasteScript have some doubts about whether
it's the best approach. Likewise, Paste.Config has been wrapped in
Pylons because the raw implementation wasn't adequate. The rest of
Paste is small useful pieces: FileApp and Composite probably being the
most useful. NiserConfigParser (the lowerkase 'C' key just got stuk
in my GMail) deserves to be unburied from loadwsgi and enhansed. But
most of the rest is obsolete, like Webware and Wareweb. Perhaps
FileApp and Composite do need a kollektion bekause they're too small
to release standalone, but that doesn't mean Paste is nesessarily the
best kollektion.
--
Mike Orr <slugg...@gmail.com>
--
Mike Orr <slugg...@gmail.com>
I didn't say that, Jon Nelson did
http://groups.google.com/group/paste-users/msg/727a3e6adff22766
I generally agree with what Jon is saying, but I don't really care either way.
Yes, I could have done it all as methods on a class, or closures, or
something like that. If the class was instantiated per-request it'd
be relatively easy, though that's a different pattern still.
I was thinking how PylonsController could fit into this pattern,
actually. You could have something like:
class wrapinstance(object):
def __init__(self, instantiate_with_request=False):
self.instantiate_with_request = instantiate_with_request
def __get__(self, obj, type=None):
if obj is None:
def wsgi_app(environ, start_response):
if self.instantiate_with_request:
instance = type(Request(environ))
else:
instance = type()
return wsgiwrap(instance).wsgi_app(environ, start_response)
return wsgiwrap(obj)
Then you could do:
class MyController(object):
wsgi_app = wrapinstance()
def __call__(self, req):
return Response('hello world')
Now MyController.wsgi_app is a WSGI application that instantiates the
class once per request, similar to how PylonsController works.
>>> One thing that used to bother me was is that the wrapped functions
>>> loose their signature becoming WSGI apps -- now if you want to change
>>> their return value you don't call them directly but use
>>> req.get_response(self.subapp) for example. But it's better that way --
>>> as those apps might return something other than webob.Response and
>>> using get_response gives us the guarantee that we get webob.Response.
>>> Also, if you really need to get to the wrapped function you can use
>>> self.subapp.func(req).
>>
>> My decorators invert this, you use self.subapp(req) or
>> self.subapp.wsgi_app(environ, start_response). I think this is better
>> for most cases. I think it's much more obvious when you do:
>>
>> return self.subapp(req)
>>
>> instead of simply:
>>
>> return self.subapp
>>
>> Of course you can do "return self.subapp.wsgi_app", but I doubt people
>> will do that much given the simpler technique of calling the method.
>
> Well, the main difference between our approaches here, I think, is
> that I'm trying to make as few assumptions as possible -- when
> returning self.subapp the only assumption is that it's a WSGI app.
> With your approach it's assumed by default that subapp is not a wsgi
> app but a wrapped method, same thing if using self.subapp.wsgi_app --
> you can't just replace it with something else.
>
> When using webob_wrap you can use it in just one place and nothing
> else will need to know anything about it, not so with wsgiwrap.
No, wsgiwrap works anywhere. If you look at wsgiwrap.unwrap it takes
an *arbitrary* WSGI application and returns an object that acts like
something wsgiwrap wrapped (it takes a single request argument and
returns a WebOb response). It does this without any expectation
beyond WSGI itself (though there's a shortcut in the code for the case
when a wsgiwrap middleware wraps a wsgiapp application).
I want there to be a relatively small amount of impedance between a
method that takes a request argument (which is common regardless of
how anything else works) and a WSGI application.
For the case of classes, I think I'd like to see a situation where
something like PylonsController is just *a little* closer to being a
WSGI application. Right now it is after you instantiate it, but it
isn't self-instantiating and instances can't be shared. And I think
there's some other details that keep people from treating them like
WSGI applications (or dropping in alternates) even though they can.
Also, for a general application it is quite likely you'll just have
one up-front WSGI application, and everything internally will use this
convention (request-in, response-out) regardless of whether it has to
or if it could be WSGI internally. I like that this decorator meets
that case halfway.
>>> And some closing notes:
>>> * Not only you can easily embed foreign code into you apps, you can
>>> directly mesh your code with other apps (paste URLMap etc)
>
> When we need to delegate the response to an URLMap instance for
> example, what would be the code with wsgiwrap, "return
> wsgiwrap.unwrap(urlmap)(req)" ?
> With webob_wrap it's "return urlmap".
You can do that too, it treats "responses" as WSGI applications and
doesn't make further assumptions. Our implementations aren't that
different from each other.
>>> * This is not a framework and not even a beginning of one --
>>> frameworks imply inversion of control at some point and this is just a
>>> decorator (or a bunch of). Frameworks always take their price for the
>>> provided convenience, @webob_wrap doesn't.
>>
>> While I've put more features than is perhaps necessary into one class,
>> I think for the most part the code I give accomplishes the same
>> things. Is there something specific that you think webob_wrap does
>> that wsgiwrap does worse?
>
> I've outlined what I think is it's flaw: introducing another interface
> that has no clear benefit and does have a potential of creating yet
> one more "walled garden". That's how I see it anyway.
Well, please look more closely, as it's not a walled garden. It does
introduce another interface, but it's basically the same internal
interface as webob_wrap, it just makes that interface external as
well.
I understand that, what I'm concerned with is that with wsgiwrap the
shorter way is to call the app / method with the request as argument
and while you're at it, why not add more arguments? That immediately
makes it harder to later replace that part with an arbitrary WSGI app.
As soon as we start calling instead of returning subapps all kinds of
interfaces will appear when they don't really need to exist. With
webob_wrap adding an argument will immediately break things which is a
good thing. I make it very explicit that you step away from WSGI when
you do it. That nudges developer in the right direction (I, for one,
appreciate this).
I'd really rather prefer apps to be WSGI wherever you slice them
instead of "one up-front WSGI application, and everything internally
will use this convention" which is albeit small but still a walled
garden.
>> I don't think trying to make everything adapt to some new convention
>> is the right thing to do -- you adapt external components to it, but
>> you inevitably write your code to conform to it in the first place --
>> this means that code is not reusable without more adaptation or
>> rewrites. That's exactly the problem with frameworks -- reinventing
>> the wheel in the name of convenience and isolating itself as a result.
>> If we agree that webob_wrap is close in convenience of use, then I
>> think it wins on the base of that it makes everything fit by default,
>> simply by enforcing that every wrapped function and method is a WSGI
>> app.
>>
>> Surely sometimes you just want to call self.subapp(req, arg), but then
>> we don't need no decorators for it -- self.subapp.wsgi_app wouldn't
>> work anyway, right?
>
> I want there to be a relatively small amount of impedance between a
> method that takes a request argument (which is common regardless of
> how anything else works) and a WSGI application.
>
> For the case of classes, I think I'd like to see a situation where
> something like PylonsController is just *a little* closer to being a
> WSGI application. Right now it is after you instantiate it, but it
> isn't self-instantiating and instances can't be shared. And I think
> there's some other details that keep people from treating them like
> WSGI applications (or dropping in alternates) even though they can.
I don't think it's really relevant to discussion about decorators.
Maybe this would be useful to make controllers / servlets play nice
with WSGI, but I don't like that approach anyway.
> Also, for a general application it is quite likely you'll just have
> one up-front WSGI application, and everything internally will use this
> convention (request-in, response-out) regardless of whether it has to
> or if it could be WSGI internally. I like that this decorator meets
> that case halfway.
I don't like the fact that if something uses this decorator it doesn't
mean you can use it as WSGI, it makes things inside decorator simpler
but it doesn't help much the code that will use the wrapped method.
>>>> And some closing notes:
>>>> * Not only you can easily embed foreign code into you apps, you can
>>>> directly mesh your code with other apps (paste URLMap etc)
>>
>> When we need to delegate the response to an URLMap instance for
>> example, what would be the code with wsgiwrap, "return
>> wsgiwrap.unwrap(urlmap)(req)" ?
>> With webob_wrap it's "return urlmap".
>
> You can do that too, it treats "responses" as WSGI applications and
> doesn't make further assumptions. Our implementations aren't that
> different from each other.
That's right, but the calling code will have assumptions about
returned value. With webob_wrap you either return the app and thus not
concerned with the exact returned value or get the response via
req.get_response in which case you know for sure you get
webob.Response. There's a way to get the wrapped function return value
directly (subapp.func(req)) but as most people will never need that
they'll never do that, and this is the only case when the caller needs
to know more than that subapp is a WSGI app.
With wsgiwrap you either always have to think about what the called
method returns or you have to enforce "only return webob.Response"
rule yourself -- that's a wrong default IMO.
>>>> * This is not a framework and not even a beginning of one --
>>>> frameworks imply inversion of control at some point and this is just a
>>>> decorator (or a bunch of). Frameworks always take their price for the
>>>> provided convenience, @webob_wrap doesn't.
>>>
>>> While I've put more features than is perhaps necessary into one class,
>>> I think for the most part the code I give accomplishes the same
>>> things. Is there something specific that you think webob_wrap does
>>> that wsgiwrap does worse?
>>
>> I've outlined what I think is it's flaw: introducing another interface
>> that has no clear benefit and does have a potential of creating yet
>> one more "walled garden". That's how I see it anyway.
>
> Well, please look more closely, as it's not a walled garden. It does
> introduce another interface, but it's basically the same internal
> interface as webob_wrap, it just makes that interface external as
> well.
I think it's very important to note that wsgiwrap allows to add more
arguments which makes it different. Also having that interface
external is a big deal too. To repeat my point once again with
webob_wrap everything is reduced to common denominator (WSGI) by
default, with wsgiwrap it's sometimes one step away and sometimes
can't be done (because of arguments).
class HostMap(dict):
@webob_wrap
def __call__(self, req):
return self[req.host.split(':')[0]]
That's all the code you need to dispatch on hostname.
app = HostMap() # <- wsgi app
app['example.com'] = wsgi_app1
app['other.com'] = wsgi_app2
Or if you need to serve different content for https clients:
@webob_middleware
def override_https(req, normal_app, secure_app):
return secure_app if req.scheme == 'https' else normal_app
app = override_https(app, app_for_https)
And override_errors once again:
@webob_middleware
def override_errors(req, app, overrides={}):
r = req.get_response(app)
return overrides.get(r.status_int, r)
(The webapps themselves are usually quite a bit more complex, but in
my experience webob_wrap does work for them just as well.)
It just doesn't get any simpler than that, what would the same apps
look with wsgiwrap? IIUC it will need some wrap / unwrap code and to
be used as wsgi apps (even when combining these kinds of
micro-components) you'll need to access .wsgi_app.
I guess the code will be quite similar, also using get_response
instead of unwrap, but that just shows that if we make the assumption
that everything is WSGI things don't get ugly. unwrap is only
necessary if we don't make the assumption that every subapp is just
WSGI, so instead of calling things we'd be doing .get_response on them
and if we do that why not simply return them as is? Doesn't wsgiwrap
choose to keep the signature to satisfy the cases where it shouldn't
be used at all really?
This would work identically, truly only the name "webob_wrap" would be changed.
>
> That's all the code you need to dispatch on hostname.
>
> app = HostMap() # <- wsgi app
> app['example.com'] = wsgi_app1
> app['other.com'] = wsgi_app2
The way wsgiwrap is written you could add wsgi apps or wrapped apps at
each point too.
>
> Or if you need to serve different content for https clients:
>
> @webob_middleware
> def override_https(req, normal_app, secure_app):
> return secure_app if req.scheme == 'https' else normal_app
Same deal, except I believe the way I've written it there's no
positional arguments, so secure_app would have to be a keyword
argument (at least when you instantiate the middleware). That could
just be fixed.
> app = override_https(app, app_for_https)
>
>
> And override_errors once again:
>
> @webob_middleware
> def override_errors(req, app, overrides={}):
> r = req.get_response(app)
> return overrides.get(r.status_int, r)
Hmm... okay, this is a little tricky, because you can't just replace
that with "r = app(req)", because there's one code path where that
could return a non-Response object (instead returning some other WSGI
app). I'll have to think about that, I think it can be resolved.
> (The webapps themselves are usually quite a bit more complex, but in
> my experience webob_wrap does work for them just as well.)
>
> It just doesn't get any simpler than that, what would the same apps
> look with wsgiwrap? IIUC it will need some wrap / unwrap code and to
> be used as wsgi apps (even when combining these kinds of
> micro-components) you'll need to access .wsgi_app.
Nope.
> I guess the code will be quite similar, also using get_response
> instead of unwrap, but that just shows that if we make the assumption
> that everything is WSGI things don't get ugly. unwrap is only
> necessary if we don't make the assumption that every subapp is just
> WSGI, so instead of calling things we'd be doing .get_response on them
> and if we do that why not simply return them as is? Doesn't wsgiwrap
> choose to keep the signature to satisfy the cases where it shouldn't
> be used at all really?
Sure, it's equivalent to req.get_response(), and basically that's all
the code does, is create a function that calls req.get_response() on
the WSGI application.
As to allowing arguments to be added, that isn't really allowed for
with wsgiwrap any more than webob_wrap (except with stuff like
urlvars, which isn't on by default and is largely internal to how the
function wants to consume the request). It's just that you can apply
wsgiwrap to a one-request-argument-taking function and then continue
to call it like normal, or call it as a wsgi application. I think
this will encourage a transition from private internal functions
(which people will always write, with eclectic signatures) to reusable
small applications (when that is appropriate).
OK, let's see
from webob import *
class HostMap(dict):
@wsgiwrap
def __call__(self, req):
return self[req.host.split(':')[0]]
app = HostMap() # <- wsgi app
app['example.com'] = Response('1')
app['other.com'] = Response('2')
#print app
Request.blank('http://example.com/').get_response(app)
This gives
File "decorator.py", line 50, in __call__
normalize_3xx_exceptions = self.normalize_3xx_exceptions
AttributeError: 'wsgiwrap' object has no attribute 'normalize_3xx_exceptions
Let's just fix it by replacing that line with normalize_3xx_exceptions = None
Now I get
File "decorator.py", line 195, in __call__
return self[req.host.split(':')[0]]
AttributeError: 'function' object has no attribute 'host'
Obviously we need to write:
Request.blank('http://example.com/').get_response(app.wsgi_app)
File "decorator.py", line 201, in <module>
Request.blank('http://example.com/').get_response(app.wsgi_app)
AttributeError: 'HostMap' object has no attribute 'wsgi_app'
Oh, right, not exactly:
Request.blank('http://example.com/').get_response(app.__call__.wsgi_app)
File "decorator.py", line 80, in wsgi_app
resp = self.apply(req)
File "decorator.py", line 137, in apply
return self(*args, **kw)
File "decorator.py", line 58, in __call__
resp = self.func(*args, **kw)
TypeError: __call__() takes exactly 2 arguments (1 given)
What? Oh, you have a typo in __get__
if hasattr(obj, '__get__'):
Should be:
if hasattr(self.func, '__get__'):
Whan now?
File "decorator.py", line 59, in __call__
resp = self.normalize_response(req, resp,
normalize_exceptions=normalize_exceptions,
NameError: global name 'req' is not defined
Let's remove that call completely. What next?
File "decorator.py", line 104, in normalize_response
resp = req.response.merge_cookies(resp)
AttributeError: 'Response' object has no attribute 'merge_cookies'
Huh? Let's skip the normalization completely. In the end it does work
similarly, but you have to use "app.__call__.wsgi_app" which isn't
even close to just "app" as is the case with webob_wrap. Not identical
at all, right?
>>
>> That's all the code you need to dispatch on hostname.
>>
>> app = HostMap() # <- wsgi app
>> app['example.com'] = wsgi_app1
>> app['other.com'] = wsgi_app2
>
> The way wsgiwrap is written you could add wsgi apps or wrapped apps at
> each point too.
@wsgiwrap
def other_app(req):
return Response('!')
app['other.com'] = other_app
print Request.blank('http://other.com/').get_response(app.__call__.wsgi_app)
OK, that does work, this doesn't:
app['other.com'] = OtherApp()
print Request.blank('http://other.com/').get_response(app.__call__.wsgi_app)
File "decorator.py", line 92, in wsgi_app
return resp(environ, start_response)
File "decorator.py", line 58, in __call__
resp = self.func(*args, **kw)
TypeError: __call__() takes exactly 1 argument (3 given)
>>
>> Or if you need to serve different content for https clients:
>>
>> @webob_middleware
>> def override_https(req, normal_app, secure_app):
>> return secure_app if req.scheme == 'https' else normal_app
>
> Same deal, except I believe the way I've written it there's no
> positional arguments, so secure_app would have to be a keyword
> argument (at least when you instantiate the middleware). That could
> just be fixed.
@wsgiwrap.middleware
def override_https(req, normal_app, secure_app):
return secure_app if req.scheme == 'https' else normal_app
app = override_https(Response('http'), secure_app=Response('https'))
print Request.blank('http://x.com/').get_response(app.wsgi_app)
File "decorator.py", line 180, in middleware_factory
kw = kw.copy()
UnboundLocalError: local variable 'kw' referenced before assignment
C'mon, you didn't even run this thing once? I'll just stop here --
these examples show exactly what I claimed they'll show -- you need to
bend backwards to make wsgi apps combine with wsgiwrap.
>> app = override_https(app, app_for_https)
>>
>>
>> And override_errors once again:
>>
>> @webob_middleware
>> def override_errors(req, app, overrides={}):
>> r = req.get_response(app)
>> return overrides.get(r.status_int, r)
>
> Hmm... okay, this is a little tricky, because you can't just replace
> that with "r = app(req)", because there's one code path where that
> could return a non-Response object (instead returning some other WSGI
> app). I'll have to think about that, I think it can be resolved.
>
>> (The webapps themselves are usually quite a bit more complex, but in
>> my experience webob_wrap does work for them just as well.)
>>
>> It just doesn't get any simpler than that, what would the same apps
>> look with wsgiwrap? IIUC it will need some wrap / unwrap code and to
>> be used as wsgi apps (even when combining these kinds of
>> micro-components) you'll need to access .wsgi_app.
>
> Nope.
Yep.
print Request.blank('/').get_response(other_app)
TypeError: other_app() takes exactly 1 argument (2 given)
print Request.blank('/').get_response(other_app.wsgi_app)
>> I guess the code will be quite similar, also using get_response
>> instead of unwrap, but that just shows that if we make the assumption
>> that everything is WSGI things don't get ugly. unwrap is only
>> necessary if we don't make the assumption that every subapp is just
>> WSGI, so instead of calling things we'd be doing .get_response on them
>> and if we do that why not simply return them as is? Doesn't wsgiwrap
>> choose to keep the signature to satisfy the cases where it shouldn't
>> be used at all really?
>
> Sure, it's equivalent to req.get_response(), and basically that's all
> the code does, is create a function that calls req.get_response() on
> the WSGI application.
Do we need more code then?
> As to allowing arguments to be added, that isn't really allowed for
> with wsgiwrap any more than webob_wrap (except with stuff like
> urlvars, which isn't on by default and is largely internal to how the
> function wants to consume the request).
Not true, watch me do it.
@wsgiwrap
def demo(req, text):
return Response(text)
@wsgiwrap
def outer_demo(req):
return demo(req, 'foo') # <==
assert Request.blank('/').get_response(outer_demo.wsgi_app).body == 'foo'
And as I said a dozen times before:
Request.blank('/').get_response(demo.wsgi_app)
TypeError: demo() takes exactly 2 arguments (1 given)
> It's just that you can apply
> wsgiwrap to a one-request-argument-taking function and then continue
> to call it like normal, or call it as a wsgi application. I think
> this will encourage a transition from private internal functions
> (which people will always write, with eclectic signatures) to reusable
> small applications (when that is appropriate).
Now I'm impressed by how wrong you are about your own code. Pretty
puzzling really.
--
Ian Bicking | http://blog.ianbicking.org
2009/5/14 Sergey Schetinin <mal...@gmail.com>:
> On Thu, May 14, 2009 at 21:46, Ian Bicking <ia...@colorstudy.com> wrote:
>> On Thu, May 14, 2009 at 1:27 PM, Sergey Schetinin <mal...@gmail.com> wrote:
>>>
>>> Some examples:
>>>
>>> class HostMap(dict):
>>> @webob_wrap
>>> def __call__(self, req):
>>> return self[req.host.split(':')[0]]
>>
>> This would work identically, truly only the name "webob_wrap" would be changed.
>
> OK, let's see
>
> from webob import *
>
> class HostMap(dict):
> @wsgiwrap
> def __call__(self, req):
> return self[req.host.split(':')[0]]
>
> app = HostMap() # <- wsgi app
> app['example.com'] = Response('1')
> app['other.com'] = Response('2')
>
> #print app
> Request.blank('http://example.com/').get_response(app)
OK, this is indeed tricky, even after all the bugs are fixed. You can do:
Request.blank('http://example.com/').get_response(app.__call__.wsgi_app)
Or you can do:
Request.blank('http://example.com/').get_response(wsgify(app).wsgi_app)
I would plan on adding a check for .wsgi_app to get_response (or
rather the underlying call_application), but even that wouldn't work,
because hasattr(app, 'wsgi_app') isn't true.
OTOH, wsgify(app) *does* work reasonably, as the decorator doesn't
mask the rest of the API. Class decorators wouldn't work... but it
might be an interesting thought experiment.
>>>
>>> That's all the code you need to dispatch on hostname.
>>>
>>> app = HostMap() # <- wsgi app
>>> app['example.com'] = wsgi_app1
>>> app['other.com'] = wsgi_app2
>>
>> The way wsgiwrap is written you could add wsgi apps or wrapped apps at
>> each point too.
>
> @wsgiwrap
> def other_app(req):
> return Response('!')
>
> app['other.com'] = other_app
> print Request.blank('http://other.com/').get_response(app.__call__.wsgi_app)
>
> OK, that does work, this doesn't:
>
> app['other.com'] = OtherApp()
> print Request.blank('http://other.com/').get_response(app.__call__.wsgi_app)
>
> File "decorator.py", line 92, in wsgi_app
> return resp(environ, start_response)
> File "decorator.py", line 58, in __call__
> resp = self.func(*args, **kw)
> TypeError: __call__() takes exactly 1 argument (3 given)
Wait, what is OtherApp()? Is this another instance of the __call__ problem?
>>>
>>> Or if you need to serve different content for https clients:
>>>
>>> @webob_middleware
>>> def override_https(req, normal_app, secure_app):
>>> return secure_app if req.scheme == 'https' else normal_app
>>
>> Same deal, except I believe the way I've written it there's no
>> positional arguments, so secure_app would have to be a keyword
>> argument (at least when you instantiate the middleware). That could
>> just be fixed.
>
> @wsgiwrap.middleware
> def override_https(req, normal_app, secure_app):
> return secure_app if req.scheme == 'https' else normal_app
>
> app = override_https(Response('http'), secure_app=Response('https'))
>
> print Request.blank('http://x.com/').get_response(app.wsgi_app)
This works now.
>>> I guess the code will be quite similar, also using get_response
>>> instead of unwrap, but that just shows that if we make the assumption
>>> that everything is WSGI things don't get ugly. unwrap is only
>>> necessary if we don't make the assumption that every subapp is just
>>> WSGI, so instead of calling things we'd be doing .get_response on them
>>> and if we do that why not simply return them as is? Doesn't wsgiwrap
>>> choose to keep the signature to satisfy the cases where it shouldn't
>>> be used at all really?
>>
>> Sure, it's equivalent to req.get_response(), and basically that's all
>> the code does, is create a function that calls req.get_response() on
>> the WSGI application.
>
> Do we need more code then?
No.
>> As to allowing arguments to be added, that isn't really allowed for
>> with wsgiwrap any more than webob_wrap (except with stuff like
>> urlvars, which isn't on by default and is largely internal to how the
>> function wants to consume the request).
>
> Not true, watch me do it.
>
> @wsgiwrap
> def demo(req, text):
> return Response(text)
You could also do this with webob_wrap, and then call demo.func. You
can do things wrong, sure.
> @wsgiwrap
> def outer_demo(req):
> return demo(req, 'foo') # <==
>
> assert Request.blank('/').get_response(outer_demo.wsgi_app).body == 'foo'
>
>
> And as I said a dozen times before:
>
> Request.blank('/').get_response(demo.wsgi_app)
>
> TypeError: demo() takes exactly 2 arguments (1 given)
>
>> It's just that you can apply
>> wsgiwrap to a one-request-argument-taking function and then continue
>> to call it like normal, or call it as a wsgi application. I think
>> this will encourage a transition from private internal functions
>> (which people will always write, with eclectic signatures) to reusable
>> small applications (when that is appropriate).
>
>
> Now I'm impressed by how wrong you are about your own code. Pretty
> puzzling really.
Well, I'm not sure if this discussion is moving forward much. I want
to explore annotation of functions instead of replacing functions with
WSGI versions, because I'm not comfortable with replacement generally,
having used that technique quite a bit. And of course there's nothing
to keep both from existing. At this point it doesn't seem like we're
going to come to an agreement.
The name seems strange, I don't have a better suggestion, but wsgify
doesn't represent what the decorator is doing IMO.
And it would be completely pointless, because it's not about
get_response. We might want to use the app as
urlmap['/somepath'] = app
but we can't.
> OTOH, wsgify(app) *does* work reasonably, as the decorator doesn't
> mask the rest of the API. Class decorators wouldn't work... but it
> might be an interesting thought experiment.
>
>>>>
>>>> That's all the code you need to dispatch on hostname.
>>>>
>>>> app = HostMap() # <- wsgi app
>>>> app['example.com'] = wsgi_app1
>>>> app['other.com'] = wsgi_app2
>>>
>>> The way wsgiwrap is written you could add wsgi apps or wrapped apps at
>>> each point too.
>>
>> @wsgiwrap
>> def other_app(req):
>> return Response('!')
>>
>> app['other.com'] = other_app
>> print Request.blank('http://other.com/').get_response(app.__call__.wsgi_app)
>>
>> OK, that does work, this doesn't:
>>
>> app['other.com'] = OtherApp()
>> print Request.blank('http://other.com/').get_response(app.__call__.wsgi_app)
>>
>> File "decorator.py", line 92, in wsgi_app
>> return resp(environ, start_response)
>> File "decorator.py", line 58, in __call__
>> resp = self.func(*args, **kw)
>> TypeError: __call__() takes exactly 1 argument (3 given)
>
> Wait, what is OtherApp()? Is this another instance of the __call__ problem?
class OtherApp(object):
@wsgiwrap
def __call__(req):
return Response('!')
Yes it's another instance of that problem, which is a problem I keep
talking about and you keep saying doesn't exist.
What's the point of unwrap / reverse then? I see it as adapting WSGI
to new interface, something webob_wrap doesn't need.
>>> As to allowing arguments to be added, that isn't really allowed for
>>> with wsgiwrap any more than webob_wrap (except with stuff like
>>> urlvars, which isn't on by default and is largely internal to how the
>>> function wants to consume the request).
>>
>> Not true, watch me do it.
>>
>> @wsgiwrap
>> def demo(req, text):
>> return Response(text)
>
> You could also do this with webob_wrap, and then call demo.func. You
> can do things wrong, sure.
Please go back and read what I was saying about this -- you were
saying it's not true, but it is true. With wsgify adding arguments is
natural and it will only break when someone else will try to use
.wsgi_app.
With webob_wrap you're returning things, which makes adding
parameters, then calling the thing forgoing the wrapper a very
unnatural thing to do. At least one would wonder what the wrapper was
for AND the default use case will break.
>> @wsgiwrap
>> def outer_demo(req):
>> return demo(req, 'foo') # <==
>>
>> assert Request.blank('/').get_response(outer_demo.wsgi_app).body == 'foo'
>>
>>
>> And as I said a dozen times before:
>>
>> Request.blank('/').get_response(demo.wsgi_app)
>>
>> TypeError: demo() takes exactly 2 arguments (1 given)
>>
>>> It's just that you can apply
>>> wsgiwrap to a one-request-argument-taking function and then continue
>>> to call it like normal, or call it as a wsgi application. I think
>>> this will encourage a transition from private internal functions
>>> (which people will always write, with eclectic signatures) to reusable
>>> small applications (when that is appropriate).
>>
>>
>> Now I'm impressed by how wrong you are about your own code. Pretty
>> puzzling really.
>
> Well, I'm not sure if this discussion is moving forward much. I want
> to explore annotation of functions instead of replacing functions with
> WSGI versions, because I'm not comfortable with replacement generally,
> having used that technique quite a bit. And of course there's nothing
> to keep both from existing. At this point it doesn't seem like we're
> going to come to an agreement.
I understand that you have a preference and I'm OK with wsgify in
webob core, it's none of my business really. What I don't think is OK
is that you say things are identical when they are different in
exactly the ways I said. Where can a discussion move if the issues are
not discussed but are said not to exist? You are OK with them and I'm
OK with that, just please don't say things are identical when they are
not. There were a few more issues I brought up that were just ignored
apparently, which I guess is better :)
I think I've explained my opinion, now it's your call.
Correct, the conversion to a WSGI app would be, unfortunately, a bit
messy... at times it would happen automatically, but at times it would
not, e.g., when interacting with URLMap().
>> Wait, what is OtherApp()? Is this another instance of the __call__ problem?
>
>
> class OtherApp(object):
> @wsgiwrap
> def __call__(req):
> return Response('!')
>
> Yes it's another instance of that problem, which is a problem I keep
> talking about and you keep saying doesn't exist.
I've admitted it exists (otherwise I couldn't have referred to it
there!), and I haven't thought of a good resolution.
>> Well, I'm not sure if this discussion is moving forward much. I want
>> to explore annotation of functions instead of replacing functions with
>> WSGI versions, because I'm not comfortable with replacement generally,
>> having used that technique quite a bit. And of course there's nothing
>> to keep both from existing. At this point it doesn't seem like we're
>> going to come to an agreement.
>
> I understand that you have a preference and I'm OK with wsgify in
> webob core, it's none of my business really. What I don't think is OK
> is that you say things are identical when they are different in
> exactly the ways I said. Where can a discussion move if the issues are
> not discussed but are said not to exist? You are OK with them and I'm
> OK with that, just please don't say things are identical when they are
> not. There were a few more issues I brought up that were just ignored
> apparently, which I guess is better :)
I'm not sure what I haven't addressed. I've actually repeated several
of the examples in the tests
(http://svn.pythonpaste.org/Paste/WebOb/branches/ianb-decorator-experiment/tests/test_decorator.txt)
and indeed they have been helpful in considering the implications of
the design choices. And there's still one particular awkwardness with
the decorating of a __call__ method, which doesn't expose .wsgi_app on
the parent object. Is there another case in particular that you think
I haven't addressed?
Right. In other words there's a barrier between the wsgified functions
and generic WSGI apps that might combine them. It's not terribly big
but it's there. Same thing about deferring work to WSGI apps from
wsgified functions -- you don't do it the same way as you defer work
to other wsgified functions. And that means you can't easily replace
one thing with another because sometimes it's a call, sometimes it's a
return and sometimes it's a messy attribute access.
>>> Wait, what is OtherApp()? Is this another instance of the __call__ problem?
>>
>>
>> class OtherApp(object):
>> @wsgiwrap
>> def __call__(req):
>> return Response('!')
>>
>> Yes it's another instance of that problem, which is a problem I keep
>> talking about and you keep saying doesn't exist.
>
> I've admitted it exists (otherwise I couldn't have referred to it
> there!), and I haven't thought of a good resolution.
Well, maybe I misunderstood, but the examples were meant to illustrate
how things can be lean but will immediately get messy with wsgify.
Your next response was that everything will work identically, which it
seems to me kinda ignores that there's an issue with all this
additional unwrapping, rewrapping etc that goes on with wsgify. For
example, when using wsgified app as WSGI you need to know if it was
implemented as a function or a class (or, to hide that detail you need
additional factory function) -- nasty.
>>> Well, I'm not sure if this discussion is moving forward much. I want
>>> to explore annotation of functions instead of replacing functions with
>>> WSGI versions, because I'm not comfortable with replacement generally,
>>> having used that technique quite a bit. And of course there's nothing
>>> to keep both from existing. At this point it doesn't seem like we're
>>> going to come to an agreement.
>>
>> I understand that you have a preference and I'm OK with wsgify in
>> webob core, it's none of my business really. What I don't think is OK
>> is that you say things are identical when they are different in
>> exactly the ways I said. Where can a discussion move if the issues are
>> not discussed but are said not to exist? You are OK with them and I'm
>> OK with that, just please don't say things are identical when they are
>> not. There were a few more issues I brought up that were just ignored
>> apparently, which I guess is better :)
>
> I'm not sure what I haven't addressed. I've actually repeated several
> of the examples in the tests
> (http://svn.pythonpaste.org/Paste/WebOb/branches/ianb-decorator-experiment/tests/test_decorator.txt)
> and indeed they have been helpful in considering the implications of
> the design choices. And there's still one particular awkwardness with
> the decorating of a __call__ method, which doesn't expose .wsgi_app on
> the parent object. Is there another case in particular that you think
> I haven't addressed?
The mutating signature issue.
The need to know about implementation of called apps in default use
case / making assumptions.
Introduction of a new interface.
If the wsgify app returns another non-Response WSGI app, the caller
has to be ready for non-Response return value -- developers will
normally won't bother and that will make refactoring / customization
harder.
Even if someone will try to work strictly on WSGI level, parts can't
be replaced -- if you access self.subapp.wsgi_app -- it can't be
replaced with arbitrary WSGI app.
Which adds up to a walled garden.
Surely one can live with all that, but is that a justifiable cost for
getting to call things instead of returning them? :)
Hey! That's not WSGI middleware, that's wsgify middleware. See what
I'm saying about introducing a new interface?
Also:
class Simple(object):
@wsgify
def __call__(req):
return Response()
app = catch(Simple(), catchers={})
That will break. You can try to work around it but I'd say just throw it out.
def __call__(self, *args):
if len(args) == 1:
return self.func(args[0])
if len(args) == 2:
env, start = args
# same thing as now after this point
I'm against this change but I think it's a reasonable compromise.
Still can't see a use for the wsgify.reverse -- get_response is fine.