-
Notifications
You must be signed in to change notification settings - Fork 422
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Split request schemas for each method/endpoint type #1047
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's pretty good!
The class declarations seem a bit verbose but I don't have any alternative proposition. Except maybe have some kind of helper:
default_collection_arguments = {
'schema': request_schema(querystring=CollectionQuerySchema),
}
But I'm not sure...
I think the comments can be improved as I commented in some place below..
GG!
kinto/core/resource/schema.py
Outdated
|
||
@staticmethod | ||
def schema_type(): | ||
return colander.Mapping(unknown='preserve') | ||
|
||
|
||
class PatchHeaderSchema(HeaderSchema): | ||
"""Header schema used with patch requests.""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nitpick: http verbs are usually written upercase
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you think the verb on the class name should be uppercase as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, in comments at least
kinto/core/resource/schema.py
Outdated
|
||
@colander.instantiate() | ||
class querystring(CollectionQuerySchema, GetQuerySchema): | ||
pass |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had never seen this :) Interesting... It took me a while to figure out why _fields
was not listed in CollectionQuerySchema
!
Maybe there is a matter of wording or comments to really distinguish : general (both endpoints), plural and single enpoints
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
_fields
is not accepted in plural DELETE, so that's why it's not there. I've tried to omit "General" and just add the Collection or Record according to each type. Turns out Record was not used. :P
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't hesitate to add as many comments as you can for this kind of details :)
kinto/core/resource/viewset.py
Outdated
args['schema'] = schema | ||
else: | ||
args['schema'] = RequestSchema() | ||
request_schema.add(record_schema) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With all the explicit declarations that we do elsewhere, I wish those few lines become extremely simple
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An interesting approach IMO would be to handle the record_schema
binding directly on the schema (instead of the viewset), what do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't understand your proposition :(
kinto/core/resource/viewset.py
Outdated
|
||
# Instantiate request schema | ||
request_schema = request_schema() | ||
|
||
if method.lower() in map(str.lower, self.validate_schema_for): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With the current implementation it still is, the body
field is still added just on the viewset. I've tried to preserve this for backward compatibility, but we can indeed solve this on the schemas.
Edit: Sorry, I've deleted @leplatrem comment by accident instead of mine, it asked if validate_schema_for
was still needed
An interesting approach for that would be to set the header, query and body on when instantiating a request (setting them on the init method). e.g: request_schema = RequestSchema(header=HeaderSchema(), querystring=QuerySchema()) But we still would have to do a bunch of imports. But I'm also not sure about that. I tend to think it's probably better to make this explicit. |
Also, do we have to keep SimpleSchema, PartialSchema and StrictSchema on the viewset? |
Works for me!
If not used, and not mentionned in docs, I'm not again removing them :) |
@leplatrem, I tried this different approach using schema binding, I think it makes the viewset a bit simpler, what do you think? |
2d2dc5c
to
20d3f6a
Compare
…/kinto into split-request-schemas
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The whole result is globally better!
I just don't know if bind()
was made for such a use case. Maybe we can ask somewhere?
kinto/core/resource/schema.py
Outdated
@@ -278,6 +289,62 @@ def deserialize(self, cstruct=colander.null): | |||
return values | |||
|
|||
|
|||
class CollectionQuerySchema(QuerySchema): | |||
"""Querystring validated fields used with collections.""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nitpick: validated fields
--> schema
kinto/core/resource/schema.py
Outdated
|
||
|
||
class RecordGetQuerySchema(QuerySchema): | ||
"""Querystring validated fields for GET record requests.""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto
kinto/core/resource/schema.py
Outdated
|
||
|
||
class CollectionGetQuerySchema(CollectionQuerySchema): | ||
"""Querystring validated fields for GET collection requests.""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ditto
kinto/core/resource/schema.py
Outdated
return colander.Mapping(unknown='raise') | ||
|
||
def bind(self, data=None, permissions=None, **kwargs): | ||
binded = super(RecordSchema, self).bind(**kwargs) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nitpick: binded
-> bound
args['schema'] = RequestSchema() | ||
request_schema = args.get('schema', RequestSchema()) | ||
record_schema = self.get_record_schema(resource_cls, method) | ||
request_schema = request_schema.bind(body=record_schema) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
tests/core/resource/test_schema.py
Outdated
|
||
def setUp(self): | ||
self.schema = schema.RequestSchema().bind( | ||
querystring=schema.RecordGetQuerySchema()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nitpick: prefer an intermediary variable to avoid indentation
kinto/core/resource/schema.py
Outdated
|
||
class RecordSchema(colander.MappingSchema): | ||
|
||
data = colander.deferred |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a comment to explain that we decide later what to put in data based on the resource schema?
kinto/core/resource/schema.py
Outdated
def bind(self, header=None, querystring=None, **kwargs): | ||
binded = super(RequestSchema, self).bind(**kwargs) | ||
if header: | ||
binded['header'] = header.bind(**kwargs) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we really want to support that .bind(**kwargs)
. I find it cool and flexible, but it looks like we don't use it and it does not ease the understanding of the code.
kinto/core/resource/schema.py
Outdated
"""Base schema for kinto requests.""" | ||
|
||
header = HeaderSchema() | ||
querystring = QuerySchema() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are they not deferred in this case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because they have a default.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall I like this. It makes the schemas feel more flexible, modular, and composable, and it's certainly better IMHO than mutating them.
tests/core/resource/test_schema.py
Outdated
|
||
def setUp(self): | ||
self.schema = schema.RequestSchema().bind( | ||
querystring=schema.RecordGetQuerySchema()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not crazy about seeing exactly the same code to produce a schema that's similar to the one we use in real code. We discussed this on Hangouts, we decided that the best approach would be to change these tests to either be tests of the bind
method, or tests of the schema that are actually being used in the viewsets.
|
||
self.assertDictEqual( | ||
arguments, | ||
{ | ||
'schema': mocked(), | ||
'schema': mocked().bind(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need to put bind()
on our mocks?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the return value if we call bind()
on a mocked schema. If we want to avoid this, we should also mock patch the bind return value. e.g.
>>> m = mock.MagicMock()
>>> m.method()
<MagicMock name='mock.method()' id='4417348560'>
And a proposed solution:
>>> m = mock.MagicMock()
>>> m.method = lambda: m
>>> m.method()
<MagicMock id='4417096080'>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I had misread this test the first time around!
viewset = ViewSet( | ||
validate_schema_for=('GET', ) | ||
) | ||
viewset = ViewSet() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems like validate_schema_for
isn't useful any more, but I can't tell if we're removing it in this PR or not. I see a couple places in the tests like this where it's being removed, and I see the class attribute is being removed from ViewSet
, but I don't see any code that uses this attribute. Are we removing it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, we are. It was used to tell if we added a body to the request schema or not, but it doesn't make sense anymore as we split the requests into RequestSchema
and PayloadRequestSchema
.
We could consider this a breaking change because it can be changed from a resource, but I can't see any reason why someone would change this attribute, because probably the requests would fail if it was different.
tests/core/resource/test_viewset.py
Outdated
resource = mock.sentinel.resource | ||
resource.schema = ResourceSchema | ||
resource.permissions = ('read', 'write') | ||
args = viewset.collection_arguments(resource, 'GET') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need something from mock.sentinel
instead of just a MagicMock
? (Above, in test_schema_is_added_when_uppercase_method_matches
, you turn a sentinel
into an ordinary MagicMock
.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It failed on the second bind as one call from colander that inserts a node would not accept a mock object, so I've set the ResourceSchema
manually. But I'll try to find a better approach.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Anyways, it's not a problem anymore with the new bind implementation.
I've asked #pyramid channel on IRC and they said that it was, and that there should be an easier way to do it. I've submitted a patch to colander which should be on the next release. Pylons/colander#280 |
…nstead of overwriting bind
2941b5c
to
6c44c2c
Compare
This is excellent work. You never stop raising the bar 👍 👍 |
Related to #1033
This PR changes the schema-viewset interaction:
e.g: we now have a generic RequestSchema where we can bind different QueryString Schemas.
Todos
r? @glasserc @leplatrem