Account Options

  1. Sign in
The old Google Groups will be going away soon.
Switch to the new Google Groups.
Google Groups Home
« Groups Home
Validation inconsistencies and custom routes
There are currently too many topics in this group that display first. To make this topic appear first, remove this option from another topic.
There was an error processing your request. Please try again.
flag
  2 messages - Collapse all  -  Translate all to Translated (View all originals)
The group you are posting to is a Usenet group. Messages posted to this group will make your email address visible to anyone on the Internet.
Your reply message has not been sent.
Your post was successful
 
From:
To:
Cc:
Followup To:
Add Cc | Add Followup-to | Edit Subject
Subject:
Validation:
For verification purposes please type the characters you see in the picture below or the numbers you hear by clicking the accessibility icon. Listen and type the numbers you hear
 
Raphael Slinckx  
View profile  
 More options Feb 19 2009, 5:55 am
From: Raphael Slinckx <rslin...@gmail.com>
Date: Thu, 19 Feb 2009 02:55:17 -0800 (PST)
Local: Thurs, Feb 19 2009 5:55 am
Subject: Validation inconsistencies and custom routes
I was playing the other day with validation and custom routes, here
are some comments

1) Validation
1.1) Positional parameters validation
Using @validate on an exposed method, it seems I can't validate
positional parameters, and errors are raised when the call has more
than one value defined per parameter. Example:

First without validator

@expose()
def foo(self, a, b, c=None, d=None):
    return "a=%r b=%r c=%r d=%r" % (a, b, c, d)

/foo/x/y/?a=z&b=w    -> foo() got multiple values for keyword argument
'a'
/foo/x/y/?c=z&d=w    -> a=u'x' b=u'y' c=u'z' d=u'w'
/foo/x/y/z/w         -> a=u'x' b=u'y' c=u'z' d=u'w'
/foo/x?b=y           -> a=u'x' b=u'y' c=None d=None
/foo?a=x&b=y&c=z&d=w -> a=u'x' b=u'y' c=u'z' d=u'w'

These results are what would be expected since a/b are mandatory, and
c/d are optional and default to None.
If I call it with two values for a (one in the positional part,
another in the query string) then it errors out
because we can't possibly disambiguate

Now, let's try to add a validator to the foo() method

@validate(validators=dict(
    a=validators.String(), b=validators.String(),
    c=validators.String(), d=validators.String(),
))
@expose()
def foo(self, a, b, c=None, d=None):
    return "a=%r b=%r c=%r d=%r" % (a, b, c, d)

/foo/x/y/?a=z&b=w    -> foo() got multiple values for keyword argument
'a'
/foo/x/y/?c=z&d=w    -> foo() got multiple values for keyword argument
'a'
/foo/x/y/z/w         -> foo() got multiple values for keyword argument
'a'
/foo/x?b=y           -> foo() got multiple values for keyword argument
'a'
/foo?a=x&b=y&c=z&d=w -> a=u'x' b=u'y' c=u'z' d=u'w'

Now that's surprising. What happens is the following code (in tg/
controllers.py:_perform_validate):

for field, validator in validation.validators.iteritems():
    try:
        validator.to_python(params.get(field))
        new_params[field] = validator.to_python(params.get(field))
    # catch individual validation errors into the errors dictionary
    except formencode.api.Invalid, inv:
        errors[field] = inv

Meaning that for each defined validators we are going to insert a new
keyword argument with the result of the validation.
(By the way is it normal that the validator is run twice ?)

The result is that when the controller method is called with /foo/x?
b=y what really happens is
foo(*remainder, **kwargs) -> foo(*['x'], **{'a': '', 'b': u'y', 'c':
'', 'd': ''})
resulting in 'a' being defined twice in the method call.

This means in the current situation that you can only use validators
for keyword arguments.
It's also worth mentioning that positional arguments are simply
ignored in the validation process, so there's no way to validate them
I can't for example define a pagination using /foo/1, /foo/2, /foo/
3... and at the same time validate the page number as an Int.

I believe the way to fix this is to inspect the exposed method's
signature and match validators with arguments name, pylons seems to do
this,
but i don't know if there are any implications...

1.2) Default values with validation
First:

@validate(validators=dict(
    a=validators.String(), b=validators.Int()
))
@expose()
def foo(self, a=None, b=None):
    return "a=%r b=%r" % (a, b)

