Skip to content

Commit

Permalink
Flask v2 Support (#242)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
TimPansino and umaannamalai authored Jun 7, 2021
1 parent 9c007df commit e2e914c
Show file tree
Hide file tree
Showing 13 changed files with 372 additions and 243 deletions.
256 changes: 166 additions & 90 deletions newrelic/hooks/framework_flask.py

Large diffs are not rendered by default.

16 changes: 4 additions & 12 deletions tests/framework_flask/_test_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
26 changes: 26 additions & 0 deletions tests/framework_flask/_test_application_async.py
Original file line number Diff line number Diff line change
@@ -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)
15 changes: 15 additions & 0 deletions tests/framework_flask/_test_blueprints.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down Expand Up @@ -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)
38 changes: 38 additions & 0 deletions tests/framework_flask/_test_views_async.py
Original file line number Diff line number Diff line change
@@ -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)
12 changes: 12 additions & 0 deletions tests/framework_flask/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)",
)
61 changes: 34 additions & 27 deletions tests/framework_flask/test_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -48,7 +50,10 @@ def target_application():
# functions are different between Python 2 and 3, with the latter
# showing <local> 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


Expand Down Expand Up @@ -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)
Expand All @@ -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),
Expand All @@ -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)
Expand All @@ -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']
Expand All @@ -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=[])
Expand All @@ -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=[])
Expand All @@ -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=[])
Expand Down Expand Up @@ -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'])
Expand Down
45 changes: 30 additions & 15 deletions tests/framework_flask/test_blueprints.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -90,11 +79,37 @@ 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)
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')
19 changes: 0 additions & 19 deletions tests/framework_flask/test_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Loading

0 comments on commit e2e914c

Please sign in to comment.