Skip to content

Commit bc4f462

Browse files
authored
Merge branch 'master' into patch-1
2 parents e84f284 + 9cd2cf8 commit bc4f462

File tree

9 files changed

+85
-23
lines changed

9 files changed

+85
-23
lines changed

README.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ With Flask-RESTX, you only import the api instance to route and document your en
7575
ns = api.namespace('todos', description='TODO operations')
7676
7777
todo = api.model('Todo', {
78-
'id': fields.Integer(readOnly=True, description='The task unique identifier'),
78+
'id': fields.Integer(readonly=True, description='The task unique identifier'),
7979
'task': fields.String(required=True, description='The task details')
8080
})
8181

doc/quickstart.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ You can also match parts of the path as variables to your resource methods.
225225
If a request does not match any of your application's endpoints,
226226
Flask-RESTX will return a 404 error message with suggestions of other
227227
endpoints that closely match the requested endpoint.
228-
This can be disabled by setting ``ERROR_404_HELP`` to ``False`` in your application config.
228+
This can be disabled by setting ``RESTX_ERROR_404_HELP`` to ``False`` in your application config.
229229
230230
231231
Argument Parsing

flask_restx/api.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import re
1010
import six
1111
import sys
12+
import warnings
1213

1314
from collections import OrderedDict
1415
from functools import wraps, partial
@@ -143,10 +144,10 @@ def __init__(
143144
self._default_error_handler = None
144145
self.tags = tags or []
145146

146-
self.error_handlers = {
147+
self.error_handlers = OrderedDict({
147148
ParseError: mask_parse_error_handler,
148149
MaskError: mask_error_handler,
149-
}
150+
})
150151
self._schema = None
151152
self.models = {}
152153
self._refresolver = None
@@ -219,6 +220,7 @@ def init_app(self, app, **kwargs):
219220
else:
220221
self.blueprint = app
221222

223+
222224
def _init_app(self, app):
223225
"""
224226
Perform initialization actions with the given :class:`flask.Flask` object.
@@ -250,6 +252,16 @@ def _init_app(self, app):
250252
app.config.setdefault("RESTX_MASK_SWAGGER", True)
251253
app.config.setdefault("RESTX_INCLUDE_ALL_MODELS", False)
252254

255+
# check for deprecated config variable names
256+
if "ERROR_404_HELP" in app.config:
257+
app.config['RESTX_ERROR_404_HELP'] = app.config['ERROR_404_HELP']
258+
warnings.warn(
259+
"'ERROR_404_HELP' config setting is deprecated and will be "
260+
"removed in the future. Use 'RESTX_ERROR_404_HELP' instead.",
261+
DeprecationWarning
262+
)
263+
264+
253265
def __getattr__(self, name):
254266
try:
255267
return getattr(self.default_namespace, name)
@@ -503,11 +515,11 @@ def endpoint(self, name):
503515
@property
504516
def specs_url(self):
505517
"""
506-
The Swagger specifications absolute url (ie. `swagger.json`)
518+
The Swagger specifications relative url (ie. `swagger.json`)
507519
508520
:rtype: str
509521
"""
510-
return url_for(self.endpoint("specs"), _external=True)
522+
return url_for(self.endpoint("specs"))
511523

512524
@property
513525
def base_url(self):
@@ -547,7 +559,7 @@ def __schema__(self):
547559

548560
@property
549561
def _own_and_child_error_handlers(self):
550-
rv = {}
562+
rv = OrderedDict()
551563
rv.update(self.error_handlers)
552564
for ns in self.namespaces:
553565
for exception, handler in six.iteritems(ns.error_handlers):
@@ -709,7 +721,7 @@ def handle_error(self, e):
709721

710722
elif (
711723
code == HTTPStatus.NOT_FOUND
712-
and current_app.config.get("ERROR_404_HELP", True)
724+
and current_app.config.get("RESTX_ERROR_404_HELP", True)
713725
and include_message_in_response
714726
):
715727
data["message"] = self._help_on_404(data.get("message", None))

flask_restx/fields.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ def __init__(
156156
self.description = description
157157
self.required = required
158158
self.readonly = readonly
159-
self.example = example or self.__schema_example__
159+
self.example = example if example is not None else self.__schema_example__
160160
self.mask = mask
161161

162162
def format(self, value):

flask_restx/model.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -132,12 +132,14 @@ class RawModel(ModelBase):
132132
133133
:param str name: The model public name
134134
:param str mask: an optional default model mask
135+
:param bool strict: validation should raise error when there is param not provided in schema
135136
"""
136137

137138
wrapper = dict
138139

139140
def __init__(self, name, *args, **kwargs):
140141
self.__mask__ = kwargs.pop("mask", None)
142+
self.__strict__ = kwargs.pop("strict", False)
141143
if self.__mask__ and not isinstance(self.__mask__, Mask):
142144
self.__mask__ = Mask(self.__mask__)
143145
super(RawModel, self).__init__(name, *args, **kwargs)
@@ -160,15 +162,18 @@ def _schema(self):
160162
if getattr(field, "discriminator", False):
161163
discriminator = name
162164

163-
return not_none(
164-
{
165-
"required": sorted(list(required)) or None,
166-
"properties": properties,
167-
"discriminator": discriminator,
168-
"x-mask": str(self.__mask__) if self.__mask__ else None,
169-
"type": "object",
170-
}
171-
)
165+
definition = {
166+
"required": sorted(list(required)) or None,
167+
"properties": properties,
168+
"discriminator": discriminator,
169+
"x-mask": str(self.__mask__) if self.__mask__ else None,
170+
"type": "object",
171+
}
172+
173+
if self.__strict__:
174+
definition['additionalProperties'] = False
175+
176+
return not_none(definition)
172177

173178
@cached_property
174179
def resolved(self):
@@ -240,6 +245,7 @@ def __deepcopy__(self, memo):
240245
self.name,
241246
[(key, copy.deepcopy(value, memo)) for key, value in iteritems(self)],
242247
mask=self.__mask__,
248+
strict=self.__strict__,
243249
)
244250
obj.__parents__ = self.__parents__
245251
return obj