/foo         -> a=''  b=None
/foo?a=x     -> a='x' b=None
/foo?a=x&b=0 -> a='x' b=0

Then:
@validate(validators=dict(
    a=validators.String(), b=validators.Int()
))
@expose()
def foo(self, a="bar", b=1):
    return "a=%r b=%r" % (a, b)

/foo         -> a=''  b=None
/foo?a=x     -> a='x' b=None
/foo?a=x&b=0 -> a='x' b=0

Basically default values are not taken into account, and will be
replaced by the validator's to_python result
which for String() is '' and for Int() is None

While this is 'normal' given the code i showed above for
_perform_validate, it might be unexpected and deserves at least
documentation
The 'correct' way to handle default values is to use:

@validate(validators=dict(
    a=validators.String(if_empty='bar'), b=validators.Int(if_empty=1)
))
@expose()
def foo(self, a=None, b=None):
    return "a=%r b=%r" % (a, b)

/foo         -> a='bar' b=1
/foo?a=x     -> a='x'   b=1
/foo?a=x&b=0 -> a='x'   b=0

1.3) @validate default error_handling when using validators=dict()
when using @validate(validators=dict(...)) when there is a validation
error and no error_handler explicitely defined, the
default behavior is to call the method regardless with unvalidated
arguments, which is surprising. The only way to detect that errors
happened
is to look at 'pylons.c.form_errors' which is then a non-empty
dictionary. Maybe the default should be to raise an Exception
triggering a 500 internal error ?
or some other form of error preventing the controller code to be
executed with bogus arguments

@validate(validators=dict(
    a=validators.Int()
))
@expose()
def foo(self, a=None):
    return "a=%r" % a

/foo     -> a=None
/foo?a=x -> a='x' ... unexpected, but can be detected by inspecting
pylons.c.form_errors
/foo?a=1 -> a=1

2) Routes
It is advertised that TG2 supports custom routes (using the routes
package) as well as a default object-dispatch mechanism.
The object dispatch is actually implemented as a stub, catch-all
route, that redirects to a special controller.

This controller (TGController) inherits from pylon's WSGIController
and overrides _perform_call to do the dispatch magic
instead of letting pylons do the 'regular' route dispatch.

def _perform_call(self, func, args):
    controller, remainder, params = self._get_routing_info(args.get
('url'))
    func_name = func.__name__
    if func_name == '__before__' or func_name == '__after__':
        if hasattr(controller.im_class, '__before__'):
            return controller.im_self.__before__(*args)
        if hasattr(controller.im_class, '__after__'):
            return controller.im_self.__before__(*args)
        return
    return DecoratedController._perform_call(
        self, controller, params, remainder=remainder)

As we can see tg2 ignores the 'args' coming from pylons and assumes
there's going to be only the 'url' key then goes on with object
dispatch.
This means that I can't use a custom route leading to a method inside
a subclass of TGController (or BaseController as defined in a
quickstarted project)
and if I do it, all routes arguments will be ignored, for example in
the route mapping in app_cfg:

map.connect('foo/:page/:filter', controller="root", action="foo"
page=1, filter="all")
map.connect('*url', controller='root', action="routes_placeholder")

calling /foo/1/2 will result in foo(*['1', '2'], **{}) being called

Next I tried to bypass the object dispatch using a controller that
didn't inherit from 'BaseController(TGController)' but from
'Controller' (both defined in $project/lib/base.py).
It turns out that 'Controller' inherits from 'object' meaning that I
simply can't use it since Pylons expect a __call__ method on a
controller.

in $project/controllers/foo.py:

from tg import expose
from $project.lib.base import Controller
class FooController(Controller):
        @expose()
        def foo(self):
                return ""

Calling /foo leads to:
File '/home/kikidonk/Whatever/tg2/lib/python2.5/site-packages/
Routes-1.10.2-py2.5.egg/routes/middleware.py', line 118 in __call__
  response = self.app(environ, start_response)
File '/home/kikidonk/Whatever/tg2/lib/python2.5/site-packages/
Pylons-0.9.7rc4-py2.5.egg/pylons/wsgiapp.py', line 117 in __call__
  response = self.dispatch(controller, environ, start_response)
