diff --git a/newrelic/hooks/framework_flask.py b/newrelic/hooks/framework_flask.py index 4e33c8df19..4535b3289a 100644 --- a/newrelic/hooks/framework_flask.py +++ b/newrelic/hooks/framework_flask.py @@ -17,18 +17,23 @@ """ from newrelic.api.wsgi_application import wrap_wsgi_application -from newrelic.api.function_trace import (FunctionTrace, wrap_function_trace, - FunctionTraceWrapper) +from newrelic.api.function_trace import ( + FunctionTrace, + wrap_function_trace, + FunctionTraceWrapper, +) from newrelic.api.transaction import current_transaction from newrelic.api.time_trace import notice_error -from newrelic.common.object_wrapper import (wrap_function_wrapper, - function_wrapper) +from newrelic.common.object_wrapper import wrap_function_wrapper, function_wrapper from newrelic.common.object_names import callable_name + def framework_details(): import flask - return ('Flask', getattr(flask, '__version__', None)) + + return ("Flask", getattr(flask, "__version__", None)) + def status_code(exc, value, tb): from werkzeug.exceptions import HTTPException @@ -40,6 +45,7 @@ def status_code(exc, value, tb): if isinstance(value, HTTPException): return value.code + @function_wrapper def _nr_wrapper_handler_(wrapped, instance, args, kwargs): transaction = current_transaction() @@ -47,7 +53,7 @@ def _nr_wrapper_handler_(wrapped, instance, args, kwargs): if transaction is None: return wrapped(*args, **kwargs) - name = getattr(wrapped, '_nr_view_func_name', callable_name(wrapped)) + name = getattr(wrapped, "_nr_view_func_name", callable_name(wrapped)) # Set priority=2 so this will take precedence over any error # handler which will be at priority=1. @@ -57,6 +63,7 @@ def _nr_wrapper_handler_(wrapped, instance, args, kwargs): with FunctionTrace(name): return wrapped(*args, **kwargs) + def _nr_wrapper_Flask_add_url_rule_input_(wrapped, instance, args, kwargs): def _bind_params(rule, endpoint=None, view_func=None, **options): return rule, endpoint, view_func, options @@ -68,11 +75,13 @@ def _bind_params(rule, endpoint=None, view_func=None, **options): return wrapped(rule, endpoint, view_func, **options) + def _nr_wrapper_Flask_views_View_as_view_(wrapped, instance, args, kwargs): view = wrapped(*args, **kwargs) - view._nr_view_func_name = '%s:%s' % (view.__module__, view.__name__) + view._nr_view_func_name = "%s:%s" % (view.__module__, view.__name__) return view + @function_wrapper def _nr_wrapper_endpoint_(wrapped, instance, args, kwargs): def _bind_params(f, *args, **kwargs): @@ -82,9 +91,11 @@ def _bind_params(f, *args, **kwargs): return wrapped(_nr_wrapper_handler_(f)) + def _nr_wrapper_Flask_endpoint_(wrapped, instance, args, kwargs): return _nr_wrapper_endpoint_(wrapped(*args, **kwargs)) + def _nr_wrapper_Flask_handle_http_exception_(wrapped, instance, args, kwargs): transaction = current_transaction() @@ -102,6 +113,7 @@ def _nr_wrapper_Flask_handle_http_exception_(wrapped, instance, args, kwargs): with FunctionTrace(name): return wrapped(*args, **kwargs) + def _nr_wrapper_Flask_handle_exception_(wrapped, instance, args, kwargs): transaction = current_transaction() @@ -119,6 +131,7 @@ def _nr_wrapper_Flask_handle_exception_(wrapped, instance, args, kwargs): with FunctionTrace(name): return wrapped(*args, **kwargs) + @function_wrapper def _nr_wrapper_error_handler_(wrapped, instance, args, kwargs): transaction = current_transaction() @@ -137,6 +150,7 @@ def _nr_wrapper_error_handler_(wrapped, instance, args, kwargs): with FunctionTrace(name): return wrapped(*args, **kwargs) + def _nr_wrapper_Flask__register_error_handler_(wrapped, instance, args, kwargs): def _bind_params(key, code_or_exception, f): return key, code_or_exception, f @@ -147,8 +161,21 @@ def _bind_params(key, code_or_exception, f): return wrapped(key, code_or_exception, f) + +def _nr_wrapper_Flask_register_error_handler_(wrapped, instance, args, kwargs): + def _bind_params(code_or_exception, f): + return code_or_exception, f + + code_or_exception, f = _bind_params(*args, **kwargs) + + f = _nr_wrapper_error_handler_(f) + + return wrapped(code_or_exception, f) + + def _nr_wrapper_Flask_try_trigger_before_first_request_functions_( - wrapped, instance, args, kwargs): + wrapped, instance, args, kwargs +): transaction = current_transaction() @@ -168,6 +195,7 @@ def _nr_wrapper_Flask_try_trigger_before_first_request_functions_( with FunctionTrace(name): return wrapped(*args, **kwargs) + def _nr_wrapper_Flask_before_first_request_(wrapped, instance, args, kwargs): def _params(f, *args, **kwargs): return f, args, kwargs @@ -177,6 +205,7 @@ def _params(f, *args, **kwargs): return wrapped(f, *_args, **_kwargs) + @function_wrapper def _nr_wrapper_Flask_before_request_wrapped_(wrapped, instance, args, kwargs): transaction = current_transaction() @@ -191,6 +220,7 @@ def _nr_wrapper_Flask_before_request_wrapped_(wrapped, instance, args, kwargs): with FunctionTrace(name): return wrapped(*args, **kwargs) + def _nr_wrapper_Flask_before_request_(wrapped, instance, args, kwargs): def _params(f, *args, **kwargs): return f, args, kwargs @@ -200,6 +230,7 @@ def _params(f, *args, **kwargs): return wrapped(f, *_args, **_kwargs) + def _nr_wrapper_Flask_after_request_(wrapped, instance, args, kwargs): def _params(f, *args, **kwargs): return f, args, kwargs @@ -209,6 +240,7 @@ def _params(f, *args, **kwargs): return wrapped(f, *_args, **_kwargs) + def _nr_wrapper_Flask_teardown_request_(wrapped, instance, args, kwargs): def _params(f, *args, **kwargs): return f, args, kwargs @@ -218,6 +250,7 @@ def _params(f, *args, **kwargs): return wrapped(f, *_args, **_kwargs) + def _nr_wrapper_Flask_teardown_appcontext_(wrapped, instance, args, kwargs): def _params(f, *args, **kwargs): return f, args, kwargs @@ -227,81 +260,106 @@ def _params(f, *args, **kwargs): return wrapped(f, *_args, **_kwargs) + def instrument_flask_views(module): - wrap_function_wrapper(module, 'View.as_view', - _nr_wrapper_Flask_views_View_as_view_) + wrap_function_wrapper(module, "View.as_view", _nr_wrapper_Flask_views_View_as_view_) + def instrument_flask_app(module): - wrap_wsgi_application(module, 'Flask.wsgi_app', - framework=framework_details()) + wrap_wsgi_application(module, "Flask.wsgi_app", framework=framework_details()) - wrap_function_wrapper(module, 'Flask.add_url_rule', - _nr_wrapper_Flask_add_url_rule_input_) + wrap_function_wrapper( + module, "Flask.add_url_rule", _nr_wrapper_Flask_add_url_rule_input_ + ) - if hasattr(module.Flask, 'endpoint'): - wrap_function_wrapper(module, 'Flask.endpoint', - _nr_wrapper_Flask_endpoint_) + if hasattr(module.Flask, "endpoint"): + wrap_function_wrapper(module, "Flask.endpoint", _nr_wrapper_Flask_endpoint_) - wrap_function_wrapper(module, 'Flask.handle_http_exception', - _nr_wrapper_Flask_handle_http_exception_) + wrap_function_wrapper( + module, "Flask.handle_http_exception", _nr_wrapper_Flask_handle_http_exception_ + ) # Use the same wrapper for initial user exception processing and # fallback for unhandled exceptions. - if hasattr(module.Flask, 'handle_user_exception'): - wrap_function_wrapper(module, 'Flask.handle_user_exception', - _nr_wrapper_Flask_handle_exception_) + if hasattr(module.Flask, "handle_user_exception"): + wrap_function_wrapper( + module, "Flask.handle_user_exception", _nr_wrapper_Flask_handle_exception_ + ) - wrap_function_wrapper(module, 'Flask.handle_exception', - _nr_wrapper_Flask_handle_exception_) + wrap_function_wrapper( + module, "Flask.handle_exception", _nr_wrapper_Flask_handle_exception_ + ) # The _register_error_handler() method was only introduced in # Flask version 0.7.0. - - if hasattr(module.Flask, '_register_error_handler'): - wrap_function_wrapper(module, 'Flask._register_error_handler', - _nr_wrapper_Flask__register_error_handler_) + if hasattr(module.Flask, "_register_error_handler"): + wrap_function_wrapper( + module, + "Flask._register_error_handler", + _nr_wrapper_Flask__register_error_handler_, + ) + + # The method changed name to register_error_handler() in + # Flask version 2.0.0. + elif hasattr(module.Flask, "register_error_handler"): + wrap_function_wrapper( + module, + "Flask.register_error_handler", + _nr_wrapper_Flask_register_error_handler_, + ) # Different before/after methods were added in different versions. # Check for the presence of everything before patching. - if hasattr(module.Flask, 'try_trigger_before_first_request_functions'): - wrap_function_wrapper(module, - 'Flask.try_trigger_before_first_request_functions', - _nr_wrapper_Flask_try_trigger_before_first_request_functions_) - wrap_function_wrapper(module, 'Flask.before_first_request', - _nr_wrapper_Flask_before_first_request_) - - if hasattr(module.Flask, 'preprocess_request'): - wrap_function_trace(module, 'Flask.preprocess_request') - wrap_function_wrapper(module, 'Flask.before_request', - _nr_wrapper_Flask_before_request_) - - if hasattr(module.Flask, 'process_response'): - wrap_function_trace(module, 'Flask.process_response') - wrap_function_wrapper(module, 'Flask.after_request', - _nr_wrapper_Flask_after_request_) - - if hasattr(module.Flask, 'do_teardown_request'): - wrap_function_trace(module, 'Flask.do_teardown_request') - wrap_function_wrapper(module, 'Flask.teardown_request', - _nr_wrapper_Flask_teardown_request_) - - if hasattr(module.Flask, 'do_teardown_appcontext'): - wrap_function_trace(module, 'Flask.do_teardown_appcontext') - wrap_function_wrapper(module, 'Flask.teardown_appcontext', - _nr_wrapper_Flask_teardown_appcontext_) + if hasattr(module.Flask, "try_trigger_before_first_request_functions"): + wrap_function_wrapper( + module, + "Flask.try_trigger_before_first_request_functions", + _nr_wrapper_Flask_try_trigger_before_first_request_functions_, + ) + wrap_function_wrapper( + module, + "Flask.before_first_request", + _nr_wrapper_Flask_before_first_request_, + ) + + if hasattr(module.Flask, "preprocess_request"): + wrap_function_trace(module, "Flask.preprocess_request") + wrap_function_wrapper( + module, "Flask.before_request", _nr_wrapper_Flask_before_request_ + ) + + if hasattr(module.Flask, "process_response"): + wrap_function_trace(module, "Flask.process_response") + wrap_function_wrapper( + module, "Flask.after_request", _nr_wrapper_Flask_after_request_ + ) + + if hasattr(module.Flask, "do_teardown_request"): + wrap_function_trace(module, "Flask.do_teardown_request") + wrap_function_wrapper( + module, "Flask.teardown_request", _nr_wrapper_Flask_teardown_request_ + ) + + if hasattr(module.Flask, "do_teardown_appcontext"): + wrap_function_trace(module, "Flask.do_teardown_appcontext") + wrap_function_wrapper( + module, "Flask.teardown_appcontext", _nr_wrapper_Flask_teardown_appcontext_ + ) + def instrument_flask_templating(module): - wrap_function_trace(module, 'render_template') - wrap_function_trace(module, 'render_template_string') + wrap_function_trace(module, "render_template") + wrap_function_trace(module, "render_template_string") + def _nr_wrapper_Blueprint_endpoint_(wrapped, instance, args, kwargs): return _nr_wrapper_endpoint_(wrapped(*args, **kwargs)) + @function_wrapper -def _nr_wrapper_Blueprint_before_request_wrapped_(wrapped, instance, - args, kwargs): +def _nr_wrapper_Blueprint_before_request_wrapped_(wrapped, instance, args, kwargs): transaction = current_transaction() @@ -315,6 +373,7 @@ def _nr_wrapper_Blueprint_before_request_wrapped_(wrapped, instance, with FunctionTrace(name): return wrapped(*args, **kwargs) + def _nr_wrapper_Blueprint_before_request_(wrapped, instance, args, kwargs): def _params(f, *args, **kwargs): return f, args, kwargs @@ -324,9 +383,8 @@ def _params(f, *args, **kwargs): return wrapped(f, *_args, **_kwargs) -def _nr_wrapper_Blueprint_before_app_request_(wrapped, instance, - args, kwargs): +def _nr_wrapper_Blueprint_before_app_request_(wrapped, instance, args, kwargs): def _params(f, *args, **kwargs): return f, args, kwargs @@ -335,9 +393,8 @@ def _params(f, *args, **kwargs): return wrapped(f, *_args, **_kwargs) -def _nr_wrapper_Blueprint_before_app_first_request_(wrapped, instance, - args, kwargs): +def _nr_wrapper_Blueprint_before_app_first_request_(wrapped, instance, args, kwargs): def _params(f, *args, **kwargs): return f, args, kwargs @@ -346,6 +403,7 @@ def _params(f, *args, **kwargs): return wrapped(f, *_args, **_kwargs) + def _nr_wrapper_Blueprint_after_request_(wrapped, instance, args, kwargs): def _params(f, *args, **kwargs): return f, args, kwargs @@ -355,6 +413,7 @@ def _params(f, *args, **kwargs): return wrapped(f, *_args, **_kwargs) + def _nr_wrapper_Blueprint_after_app_request_(wrapped, instance, args, kwargs): def _params(f, *args, **kwargs): return f, args, kwargs @@ -364,6 +423,7 @@ def _params(f, *args, **kwargs): return wrapped(f, *_args, **_kwargs) + def _nr_wrapper_Blueprint_teardown_request_(wrapped, instance, args, kwargs): def _params(f, *args, **kwargs): return f, args, kwargs @@ -373,9 +433,8 @@ def _params(f, *args, **kwargs): return wrapped(f, *_args, **_kwargs) -def _nr_wrapper_Blueprint_teardown_app_request_(wrapped, instance, - args, kwargs): +def _nr_wrapper_Blueprint_teardown_app_request_(wrapped, instance, args, kwargs): def _params(f, *args, **kwargs): return f, args, kwargs @@ -384,30 +443,47 @@ def _params(f, *args, **kwargs): return wrapped(f, *_args, **_kwargs) + def instrument_flask_blueprints(module): - wrap_function_wrapper(module, 'Blueprint.endpoint', - _nr_wrapper_Blueprint_endpoint_) - - if hasattr(module.Blueprint, 'before_request'): - wrap_function_wrapper(module, 'Blueprint.before_request', - _nr_wrapper_Blueprint_before_request_) - if hasattr(module.Blueprint, 'before_app_request'): - wrap_function_wrapper(module, 'Blueprint.before_app_request', - _nr_wrapper_Blueprint_before_app_request_) - if hasattr(module.Blueprint, 'before_app_first_request'): - wrap_function_wrapper(module, 'Blueprint.before_app_first_request', - _nr_wrapper_Blueprint_before_app_first_request_) - - if hasattr(module.Blueprint, 'after_request'): - wrap_function_wrapper(module, 'Blueprint.after_request', - _nr_wrapper_Blueprint_after_request_) - if hasattr(module.Blueprint, 'after_app_request'): - wrap_function_wrapper(module, 'Blueprint.after_app_request', - _nr_wrapper_Blueprint_after_app_request_) - - if hasattr(module.Blueprint, 'teardown_request'): - wrap_function_wrapper(module, 'Blueprint.teardown_request', - _nr_wrapper_Blueprint_teardown_request_) - if hasattr(module.Blueprint, 'teardown_app_request'): - wrap_function_wrapper(module, 'Blueprint.teardown_app_request', - _nr_wrapper_Blueprint_teardown_app_request_) + wrap_function_wrapper(module, "Blueprint.endpoint", _nr_wrapper_Blueprint_endpoint_) + + if hasattr(module.Blueprint, "before_request"): + wrap_function_wrapper( + module, "Blueprint.before_request", _nr_wrapper_Blueprint_before_request_ + ) + if hasattr(module.Blueprint, "before_app_request"): + wrap_function_wrapper( + module, + "Blueprint.before_app_request", + _nr_wrapper_Blueprint_before_app_request_, + ) + if hasattr(module.Blueprint, "before_app_first_request"): + wrap_function_wrapper( + module, + "Blueprint.before_app_first_request", + _nr_wrapper_Blueprint_before_app_first_request_, + ) + + if hasattr(module.Blueprint, "after_request"): + wrap_function_wrapper( + module, "Blueprint.after_request", _nr_wrapper_Blueprint_after_request_ + ) + if hasattr(module.Blueprint, "after_app_request"): + wrap_function_wrapper( + module, + "Blueprint.after_app_request", + _nr_wrapper_Blueprint_after_app_request_, + ) + + if hasattr(module.Blueprint, "teardown_request"): + wrap_function_wrapper( + module, + "Blueprint.teardown_request", + _nr_wrapper_Blueprint_teardown_request_, + ) + if hasattr(module.Blueprint, "teardown_app_request"): + wrap_function_wrapper( + module, + "Blueprint.teardown_app_request", + _nr_wrapper_Blueprint_teardown_app_request_, + ) diff --git a/tests/framework_flask/_test_application.py b/tests/framework_flask/_test_application.py index ff8666817a..4c1d044527 100644 --- a/tests/framework_flask/_test_application.py +++ b/tests/framework_flask/_test_application.py @@ -18,25 +18,17 @@ from werkzeug.exceptions import NotFound from werkzeug.routing import Rule -try: - # The __version__ attribute was only added in 0.7.0. - from flask import __version__ as flask_version - is_gt_flask060 = True -except ImportError: - is_gt_flask060 = False - application = Flask(__name__) @application.route('/index') def index_page(): return 'INDEX RESPONSE' -if is_gt_flask060: - application.url_map.add(Rule('/endpoint', endpoint='endpoint')) +application.url_map.add(Rule('/endpoint', endpoint='endpoint')) - @application.endpoint('endpoint') - def endpoint_page(): - return 'ENDPOINT RESPONSE' +@application.endpoint('endpoint') +def endpoint_page(): + return 'ENDPOINT RESPONSE' @application.route('/error') def error_page(): diff --git a/tests/framework_flask/_test_application_async.py b/tests/framework_flask/_test_application_async.py new file mode 100644 index 0000000000..05b823c291 --- /dev/null +++ b/tests/framework_flask/_test_application_async.py @@ -0,0 +1,26 @@ +# Copyright 2010 New Relic, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import webtest +from _test_application import application + +from conftest import async_handler_support + +# Async handlers only supported in Flask >2.0.0 +if async_handler_support: + @application.route('/async') + async def async_page(): + return 'ASYNC RESPONSE' + +_test_application = webtest.TestApp(application) diff --git a/tests/framework_flask/_test_blueprints.py b/tests/framework_flask/_test_blueprints.py index 950ed7f7e2..11236eb33c 100644 --- a/tests/framework_flask/_test_blueprints.py +++ b/tests/framework_flask/_test_blueprints.py @@ -18,6 +18,8 @@ from flask import Blueprint from werkzeug.routing import Rule +from conftest import is_flask_v2 as nested_blueprint_support + # Blueprints are only available in 0.7.0 onwards. blueprint = Blueprint('blueprint', __name__) @@ -60,8 +62,21 @@ def teardown_request(exc): def teardown_app_request(exc): pass +# Support for nested blueprints was added in Flask 2.0 +if nested_blueprint_support: + parent = Blueprint('parent', __name__, url_prefix='/parent') + child = Blueprint('child', __name__, url_prefix='/child') + + parent.register_blueprint(child) + + @child.route('/nested') + def nested_page(): + return 'PARENT NESTED RESPONSE' + application.register_blueprint(parent) + application.register_blueprint(blueprint) + application.url_map.add(Rule('/endpoint', endpoint='endpoint')) _test_application = webtest.TestApp(application) diff --git a/tests/framework_flask/_test_views_async.py b/tests/framework_flask/_test_views_async.py new file mode 100644 index 0000000000..b858d02a6e --- /dev/null +++ b/tests/framework_flask/_test_views_async.py @@ -0,0 +1,38 @@ +# Copyright 2010 New Relic, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import webtest +import flask.views + +from _test_views import app + +from conftest import async_handler_support + +# Async view support added in flask v2 +if async_handler_support: + class TestAsyncView(flask.views.View): + async def dispatch_request(self): + return "ASYNC VIEW RESPONSE" + + class TestAsyncMethodView(flask.views.MethodView): + async def get(self): + return "ASYNC METHODVIEW GET RESPONSE" + + app.add_url_rule("/async_view", view_func=TestAsyncView.as_view("test_async_view")) + app.add_url_rule( + "/async_methodview", + view_func=TestAsyncMethodView.as_view("test_async_methodview"), + ) + +_test_application = webtest.TestApp(app) diff --git a/tests/framework_flask/conftest.py b/tests/framework_flask/conftest.py index 71d482520e..abf1248178 100644 --- a/tests/framework_flask/conftest.py +++ b/tests/framework_flask/conftest.py @@ -12,7 +12,10 @@ # See the License for the specific language governing permissions and # limitations under the License. +import platform + import pytest +from flask import __version__ as flask_version from testing_support.fixtures import (code_coverage_fixture, collector_agent_registration_fixture, collector_available_fixture) @@ -35,3 +38,12 @@ collector_agent_registration = collector_agent_registration_fixture( app_name='Python Agent Test (framework_flask)', default_settings=_default_settings) + + +is_flask_v2 = int(flask_version.split('.')[0]) >= 2 +is_pypy = platform.python_implementation() == "PyPy" +async_handler_support = is_flask_v2 and not is_pypy +skip_if_not_async_handler_support = pytest.mark.skipif( + not async_handler_support, + reason="Requires async handler support. (Flask >=v2.0.0)", +) \ No newline at end of file diff --git a/tests/framework_flask/test_application.py b/tests/framework_flask/test_application.py index d301402c3a..a974a95a67 100644 --- a/tests/framework_flask/test_application.py +++ b/tests/framework_flask/test_application.py @@ -20,6 +20,8 @@ from newrelic.packages import six +from conftest import async_handler_support, skip_if_not_async_handler_support + try: # The __version__ attribute was only added in 0.7.0. # Flask team does not use semantic versioning during development. @@ -48,7 +50,10 @@ def target_application(): # functions are different between Python 2 and 3, with the latter # showing scope in path. - from _test_application import _test_application + if not async_handler_support: + from _test_application import _test_application + else: + from _test_application_async import _test_application return _test_application @@ -87,7 +92,6 @@ def target_application(): ('FunctionNode', []), ) - @validate_transaction_errors(errors=[]) @validate_transaction_metrics('_test_application:index_page', scoped_metrics=_test_application_index_scoped_metrics) @@ -97,6 +101,23 @@ def test_application_index(): response = application.get('/index') response.mustcontain('INDEX RESPONSE') +_test_application_async_scoped_metrics = [ + ('Function/flask.app:Flask.wsgi_app', 1), + ('Python/WSGI/Application', 1), + ('Python/WSGI/Response', 1), + ('Python/WSGI/Finalize', 1), + ('Function/_test_application_async:async_page', 1), + ('Function/werkzeug.wsgi:ClosingIterator.close', 1)] + +@skip_if_not_async_handler_support +@validate_transaction_errors(errors=[]) +@validate_transaction_metrics('_test_application_async:async_page', + scoped_metrics=_test_application_async_scoped_metrics) +@validate_tt_parenting(_test_application_index_tt_parenting) +def test_application_async(): + application = target_application() + response = application.get('/async') + response.mustcontain('ASYNC RESPONSE') _test_application_endpoint_scoped_metrics = [ ('Function/flask.app:Flask.wsgi_app', 1), @@ -107,7 +128,6 @@ def test_application_index(): ('Function/werkzeug.wsgi:ClosingIterator.close', 1)] -@requires_endpoint_decorator @validate_transaction_errors(errors=[]) @validate_transaction_metrics('_test_application:endpoint_page', scoped_metrics=_test_application_endpoint_scoped_metrics) @@ -124,11 +144,10 @@ def test_application_endpoint(): ('Python/WSGI/Finalize', 1), ('Function/_test_application:error_page', 1), ('Function/flask.app:Flask.handle_exception', 1), - ('Function/werkzeug.wsgi:ClosingIterator.close', 1)] + ('Function/werkzeug.wsgi:ClosingIterator.close', 1), + ('Function/flask.app:Flask.handle_user_exception', 1), + ('Function/flask.app:Flask.handle_user_exception', 1)] -if is_gt_flask060: - _test_application_error_scoped_metrics.extend([ - ('Function/flask.app:Flask.handle_user_exception', 1)]) if six.PY3: _test_application_error_errors = ['builtins:RuntimeError'] @@ -151,11 +170,8 @@ def test_application_error(): ('Python/WSGI/Finalize', 1), ('Function/_test_application:abort_404_page', 1), ('Function/flask.app:Flask.handle_http_exception', 1), - ('Function/werkzeug.wsgi:ClosingIterator.close', 1)] - -if is_gt_flask060: - _test_application_abort_404_scoped_metrics.extend([ - ('Function/flask.app:Flask.handle_user_exception', 1)]) + ('Function/werkzeug.wsgi:ClosingIterator.close', 1), + ('Function/flask.app:Flask.handle_user_exception', 1)] @validate_transaction_errors(errors=[]) @@ -173,11 +189,8 @@ def test_application_abort_404(): ('Python/WSGI/Finalize', 1), ('Function/_test_application:exception_404_page', 1), ('Function/flask.app:Flask.handle_http_exception', 1), - ('Function/werkzeug.wsgi:ClosingIterator.close', 1)] - -if is_gt_flask060: - _test_application_exception_404_scoped_metrics.extend([ - ('Function/flask.app:Flask.handle_user_exception', 1)]) + ('Function/werkzeug.wsgi:ClosingIterator.close', 1), + ('Function/flask.app:Flask.handle_user_exception', 1)] @validate_transaction_errors(errors=[]) @@ -194,11 +207,8 @@ def test_application_exception_404(): ('Python/WSGI/Response', 1), ('Python/WSGI/Finalize', 1), ('Function/flask.app:Flask.handle_http_exception', 1), - ('Function/werkzeug.wsgi:ClosingIterator.close', 1)] - -if is_gt_flask060: - _test_application_not_found_scoped_metrics.extend([ - ('Function/flask.app:Flask.handle_user_exception', 1)]) + ('Function/werkzeug.wsgi:ClosingIterator.close', 1), + ('Function/flask.app:Flask.handle_user_exception', 1)] @validate_transaction_errors(errors=[]) @@ -235,11 +245,8 @@ def test_application_render_template_string(): ('Python/WSGI/Finalize', 1), ('Function/_test_application:template_not_found', 1), ('Function/flask.app:Flask.handle_exception', 1), - ('Function/werkzeug.wsgi:ClosingIterator.close', 1)] - -if is_gt_flask060: - _test_application_render_template_not_found_scoped_metrics.extend([ - ('Function/flask.app:Flask.handle_user_exception', 1)]) + ('Function/werkzeug.wsgi:ClosingIterator.close', 1), + ('Function/flask.app:Flask.handle_user_exception', 1)] @validate_transaction_errors(errors=['jinja2.exceptions:TemplateNotFound']) diff --git a/tests/framework_flask/test_blueprints.py b/tests/framework_flask/test_blueprints.py index bfc1e00e12..50af612b60 100644 --- a/tests/framework_flask/test_blueprints.py +++ b/tests/framework_flask/test_blueprints.py @@ -19,21 +19,11 @@ from newrelic.packages import six -try: - # The __version__ attribute was only added in 0.7.0. - # Flask team does not use semantic versioning during development. - from flask import __version__ as flask_version - is_gt_flask080 = 'dev' in flask_version or tuple( - map(int, flask_version.split('.')))[:2] > (0, 8) -except ImportError: - is_gt_flask080 = False +from conftest import is_flask_v2 as nested_blueprint_support -# Technically parts of blueprints support is available in older -# versions, but just check with latest versions. The instrumentation -# always checks for presence of required functions before patching. +skip_if_not_nested_blueprint_support = pytest.mark.skipif(not nested_blueprint_support, + reason="Requires nested blueprint support. (Flask >=v2.0.0)") -requires_blueprint = pytest.mark.skipif(not is_gt_flask080, - reason="The blueprint mechanism is not supported.") def target_application(): # We need to delay Flask application creation because of ordering @@ -66,7 +56,6 @@ def target_application(): ('Function/flask.app:Flask.do_teardown_appcontext', 1), ('Function/werkzeug.wsgi:ClosingIterator.close', 1)] -@requires_blueprint @validate_transaction_errors(errors=[]) @validate_transaction_metrics('_test_blueprints:index_page', scoped_metrics=_test_blueprints_index_scoped_metrics) @@ -90,7 +79,6 @@ def test_blueprints_index(): ('Function/flask.app:Flask.do_teardown_appcontext', 1), ('Function/werkzeug.wsgi:ClosingIterator.close', 1)] -@requires_blueprint @validate_transaction_errors(errors=[]) @validate_transaction_metrics('_test_blueprints:endpoint_page', scoped_metrics=_test_blueprints_endpoint_scoped_metrics) @@ -98,3 +86,30 @@ def test_blueprints_endpoint(): application = target_application() response = application.get('/endpoint') response.mustcontain('BLUEPRINT ENDPOINT RESPONSE') + + +_test_blueprints_nested_scoped_metrics = [ + ('Function/flask.app:Flask.wsgi_app', 1), + ('Python/WSGI/Application', 1), + ('Python/WSGI/Response', 1), + ('Python/WSGI/Finalize', 1), + ('Function/_test_blueprints:nested_page', 1), + ('Function/flask.app:Flask.preprocess_request', 1), + ('Function/_test_blueprints:before_app_request', 1), + ('Function/_test_blueprints:before_request', 1), + ('Function/flask.app:Flask.process_response', 1), + ('Function/_test_blueprints:after_request', 1), + ('Function/_test_blueprints:after_app_request', 1), + ('Function/flask.app:Flask.do_teardown_request', 1), + ('Function/_test_blueprints:teardown_app_request', 1), + ('Function/_test_blueprints:teardown_request', 1), + ('Function/flask.app:Flask.do_teardown_appcontext', 1), + ('Function/werkzeug.wsgi:ClosingIterator.close', 1)] + +@skip_if_not_nested_blueprint_support +@validate_transaction_errors(errors=[]) +@validate_transaction_metrics('_test_blueprints:nested_page') +def test_blueprints_nested(): + application = target_application() + response = application.get('/parent/child/nested') + response.mustcontain('PARENT NESTED RESPONSE') diff --git a/tests/framework_flask/test_middleware.py b/tests/framework_flask/test_middleware.py index 34c00bdf36..e7f01e857e 100644 --- a/tests/framework_flask/test_middleware.py +++ b/tests/framework_flask/test_middleware.py @@ -17,24 +17,6 @@ from testing_support.fixtures import (validate_transaction_metrics, validate_transaction_errors, override_application_settings) -try: - # The __version__ attribute was only added in 0.7.0. - # Flask team does not use semantic versioning during development. - from flask import __version__ as flask_version - is_gt_flask080 = 'dev' in flask_version or tuple( - map(int, flask_version.split('.')))[:2] > (0, 8) -except ValueError: - is_gt_flask080 = True -except ImportError: - is_gt_flask080 = False - -# Technically parts of before/after support is available in older -# versions, but just check with latest versions. The instrumentation -# always checks for presence of required functions before patching. - -requires_before_after = pytest.mark.skipif(not is_gt_flask080, - reason="Not all before/after methods are supported.") - def target_application(): # We need to delay Flask application creation because of ordering # issues whereby the agent needs to be initialised before Flask is @@ -66,7 +48,6 @@ def target_application(): ('Function/_test_middleware:teardown_appcontext', 1), ('Function/werkzeug.wsgi:ClosingIterator.close', 1)] -@requires_before_after @validate_transaction_errors(errors=[]) @validate_transaction_metrics('_test_middleware:index_page', scoped_metrics=_test_application_app_middleware_scoped_metrics) diff --git a/tests/framework_flask/test_not_found.py b/tests/framework_flask/test_not_found.py index 25f61ecb7e..22ad5efcde 100644 --- a/tests/framework_flask/test_not_found.py +++ b/tests/framework_flask/test_not_found.py @@ -17,16 +17,6 @@ from testing_support.fixtures import (validate_transaction_metrics, validate_transaction_errors) -try: - # The __version__ attribute was only added in 0.7.0. - from flask import __version__ as flask_version - is_gt_flask060 = True -except ImportError: - is_gt_flask060 = False - -requires_error_handler = pytest.mark.skipif(not is_gt_flask060, - reason="The error handler decorator is not supported.") - def target_application(): # We need to delay Flask application creation because of ordering # issues whereby the agent needs to be initialised before Flask is @@ -47,13 +37,10 @@ def target_application(): ('Python/WSGI/Finalize', 1), ('Function/_test_not_found:page_not_found', 1), ('Function/flask.app:Flask.handle_http_exception', 1), - ('Function/werkzeug.wsgi:ClosingIterator.close', 1)] + ('Function/werkzeug.wsgi:ClosingIterator.close', 1), + ('Function/flask.app:Flask.handle_user_exception', 1)] -if is_gt_flask060: - _test_error_handler_not_found_scoped_metrics.extend([ - ('Function/flask.app:Flask.handle_user_exception', 1)]) -@requires_error_handler @validate_transaction_errors(errors=[]) @validate_transaction_metrics('_test_not_found:page_not_found', scoped_metrics=_test_error_handler_not_found_scoped_metrics) diff --git a/tests/framework_flask/test_user_exceptions.py b/tests/framework_flask/test_user_exceptions.py index 213fe2087e..5c8f3a6587 100644 --- a/tests/framework_flask/test_user_exceptions.py +++ b/tests/framework_flask/test_user_exceptions.py @@ -17,16 +17,6 @@ from testing_support.fixtures import (validate_transaction_metrics, validate_transaction_errors) -try: - # The __version__ attribute was only added in 0.7.0. - from flask import __version__ as flask_version - is_gt_flask060 = True -except ImportError: - is_gt_flask060 = False - -requires_error_handler = pytest.mark.skipif(not is_gt_flask060, - reason="The error handler decorator is not supported.") - def target_application(): # We need to delay Flask application creation because of ordering # issues whereby the agent needs to be initialised before Flask is @@ -47,13 +37,11 @@ def target_application(): ('Python/WSGI/Finalize', 1), ('Function/_test_user_exceptions:page_not_found', 1), ('Function/_test_user_exceptions:error_page', 1), - ('Function/werkzeug.wsgi:ClosingIterator.close', 1)] + ('Function/werkzeug.wsgi:ClosingIterator.close', 1), + ('Function/flask.app:Flask.handle_user_exception', 1)] + -if is_gt_flask060: - _test_user_exception_handler_scoped_metrics.extend([ - ('Function/flask.app:Flask.handle_user_exception', 1)]) -@requires_error_handler @validate_transaction_errors(errors=['_test_user_exceptions:UserException']) @validate_transaction_metrics('_test_user_exceptions:error_page', scoped_metrics=_test_user_exception_handler_scoped_metrics) diff --git a/tests/framework_flask/test_views.py b/tests/framework_flask/test_views.py index e13f3561ec..5f491dfb9e 100644 --- a/tests/framework_flask/test_views.py +++ b/tests/framework_flask/test_views.py @@ -17,14 +17,20 @@ from testing_support.fixtures import (validate_transaction_metrics, validate_transaction_errors, override_application_settings) -try: - import flask.views - has_view_support = True -except ImportError: - has_view_support = False +from conftest import async_handler_support, skip_if_not_async_handler_support + + +scoped_metrics = [ + ('Function/flask.app:Flask.wsgi_app', 1), + ('Python/WSGI/Application', 1), + ('Python/WSGI/Response', 1), + ('Python/WSGI/Finalize', 1), + ('Function/flask.app:Flask.preprocess_request', 1), + ('Function/flask.app:Flask.process_response', 1), + ('Function/flask.app:Flask.do_teardown_request', 1), + ('Function/werkzeug.wsgi:ClosingIterator.close', 1), +] -skip_if_no_view_support = pytest.mark.skipif(not has_view_support, - reason='This flask version does hot support class based views.') def target_application(): # We need to delay Flask application creation because of ordering @@ -36,65 +42,52 @@ def target_application(): # functions are different between Python 2 and 3, with the latter # showing scope in path. - from _test_views import _test_application + if not async_handler_support: + from _test_views import _test_application + else: + from _test_views_async import _test_application return _test_application -_test_class_based_view_scoped_metrics = [ - ('Function/flask.app:Flask.wsgi_app', 1), - ('Python/WSGI/Application', 1), - ('Python/WSGI/Response', 1), - ('Python/WSGI/Finalize', 1), - ('Function/flask.app:Flask.preprocess_request', 1), - ('Function/flask.app:Flask.process_response', 1), - ('Function/flask.app:Flask.do_teardown_request', 1), - ('Function/werkzeug.wsgi:ClosingIterator.close', 1), -] - -@skip_if_no_view_support @validate_transaction_errors(errors=[]) @validate_transaction_metrics('_test_views:test_view', - scoped_metrics=_test_class_based_view_scoped_metrics) + scoped_metrics=scoped_metrics) def test_class_based_view(): application = target_application() response = application.get('/view') response.mustcontain('VIEW RESPONSE') -_test_get_method_view_scoped_metrics = [ - ('Function/flask.app:Flask.wsgi_app', 1), - ('Python/WSGI/Application', 1), - ('Python/WSGI/Response', 1), - ('Python/WSGI/Finalize', 1), - ('Function/flask.app:Flask.preprocess_request', 1), - ('Function/flask.app:Flask.process_response', 1), - ('Function/flask.app:Flask.do_teardown_request', 1), - ('Function/werkzeug.wsgi:ClosingIterator.close', 1), -] +@pytest.mark.xfail(reason="Currently broken in flask.") +@skip_if_not_async_handler_support +@validate_transaction_errors(errors=[]) +@validate_transaction_metrics('_test_views_async:test_async_view', + scoped_metrics=scoped_metrics) +def test_class_based_async_view(): + application = target_application() + response = application.get('/async_view') + response.mustcontain('ASYNC VIEW RESPONSE') -@skip_if_no_view_support @validate_transaction_errors(errors=[]) @validate_transaction_metrics('_test_views:test_methodview', - scoped_metrics=_test_get_method_view_scoped_metrics) + scoped_metrics=scoped_metrics) def test_get_method_view(): application = target_application() response = application.get('/methodview') response.mustcontain('METHODVIEW GET RESPONSE') -_test_post_method_view_scoped_metrics = [ - ('Function/flask.app:Flask.wsgi_app', 1), - ('Python/WSGI/Application', 1), - ('Python/WSGI/Response', 1), - ('Python/WSGI/Finalize', 1), - ('Function/flask.app:Flask.preprocess_request', 1), - ('Function/flask.app:Flask.process_response', 1), - ('Function/flask.app:Flask.do_teardown_request', 1), - ('Function/werkzeug.wsgi:ClosingIterator.close', 1), -] - -@skip_if_no_view_support @validate_transaction_errors(errors=[]) @validate_transaction_metrics('_test_views:test_methodview', - scoped_metrics=_test_post_method_view_scoped_metrics) + scoped_metrics=scoped_metrics) def test_post_method_view(): application = target_application() response = application.post('/methodview') response.mustcontain('METHODVIEW POST RESPONSE') + +@pytest.mark.xfail(reason="Currently broken in flask.") +@skip_if_not_async_handler_support +@validate_transaction_errors(errors=[]) +@validate_transaction_metrics('_test_views_async:test_async_methodview', + scoped_metrics=scoped_metrics) +def test_get_method_async_view(): + application = target_application() + response = application.get('/async_methodview') + response.mustcontain('ASYNC METHODVIEW GET RESPONSE') diff --git a/tox.ini b/tox.ini index 53270e0c90..af17753bff 100644 --- a/tox.ini +++ b/tox.ini @@ -112,8 +112,7 @@ envlist = python-framework_fastapi-{py36,py37,py38}, python-framework_flask-{pypy,py27}-flask0012, python-framework_flask-{pypy,py27,py36,py37,py38,pypy3}-flask0101, - ; disabled until v2 support is released - ; python-framework_flask-{py36,py37,py38,pypy3}-flaskmaster, + python-framework_flask-{py37,py38,pypy3}-flask{latest,master}, grpc-framework_grpc-{py27,py36}-grpc0125, grpc-framework_grpc-{py27,py36,py37,py38,py39}-grpclatest, python-framework_pyramid-{pypy,py27,py38}-Pyramid0104, @@ -234,9 +233,9 @@ deps = framework_flask: Flask-Compress framework_flask-flask0012: flask<0.13 framework_flask-flask0101: flask<1.2 - framework_flask-flasklatest: flask + framework_flask-flasklatest: flask[async] framework_flask-flaskmaster: https://github.com/pallets/werkzeug/archive/main.zip - framework_flask-flaskmaster: https://github.com/pallets/flask/archive/main.zip + framework_flask-flaskmaster: https://github.com/pallets/flask/archive/main.zip#egg=flask[async] framework_grpc-grpc0125: grpcio<1.26 framework_grpc-grpc0125: grpcio-tools<1.26 framework_grpc-grpclatest: grpcio