Skip to content

Commit 9ab99ea

Browse files
authored
Merge branch 'master' into fix/infinite-recursion-bug
2 parents cd26463 + a7e363a commit 9ab99ea

File tree

11 files changed

+176
-15
lines changed

11 files changed

+176
-15
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
---
2+
name: Bug Report
3+
about: Tell us how Flask-RESTPlus is broken
4+
title: ''
5+
labels: bug
6+
assignees: ''
7+
8+
---
9+
10+
### ***** **BEFORE LOGGING AN ISSUE** *****
11+
12+
- Is this something you can **debug and fix**? Send a pull request! Bug fixes and documentation fixes are welcome.
13+
- Please check if a similar issue already exists or has been closed before. Seriously, nobody here is getting paid. Help us out and take five minutes to make sure you aren't submitting a duplicate.
14+
- Please review the [guidelines for contributing](https://github.com/noirbizarre/flask-restplus/blob/master/CONTRIBUTING.rst)
15+
16+
### **Code**
17+
18+
```python
19+
from your_code import your_buggy_implementation
20+
```
21+
22+
### **Repro Steps** (if applicable)
23+
1. ...
24+
2. ...
25+
3. Broken!
26+
27+
### **Expected Behavior**
28+
A description of what you expected to happen.
29+
30+
### **Actual Behavior**
31+
A description of the unexpected, buggy behavior.
32+
33+
### **Error Messages/Stack Trace**
34+
If applicable, add the stack trace produced by the error
35+
36+
### **Environment**
37+
- Python version
38+
- Flask version
39+
- Flask-RESTPlus version
40+
- Other installed Flask extensions
41+
42+
### **Additional Context**
43+
44+
This is your last chance to provide any pertinent details, don't let this opportunity pass you by!
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
## Proposed changes
2+
3+
At a high level, describe your reasoning for making these changes. If you are fixing a bug or resolving a feature request, **please include a link to the issue**.
4+
5+
## Types of changes
6+
7+
What types of changes does your code introduce?
8+
_Put an `x` in the boxes that apply_
9+
10+
- [ ] Bugfix (non-breaking change which fixes an issue)
11+
- [ ] New feature (non-breaking change which adds functionality)
12+
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
13+
14+
## Checklist
15+
16+
_Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your code._
17+
18+
- [ ] I have read the [guidelines for contributing](https://github.com/noirbizarre/flask-restplus/blob/master/CONTRIBUTING.rst)
19+
- [ ] All unit tests pass on my local version with my changes
20+
- [ ] I have added tests that prove my fix is effective or that my feature works
21+
- [ ] I have added necessary documentation (if appropriate)
22+
23+
## Further comments
24+
25+
If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you did and what alternatives you considered, etc...

CHANGELOG.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Changelog
66
Current
77
-------
88

9-
- Nothing yet
9+
- Ensure that exceptions raised in error handler, including programming errors, are logged (:issue:`705`, :pr:`706`)
1010

1111
0.13.0 (2019-08-12)
1212
-------------------
@@ -17,6 +17,7 @@ Current
1717
- Ensure `basePath` is always a path
1818
- Hide Namespaces with all hidden Resources from Swagger documentation
1919
- Per route Swagger documentation for multiple routes on a ``Resource``
20+
- Fix Swagger `duplicate mapping key` problem from conflicts between response codes given as string or integer (:issue`661`)
2021

2122
0.12.1 (2018-09-28)
2223
-------------------

flask_restplus/api.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -579,8 +579,8 @@ def error_router(self, original_handler, e):
579579
if self._has_fr_route():
580580
try:
581581
return self.handle_error(e)
582-
except Exception:
583-
pass # Fall through to original handler
582+
except Exception as f:
583+
return original_handler(f)
584584
return original_handler(e)
585585

586586
def handle_error(self, e):

flask_restplus/fields.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -674,7 +674,7 @@ def output(self, key, obj, ordered=False, **kwargs):
674674
if not hasattr(value, '__class__'):
675675
raise ValueError('Polymorph field only accept class instances')
676676

677-
candidates = [fields for cls, fields in iteritems(self.mapping) if isinstance(value, cls)]
677+
candidates = [fields for cls, fields in iteritems(self.mapping) if type(value) == cls]
678678

679679
if len(candidates) <= 0:
680680
raise ValueError('Unknown class: ' + value.__class__.__name__)

flask_restplus/namespace.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ def marshal_with(self, fields, as_list=False, code=HTTPStatus.OK, description=No
238238
def wrapper(func):
239239
doc = {
240240
'responses': {
241-
code: (description, [fields]) if as_list else (description, fields)
241+
str(code): (description, [fields]) if as_list else (description, fields)
242242
},
243243
'__mask__': kwargs.get('mask', True), # Mask values can't be determined outside app context
244244
}
@@ -289,7 +289,7 @@ def response(self, code, description, model=None, **kwargs):
289289
:param ModelBase model: an optional response model
290290
291291
'''
292-
return self.doc(responses={code: (description, model, kwargs)})
292+
return self.doc(responses={str(code): (description, model, kwargs)})
293293

294294
def header(self, name, description=None, **kwargs):
295295
'''

flask_restplus/swagger.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,7 @@ def responses_for(self, doc, method):
485485
for d in doc, doc[method]:
486486
if 'responses' in d:
487487
for code, response in iteritems(d['responses']):
488+
code = str(code)
488489
if isinstance(response, string_types):
489490
description = response
490491
model = None
@@ -514,7 +515,7 @@ def responses_for(self, doc, method):
514515
for name, description in iteritems(d['docstring']['raises']):
515516
for exception, handler in iteritems(self.api.error_handlers):
516517
error_responses = getattr(handler, '__apidoc__', {}).get('responses', {})
517-
code = list(error_responses.keys())[0] if error_responses else None
518+
code = str(list(error_responses.keys())[0]) if error_responses else None
518519
if code and exception.__name__ == name:
519520
responses[code] = {'$ref': '#/responses/{0}'.format(name)}
520521
break

tests/conftest.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,23 @@
1111

1212

1313
class TestClient(FlaskClient):
14+
# Borrowed from https://pythonadventures.wordpress.com/2016/03/06/detect-duplicate-keys-in-a-json-file/
15+
# Thank you to Wordpress author @ubuntuincident, aka Jabba Laci.
16+
def dict_raise_on_duplicates(self, ordered_pairs):
17+
"""Reject duplicate keys."""
18+
d = {}
19+
for k, v in ordered_pairs:
20+
if k in d:
21+
raise ValueError("duplicate key: %r" % (k,))
22+
else:
23+
d[k] = v
24+
return d
25+
1426
def get_json(self, url, status=200, **kwargs):
1527
response = self.get(url, **kwargs)
1628
assert response.status_code == status
1729
assert response.content_type == 'application/json'
18-
return json.loads(response.data.decode('utf8'))
30+
return json.loads(response.data.decode('utf8'), object_pairs_hook=self.dict_raise_on_duplicates)
1931

2032
def post_json(self, url, data, status=200, **kwargs):
2133
response = self.post(url, data=json.dumps(data),

tests/test_errors.py

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
from __future__ import unicode_literals
33

44
import json
5+
import logging
6+
57
import pytest
68

79
from flask import Blueprint, abort
@@ -162,6 +164,31 @@ def handle_custom_exception(error):
162164
'test': 'value',
163165
}
164166

167+
def test_blunder_in_errorhandler_is_not_suppressed_in_logs(self, app, client, caplog):
168+
169+
api = restplus.Api(app)
170+
171+
class CustomException(RuntimeError):
172+
pass
173+
174+
class ProgrammingBlunder(Exception):
175+
pass
176+
177+
@api.route('/test/', endpoint="test")
178+
class TestResource(restplus.Resource):
179+
def get(self):
180+
raise CustomException('error')
181+
182+
@api.errorhandler(CustomException)
183+
def handle_custom_exception(error):
184+
raise ProgrammingBlunder("This exception needs to be logged, not suppressed, then cause 500")
185+
186+
with caplog.at_level(logging.ERROR):
187+
response = client.get('/test/')
188+
exc_type, value, traceback = caplog.records[0].exc_info
189+
assert exc_type is ProgrammingBlunder
190+
assert response.status_code == 500
191+
165192
def test_errorhandler_for_custom_exception_with_headers(self, app, client):
166193
api = restplus.Api(app)
167194

@@ -480,15 +507,23 @@ def test_handle_not_include_error_message(self, app):
480507
assert 'message' not in json.loads(response.data.decode())
481508

482509
def test_error_router_falls_back_to_original(self, app, mocker):
510+
class ProgrammingBlunder(Exception):
511+
pass
512+
513+
blunder = ProgrammingBlunder("This exception needs to be detectable")
514+
515+
def raise_blunder(arg):
516+
raise blunder
517+
483518
api = restplus.Api(app)
484519
app.handle_exception = mocker.Mock()
485-
api.handle_error = mocker.Mock(side_effect=Exception())
520+
api.handle_error = mocker.Mock(side_effect=raise_blunder)
486521
api._has_fr_route = mocker.Mock(return_value=True)
487522
exception = mocker.Mock(spec=HTTPException)
488523

489524
api.error_router(app.handle_exception, exception)
490525

491-
app.handle_exception.assert_called_with(exception)
526+
app.handle_exception.assert_called_with(blunder)
492527

493528
def test_fr_405(self, app, client):
494529
api = restplus.Api(app)

tests/test_fields.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1177,7 +1177,10 @@ class Child2(object):
11771177
with pytest.raises(ValueError):
11781178
api.marshal({'owner': object()}, thing)
11791179

1180-
def test_polymorph_field_ambiguous_mapping(self, api):
1180+
def test_polymorph_field_does_not_have_ambiguous_mappings(self, api):
1181+
"""
1182+
Regression test for https://github.com/noirbizarre/flask-restplus/pull/691
1183+
"""
11811184
parent = api.model('Parent', {
11821185
'name': fields.String,
11831186
})
@@ -1201,8 +1204,7 @@ class Child(Parent):
12011204
'owner': fields.Polymorph(mapping),
12021205
})
12031206

1204-
with pytest.raises(ValueError):
1205-
api.marshal({'owner': Child()}, thing)
1207+
api.marshal({'owner': Child()}, thing)
12061208

12071209
def test_polymorph_field_required_default(self, api):
12081210
parent = api.model('Person', {

0 commit comments

Comments
 (0)