File '/home/kikidonk/Whatever/tg2/lib/python2.5/site-packages/
Pylons-0.9.7rc4-py2.5.egg/pylons/wsgiapp.py', line 316 in dispatch
  return controller(environ, start_response)
TypeError: 'FooController' object is not callable

In tg2's tg/controllers.py there is a better parent class i could use
(from which TGController inherits): DecoratedController which also
seems
to perform the decoration magic for expose/validate, etc
I'm guessing that the base.py's 'Controller' should inherit from that
DecoratedController to work properly.

The problem if I do that is that now, pylons will try to call
__before__ and __after__ (since they are both defined in that class)
and those methods have no decoration, leading to an error:

from tg.controllers import DecoratedController
from tg import expose
class FooController(DecoratedController):
        @expose()
        def foo(self):
                return ""

File '/home/kikidonk/Whatever/tg2/lib/python2.5/site-packages/
Pylons-0.9.7rc4-py2.5.egg/pylons/wsgiapp.py', line 117 in __call__
  response = self.dispatch(controller, environ, start_response)
File '/home/kikidonk/Whatever/tg2/lib/python2.5/site-packages/
Pylons-0.9.7rc4-py2.5.egg/pylons/wsgiapp.py', line 316 in dispatch
  return controller(environ, start_response)
File '/home/kikidonk/Whatever/tg2/lib/python2.5/site-packages/
Pylons-0.9.7rc4-py2.5.egg/pylons/controllers/core.py', line 200 in
__call__
  response = self._inspect_call(self.__before__)
File '/home/kikidonk/Whatever/tg2/lib/python2.5/site-packages/
Pylons-0.9.7rc4-py2.5.egg/pylons/controllers/core.py', line 95 in
_inspect_call
  result = self._perform_call(func, args)
File '/home/kikidonk/Whatever/tg2/lib/python2.5/site-packages/
TurboGears2-2.0b5.1-py2.5.egg/tg/controllers.py', line 109 in
_perform_call
  controller.decoration.run_hooks('before_validate', remainder,
AttributeError: 'function' object has no attribute 'decoration'

where function is __before__ (<bound method FooController.__before__
of <$project.controllers.foo.FooController object at  ...
0xa6e074c>>)

I must admit i'm a bit puzzled here on what to do, probably removing
__before__ and __after__ would work, i'm not sure...
I'm tempted to conclude that I can't currently have a controller that
uses TG2's decorators, and at the same time uses completely custom
routes ...

read more »


 
You must Sign in before you can post messages.
To post a message you must first join this group.
Please update your nickname on the subscription settings page before posting.
You do not have the permission required to post.
Mark Ramm  
View profile  
 More options Feb 19 2009, 10:26 pm
From: Mark Ramm <mark.mchristen...@gmail.com>
Date: Thu, 19 Feb 2009 22:26:28 -0500
Local: Thurs, Feb 19 2009 10:26 pm
Subject: Re: [TurboGears] Validation inconsistencies and custom routes

Wow, this is quite a thorough post!

Yea, validators and the @validate decorator are designed to validate user
input from get params, or post values, and those are always keyword
arguments.   We're not validating the positional params because those come
from the routing process, and the standard TG idiom has been to handle
conversion/validation of those params internally to the controller method,
though I do think that perhaps we could do this better in TG.

I don't know if a @validate_routes decorator is exactly the right API, but I
like the direction you're going.   Perhaps we could open a ticket for 2.1 to
make something like this standard.  Unfortunately it's too late for changes
to 2.0 since we're in feature freeze, and this is actually a new feature,
not a bug or backwards incompatibilty issue.

As for the issue of validation failure calling the method with unvalidated
arguments, that's only the case if you don't also define an error handler,
which you mention.   There was some discussion of this in the past, and it
was decided to keep this behavior, but if we can resolve those issues, this
is something that we may also want to clean up for 2.1.

And I'm not exactly sure what's happening to you with DecoratedController
not working, but that's likely a bug which should be filed against 2.0rc1 so
that we can resolve it before releasing the release candidate.   It may
already be fixed in trunk, I remember seeing something like it, but can't
find the discussion at the moment.

...

read more »


 
You must Sign in before you can post messages.
To post a message you must first join this group.
Please update your nickname on the subscription settings page before posting.
You do not have the permission required to post.
End of messages
« Back to Discussions « Newer topic     Older topic »