Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Flask v2 Support #242

Merged
merged 5 commits into from
Jun 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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