flask_restx/namespace.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import inspect
55
import warnings
66
import logging
7-
from collections import namedtuple
7+
from collections import namedtuple, OrderedDict
88

99
import six
1010
from flask import request
@@ -57,7 +57,7 @@ def __init__(
5757
self.urls = {}
5858
self.decorators = decorators if decorators else []
5959
self.resources = [] # List[ResourceRoute]
60-
self.error_handlers = {}
60+
self.error_handlers = OrderedDict()
6161
self.default_error_handler = None
6262
self.authorizations = authorizations
6363
self.ordered = ordered
@@ -162,14 +162,17 @@ def add_model(self, name, definition):
162162
api.models[name] = definition
163163
return definition
164164

165-
def model(self, name=None, model=None, mask=None, **kwargs):
165+
def model(self, name=None, model=None, mask=None, strict=False, **kwargs):
166166
"""
167167
Register a model
168168
169+
:param bool strict - should model validation raise error when non-specified param
170+
is provided?
171+
169172
.. seealso:: :class:`Model`
170173
"""
171174
cls = OrderedModel if self.ordered else Model
172-
model = cls(name, model, mask=mask)
175+
model = cls(name, model, mask=mask, strict=strict)
173176
model.__apidoc__.update(kwargs)
174177
return self.add_model(name, model)
175178

tests/test_errors.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -480,7 +480,7 @@ def test_handle_smart_errors(self, app):
480480
assert response.status_code == 404
481481
assert "did you mean /foo ?" in response.data.decode()
482482

483-
app.config["ERROR_404_HELP"] = False
483+
app.config["RESTX_ERROR_404_HELP"] = False
484484

485485
response = api.handle_error(NotFound())
486486
assert response.status_code == 404

tests/test_fields.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,14 @@ def test_with_default(self):
298298
assert not field.required
299299
assert field.__schema__ == {"type": "boolean", "default": True}
300300

301+
def test_with_example(self):
302+
field = fields.Boolean(default=True, example=False)
303+
assert field.__schema__ == {
304+
"type": "boolean",
305+
"default": True,
306+
"example": False,
307+
}
308+
301309
@pytest.mark.parametrize(
302310
"value,expected",
303311
[

tests/test_namespace.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# -*- coding: utf-8 -*-
22
from __future__ import unicode_literals
3+
import re
34

45
import flask_restx as restx
56

@@ -143,3 +144,35 @@ def post(self):
143144
client.post_json("/apples/validation/", data)
144145

145146
assert Payload.payload == data
147+
148+
def test_api_payload_strict_verification(self, app, client):
149+
api = restx.Api(app, validate=True)
150+
ns = restx.Namespace("apples")
151+
api.add_namespace(ns)
152+
153+
fields = ns.model(
154+
"Person",
155+
{
156+
"name": restx.fields.String(required=True),
157+
"age": restx.fields.Integer,
158+
"birthdate": restx.fields.DateTime,
159+
},
160+
strict=True,
161+
)
162+
163+
@ns.route("/validation/")
164+
class Payload(restx.Resource):
165+
payload = None
166+
167+
@ns.expect(fields)
168+
def post(self):
169+
Payload.payload = ns.payload
170+
return {}
171+
172+
data = {
173+
"name": "John Doe",
174+
"agge": 15, # typo
175+
}
176+
177+
resp = client.post_json("/apples/validation/", data, status=400)
178+
assert re.match("Additional properties are not allowed \(u*'agge' was unexpected\)", resp["errors"][""])

0 commit comments

Comments
 (0)