Skip to content
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

Merged
merged 15 commits into from
Feb 1, 2017

Conversation

gabisurita
Copy link
Member

@gabisurita gabisurita commented Jan 29, 2017

Related to #1033

This PR changes the schema-viewset interaction:

  • Request schemas are now explicitly declared for each method-endpoint type
  • We introduce deferred fields on schemas that allow binding.
    e.g: we now have a generic RequestSchema where we can bind different QueryString Schemas.

Todos

  • Add tests.
  • Add a changelog entry.
  • Wait for colander release

r? @glasserc @leplatrem

@gabisurita gabisurita changed the title Split request schemas for each method/resource type Split request schemas for each method/endpoint type Jan 29, 2017
Copy link
Contributor

@leplatrem leplatrem left a 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!


@staticmethod
def schema_type():
return colander.Mapping(unknown='preserve')


class PatchHeaderSchema(HeaderSchema):
"""Header schema used with patch requests."""
Copy link
Contributor

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

Copy link
Member Author

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?

Copy link
Contributor

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


@colander.instantiate()
class querystring(CollectionQuerySchema, GetQuerySchema):
pass
Copy link
Contributor

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

Copy link
Member Author

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

Copy link
Contributor

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 :)

args['schema'] = schema
else:
args['schema'] = RequestSchema()
request_schema.add(record_schema)
Copy link
Contributor

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

Copy link
Member Author

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?

Copy link
Contributor

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 :(


# Instantiate request schema
request_schema = request_schema()

if method.lower() in map(str.lower, self.validate_schema_for):
Copy link
Member Author

@gabisurita gabisurita Jan 30, 2017

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

@gabisurita
Copy link
Member Author

gabisurita commented Jan 30, 2017

The class declarations seem a bit verbose but I don't have any alternative proposition. Except maybe have some kind of helper

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.

@gabisurita
Copy link
Member Author

Also, do we have to keep SimpleSchema, PartialSchema and StrictSchema on the viewset?

@leplatrem
Copy link
Contributor

better to make this explicit.

Works for me!

keep SimpleSchema, PartialSchema and StrictSchema on the viewset?

If not used, and not mentionned in docs, I'm not again removing them :)

@gabisurita
Copy link
Member Author

gabisurita commented Jan 30, 2017

@leplatrem, I tried this different approach using schema binding, I think it makes the viewset a bit simpler, what do you think?

@gabisurita gabisurita force-pushed the split-request-schemas branch from 2d2dc5c to 20d3f6a Compare January 30, 2017 21:05
Copy link
Contributor

@leplatrem leplatrem left a 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?

@@ -278,6 +289,62 @@ def deserialize(self, cstruct=colander.null):
return values


class CollectionQuerySchema(QuerySchema):
"""Querystring validated fields used with collections."""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: validated fields --> schema



class RecordGetQuerySchema(QuerySchema):
"""Querystring validated fields for GET record requests."""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto



class CollectionGetQuerySchema(CollectionQuerySchema):
"""Querystring validated fields for GET collection requests."""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

return colander.Mapping(unknown='raise')

def bind(self, data=None, permissions=None, **kwargs):
binded = super(RecordSchema, self).bind(**kwargs)
Copy link
Contributor

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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍


def setUp(self):
self.schema = schema.RequestSchema().bind(
querystring=schema.RecordGetQuerySchema())
Copy link
Contributor

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


class RecordSchema(colander.MappingSchema):

data = colander.deferred
Copy link
Contributor

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?

def bind(self, header=None, querystring=None, **kwargs):
binded = super(RequestSchema, self).bind(**kwargs)
if header:
binded['header'] = header.bind(**kwargs)
Copy link
Contributor

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.

"""Base schema for kinto requests."""

header = HeaderSchema()
querystring = QuerySchema()
Copy link
Contributor

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?

Copy link
Member Author

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.

Copy link
Contributor

@glasserc glasserc left a 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.


def setUp(self):
self.schema = schema.RequestSchema().bind(
querystring=schema.RecordGetQuerySchema())
Copy link
Contributor

@glasserc glasserc Jan 31, 2017

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(),
Copy link
Contributor

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?

Copy link
Member Author

@gabisurita gabisurita Feb 1, 2017

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'>

Copy link
Contributor

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()
Copy link
Contributor

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?

Copy link
Member Author

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.

resource = mock.sentinel.resource
resource.schema = ResourceSchema
resource.permissions = ('read', 'write')
args = viewset.collection_arguments(resource, 'GET')
Copy link
Contributor

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.)

Copy link
Member Author

@gabisurita gabisurita Feb 1, 2017

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.

Copy link
Member Author

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.

@gabisurita
Copy link
Member Author

gabisurita commented Jan 31, 2017

I just don't know if bind() was made for such a use case. Maybe we can ask somewhere?

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

@gabisurita gabisurita force-pushed the split-request-schemas branch from 2941b5c to 6c44c2c Compare February 1, 2017 03:37
@leplatrem
Copy link
Contributor

This is excellent work. You never stop raising the bar 👍 👍

@gabisurita gabisurita merged commit c04a62a into Kinto:master Feb 1, 2017
@gabisurita gabisurita deleted the split-request-schemas branch February 23, 2017 00:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants