Skip to content

Commit e2e914c

Browse files
Flask v2 Support (#242)
* Add testing for Flask nested blueprints. (#238) * Add testing for nested blueprints. * Remove comment in tox.ini. * Add pytest xfails for broken exception handling tests. * Add flask version checks for nested blueprints. * Update error handler instrumentation point for Flask v2  (#239) * Update error handler instrumentation point for flask v2. * Reformat framework_flask.py file. * Remove pytest xfails. * Remove outdated flask versioning checks in tests, * Flask async view tests (#240) * Flask async view testing * Add skip logic for older versions of flask * Merge skip logic together for blueprints * Fix extras on master branch for flask * Remove old flask version skip logic * Restore required flask version testing (#244) * Restore required flask version testing * Fix version python 2 syntax issues * Remove py36 from latest flask tests * Separate async support skipping for pypy (#248) Co-authored-by: Uma Annamalai <[email protected]>
1 parent 9c007df commit e2e914c

13 files changed

+372
-243
lines changed

newrelic/hooks/framework_flask.py

+166-90
Large diffs are not rendered by default.

tests/framework_flask/_test_application.py

+4-12
Original file line numberDiff line numberDiff line change
@@ -18,25 +18,17 @@
1818
from werkzeug.exceptions import NotFound
1919
from werkzeug.routing import Rule
2020

21-
try:
22-
# The __version__ attribute was only added in 0.7.0.
23-
from flask import __version__ as flask_version
24-
is_gt_flask060 = True
25-
except ImportError:
26-
is_gt_flask060 = False
27-
2821
application = Flask(__name__)
2922

3023
@application.route('/index')
3124
def index_page():
3225
return 'INDEX RESPONSE'
3326

34-
if is_gt_flask060:
35-
application.url_map.add(Rule('/endpoint', endpoint='endpoint'))
27+
application.url_map.add(Rule('/endpoint', endpoint='endpoint'))
3628

37-
@application.endpoint('endpoint')
38-
def endpoint_page():
39-
return 'ENDPOINT RESPONSE'
29+
@application.endpoint('endpoint')
30+
def endpoint_page():
31+
return 'ENDPOINT RESPONSE'
4032

4133
@application.route('/error')
4234
def error_page():
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Copyright 2010 New Relic, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import webtest
16+
from _test_application import application
17+
18+
from conftest import async_handler_support
19+
20+
# Async handlers only supported in Flask >2.0.0
21+
if async_handler_support:
22+
@application.route('/async')
23+
async def async_page():
24+
return 'ASYNC RESPONSE'
25+
26+
_test_application = webtest.TestApp(application)

tests/framework_flask/_test_blueprints.py

+15
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
from flask import Blueprint
1919
from werkzeug.routing import Rule
2020

21+
from conftest import is_flask_v2 as nested_blueprint_support
22+
2123
# Blueprints are only available in 0.7.0 onwards.
2224

2325
blueprint = Blueprint('blueprint', __name__)
@@ -60,8 +62,21 @@ def teardown_request(exc):
6062
def teardown_app_request(exc):
6163
pass
6264

65+
# Support for nested blueprints was added in Flask 2.0
66+
if nested_blueprint_support:
67+
parent = Blueprint('parent', __name__, url_prefix='/parent')
68+
child = Blueprint('child', __name__, url_prefix='/child')
69+
70+
parent.register_blueprint(child)
71+
72+
@child.route('/nested')
73+
def nested_page():
74+
return 'PARENT NESTED RESPONSE'
75+
application.register_blueprint(parent)
76+
6377
application.register_blueprint(blueprint)
6478

79+
6580
application.url_map.add(Rule('/endpoint', endpoint='endpoint'))
6681

6782
_test_application = webtest.TestApp(application)
+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Copyright 2010 New Relic, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import webtest
16+
import flask.views
17+
18+
from _test_views import app
19+
20+
from conftest import async_handler_support
21+
22+
# Async view support added in flask v2
23+
if async_handler_support:
24+
class TestAsyncView(flask.views.View):
25+
async def dispatch_request(self):
26+
return "ASYNC VIEW RESPONSE"
27+
28+
class TestAsyncMethodView(flask.views.MethodView):
29+
async def get(self):
30+
return "ASYNC METHODVIEW GET RESPONSE"
31+
32+
app.add_url_rule("/async_view", view_func=TestAsyncView.as_view("test_async_view"))
33+
app.add_url_rule(
34+
"/async_methodview",
35+
view_func=TestAsyncMethodView.as_view("test_async_methodview"),
36+
)
37+
38+
_test_application = webtest.TestApp(app)

tests/framework_flask/conftest.py

+12
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import platform
16+
1517
import pytest
18+
from flask import __version__ as flask_version
1619

1720
from testing_support.fixtures import (code_coverage_fixture,
1821
collector_agent_registration_fixture, collector_available_fixture)
@@ -35,3 +38,12 @@
3538
collector_agent_registration = collector_agent_registration_fixture(
3639
app_name='Python Agent Test (framework_flask)',
3740
default_settings=_default_settings)
41+
42+
43+
is_flask_v2 = int(flask_version.split('.')[0]) >= 2
44+
is_pypy = platform.python_implementation() == "PyPy"
45+
async_handler_support = is_flask_v2 and not is_pypy
46+
skip_if_not_async_handler_support = pytest.mark.skipif(
47+
not async_handler_support,
48+
reason="Requires async handler support. (Flask >=v2.0.0)",
49+
)

tests/framework_flask/test_application.py

+34-27
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020

2121
from newrelic.packages import six
2222

23+
from conftest import async_handler_support, skip_if_not_async_handler_support
24+
2325
try:
2426
# The __version__ attribute was only added in 0.7.0.
2527
# Flask team does not use semantic versioning during development.
@@ -48,7 +50,10 @@ def target_application():
4850
# functions are different between Python 2 and 3, with the latter
4951
# showing <local> scope in path.
5052

51-
from _test_application import _test_application
53+
if not async_handler_support:
54+
from _test_application import _test_application
55+
else:
56+
from _test_application_async import _test_application
5257
return _test_application
5358

5459

@@ -87,7 +92,6 @@ def target_application():
8792
('FunctionNode', []),
8893
)
8994

90-
9195
@validate_transaction_errors(errors=[])
9296
@validate_transaction_metrics('_test_application:index_page',
9397
scoped_metrics=_test_application_index_scoped_metrics)
@@ -97,6 +101,23 @@ def test_application_index():
97101
response = application.get('/index')
98102
response.mustcontain('INDEX RESPONSE')
99103

104+
_test_application_async_scoped_metrics = [
105+
('Function/flask.app:Flask.wsgi_app', 1),
106+
('Python/WSGI/Application', 1),
107+
('Python/WSGI/Response', 1),
108+
('Python/WSGI/Finalize', 1),
109+
('Function/_test_application_async:async_page', 1),
110+
('Function/werkzeug.wsgi:ClosingIterator.close', 1)]
111+
112+
@skip_if_not_async_handler_support
113+
@validate_transaction_errors(errors=[])
114+
@validate_transaction_metrics('_test_application_async:async_page',
115+
scoped_metrics=_test_application_async_scoped_metrics)
116+
@validate_tt_parenting(_test_application_index_tt_parenting)
117+
def test_application_async():
118+
application = target_application()
119+
response = application.get('/async')
120+
response.mustcontain('ASYNC RESPONSE')
100121

101122
_test_application_endpoint_scoped_metrics = [
102123
('Function/flask.app:Flask.wsgi_app', 1),
@@ -107,7 +128,6 @@ def test_application_index():
107128
('Function/werkzeug.wsgi:ClosingIterator.close', 1)]
108129

109130

110-
@requires_endpoint_decorator
111131
@validate_transaction_errors(errors=[])
112132
@validate_transaction_metrics('_test_application:endpoint_page',
113133
scoped_metrics=_test_application_endpoint_scoped_metrics)
@@ -124,11 +144,10 @@ def test_application_endpoint():
124144
('Python/WSGI/Finalize', 1),
125145
('Function/_test_application:error_page', 1),
126146
('Function/flask.app:Flask.handle_exception', 1),
127-
('Function/werkzeug.wsgi:ClosingIterator.close', 1)]
147+
('Function/werkzeug.wsgi:ClosingIterator.close', 1),
148+
('Function/flask.app:Flask.handle_user_exception', 1),
149+
('Function/flask.app:Flask.handle_user_exception', 1)]
128150

129-
if is_gt_flask060:
130-
_test_application_error_scoped_metrics.extend([
131-
('Function/flask.app:Flask.handle_user_exception', 1)])
132151

133152
if six.PY3:
134153
_test_application_error_errors = ['builtins:RuntimeError']
@@ -151,11 +170,8 @@ def test_application_error():
151170
('Python/WSGI/Finalize', 1),
152171
('Function/_test_application:abort_404_page', 1),
153172
('Function/flask.app:Flask.handle_http_exception', 1),
154-
('Function/werkzeug.wsgi:ClosingIterator.close', 1)]
155-
156-
if is_gt_flask060:
157-
_test_application_abort_404_scoped_metrics.extend([
158-
('Function/flask.app:Flask.handle_user_exception', 1)])
173+
('Function/werkzeug.wsgi:ClosingIterator.close', 1),
174+
('Function/flask.app:Flask.handle_user_exception', 1)]
159175

160176

161177
@validate_transaction_errors(errors=[])
@@ -173,11 +189,8 @@ def test_application_abort_404():
173189
('Python/WSGI/Finalize', 1),
174190
('Function/_test_application:exception_404_page', 1),
175191
('Function/flask.app:Flask.handle_http_exception', 1),
176-
('Function/werkzeug.wsgi:ClosingIterator.close', 1)]
177-
178-
if is_gt_flask060:
179-
_test_application_exception_404_scoped_metrics.extend([
180-
('Function/flask.app:Flask.handle_user_exception', 1)])
192+
('Function/werkzeug.wsgi:ClosingIterator.close', 1),
193+
('Function/flask.app:Flask.handle_user_exception', 1)]
181194

182195

183196
@validate_transaction_errors(errors=[])
@@ -194,11 +207,8 @@ def test_application_exception_404():
194207
('Python/WSGI/Response', 1),
195208
('Python/WSGI/Finalize', 1),
196209
('Function/flask.app:Flask.handle_http_exception', 1),
197-
('Function/werkzeug.wsgi:ClosingIterator.close', 1)]
198-
199-
if is_gt_flask060:
200-
_test_application_not_found_scoped_metrics.extend([
201-
('Function/flask.app:Flask.handle_user_exception', 1)])
210+
('Function/werkzeug.wsgi:ClosingIterator.close', 1),
211+
('Function/flask.app:Flask.handle_user_exception', 1)]
202212

203213

204214
@validate_transaction_errors(errors=[])
@@ -235,11 +245,8 @@ def test_application_render_template_string():
235245
('Python/WSGI/Finalize', 1),
236246
('Function/_test_application:template_not_found', 1),
237247
('Function/flask.app:Flask.handle_exception', 1),
238-
('Function/werkzeug.wsgi:ClosingIterator.close', 1)]
239-
240-
if is_gt_flask060:
241-
_test_application_render_template_not_found_scoped_metrics.extend([
242-
('Function/flask.app:Flask.handle_user_exception', 1)])
248+
('Function/werkzeug.wsgi:ClosingIterator.close', 1),
249+
('Function/flask.app:Flask.handle_user_exception', 1)]
243250

244251

245252
@validate_transaction_errors(errors=['jinja2.exceptions:TemplateNotFound'])

tests/framework_flask/test_blueprints.py

+30-15
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,11 @@
1919

2020
from newrelic.packages import six
2121

22-
try:
23-
# The __version__ attribute was only added in 0.7.0.
24-
# Flask team does not use semantic versioning during development.
25-
from flask import __version__ as flask_version
26-
is_gt_flask080 = 'dev' in flask_version or tuple(
27-
map(int, flask_version.split('.')))[:2] > (0, 8)
28-
except ImportError:
29-
is_gt_flask080 = False
22+
from conftest import is_flask_v2 as nested_blueprint_support
3023

31-
# Technically parts of blueprints support is available in older
32-
# versions, but just check with latest versions. The instrumentation
33-
# always checks for presence of required functions before patching.
24+
skip_if_not_nested_blueprint_support = pytest.mark.skipif(not nested_blueprint_support,
25+
reason="Requires nested blueprint support. (Flask >=v2.0.0)")
3426

35-
requires_blueprint = pytest.mark.skipif(not is_gt_flask080,
36-
reason="The blueprint mechanism is not supported.")
3727

3828
def target_application():
3929
# We need to delay Flask application creation because of ordering
@@ -66,7 +56,6 @@ def target_application():
6656
('Function/flask.app:Flask.do_teardown_appcontext', 1),
6757
('Function/werkzeug.wsgi:ClosingIterator.close', 1)]
6858

69-
@requires_blueprint
7059
@validate_transaction_errors(errors=[])
7160
@validate_transaction_metrics('_test_blueprints:index_page',
7261
scoped_metrics=_test_blueprints_index_scoped_metrics)
@@ -90,11 +79,37 @@ def test_blueprints_index():
9079
('Function/flask.app:Flask.do_teardown_appcontext', 1),
9180
('Function/werkzeug.wsgi:ClosingIterator.close', 1)]
9281

93-
@requires_blueprint
9482
@validate_transaction_errors(errors=[])
9583
@validate_transaction_metrics('_test_blueprints:endpoint_page',
9684
scoped_metrics=_test_blueprints_endpoint_scoped_metrics)
9785
def test_blueprints_endpoint():
9886
application = target_application()
9987
response = application.get('/endpoint')
10088
response.mustcontain('BLUEPRINT ENDPOINT RESPONSE')
89+
90+
91+
_test_blueprints_nested_scoped_metrics = [
92+
('Function/flask.app:Flask.wsgi_app', 1),
93+
('Python/WSGI/Application', 1),
94+
('Python/WSGI/Response', 1),
95+
('Python/WSGI/Finalize', 1),
96+
('Function/_test_blueprints:nested_page', 1),
97+
('Function/flask.app:Flask.preprocess_request', 1),
98+
('Function/_test_blueprints:before_app_request', 1),
99+
('Function/_test_blueprints:before_request', 1),
100+
('Function/flask.app:Flask.process_response', 1),
101+
('Function/_test_blueprints:after_request', 1),
102+
('Function/_test_blueprints:after_app_request', 1),
103+
('Function/flask.app:Flask.do_teardown_request', 1),
104+
('Function/_test_blueprints:teardown_app_request', 1),
105+
('Function/_test_blueprints:teardown_request', 1),
106+
('Function/flask.app:Flask.do_teardown_appcontext', 1),
107+
('Function/werkzeug.wsgi:ClosingIterator.close', 1)]
108+
109+
@skip_if_not_nested_blueprint_support
110+
@validate_transaction_errors(errors=[])
111+
@validate_transaction_metrics('_test_blueprints:nested_page')
112+
def test_blueprints_nested():
113+
application = target_application()
114+
response = application.get('/parent/child/nested')
115+
response.mustcontain('PARENT NESTED RESPONSE')

tests/framework_flask/test_middleware.py

-19
Original file line numberDiff line numberDiff line change
@@ -17,24 +17,6 @@
1717
from testing_support.fixtures import (validate_transaction_metrics,
1818
validate_transaction_errors, override_application_settings)
1919

20-
try:
21-
# The __version__ attribute was only added in 0.7.0.
22-
# Flask team does not use semantic versioning during development.
23-
from flask import __version__ as flask_version
24-
is_gt_flask080 = 'dev' in flask_version or tuple(
25-
map(int, flask_version.split('.')))[:2] > (0, 8)
26-
except ValueError:
27-
is_gt_flask080 = True
28-
except ImportError:
29-
is_gt_flask080 = False
30-
31-
# Technically parts of before/after support is available in older
32-
# versions, but just check with latest versions. The instrumentation
33-
# always checks for presence of required functions before patching.
34-
35-
requires_before_after = pytest.mark.skipif(not is_gt_flask080,
36-
reason="Not all before/after methods are supported.")
37-
3820
def target_application():
3921
# We need to delay Flask application creation because of ordering
4022
# issues whereby the agent needs to be initialised before Flask is
@@ -66,7 +48,6 @@ def target_application():
6648
('Function/_test_middleware:teardown_appcontext', 1),
6749
('Function/werkzeug.wsgi:ClosingIterator.close', 1)]
6850

69-
@requires_before_after
7051
@validate_transaction_errors(errors=[])
7152
@validate_transaction_metrics('_test_middleware:index_page',
7253
scoped_metrics=_test_application_app_middleware_scoped_metrics)

0 commit comments

Comments
 (0)