How can I test a custom Flask error page? - python

I'm attempting to test a custom error page in flask (404 in this case).
I've defined my custom 404 page as such:
#app.errorhandler(404)
def page_not_found(e):
print "Custom 404!"
return render_template('404.html'), 404
This works perfectly when hitting an unknown page in a browser (I see Custom 404! in stdout and my custom content is visible). However, when trying to trigger a 404 via unittest with nose, the standard/server 404 page renders. I get no log message or the custom content I am trying to test for.
My test case is defined like so:
class MyTestCase(TestCase):
def setUp(self):
self.app = create_app()
self.app_context = self.app.app_context()
self.app.config.from_object('config.TestConfiguration')
self.app.debug = False # being explicit to debug what's going on...
self.app_context.push()
self.client = self.app.test_client()
def tearDown(self):
self.app_context.pop()
def test_custom_404(self):
path = '/non_existent_endpoint'
response = self.client.get(path)
self.assertEqual(response.status_code, 404)
self.assertIn(path, response.data)
I have app.debug explicitly set to False on my test app. Is there something else I have to explicitly set?

After revisiting this with fresh eyes, it's obvious that the problem is in my initialization of the application and not in my test/configuration. My app's __init__.py basically looks like this:
def create_app():
app = Flask(__name__)
app.config.from_object('config.BaseConfiguration')
app.secret_key = app.config.get('SECRET_KEY')
app.register_blueprint(main.bp)
return app
app = create_app()
# Custom error pages
#app.errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 404
Notice that the error handler is attached to #app outside of create_app(), the method I'm calling in my TestCase.setUp() method.
If I simply move that error handler up into the create_app() method, everything works fine... but it feels a bit gross? Maybe?
def create_app():
app = Flask(__name__)
app.config.from_object('config.BaseConfiguration')
app.secret_key = app.config.get('SECRET_KEY')
app.register_blueprint(main.bp)
# Custom error pages
#app.errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 404
return app
This ultimately answers my question and fixes my problem, but I'd love other thoughts on how to differently register those errorhandlers.

The Flask application object has an error_handler_spec property that can be mocked to solve this:
A dictionary of all registered error handlers. The key is None for
error handlers active on the application, otherwise the key is the
name of the blueprint. Each key points to another dictionary where the
key is the status code of the http exception. The special key None
points to a list of tuples where the first item is the class for the
instance check and the second the error handler function.
So something like this in your test method should work:
mock_page_not_found = mock.magicMock()
mock_page_not_found.return_value = {}, 404
with mock.patch.dict(self.app.error_handler_spec[None], {404: mock_page_not_found}):
path = '/non_existent_endpoint'
response = self.client.get(path)
self.assertEqual(response.status_code, 404)
mock_page_not_found.assert_called_once()

In reference to the following comment you made "If I simply move that error handler up into the create_app() method, everything works fine... but it feels a bit gross? Maybe?":
You can define a function to register an error handler and call that in your create_app function:
def create_app():
app = Flask(__name__)
app.config.from_object('config.BaseConfiguration')
app.secret_key = app.config.get('SECRET_KEY')
app.register_blueprint(main.bp)
register_error_pages(app)
return app
app = create_app()
# Custom error pages
def register_error_pages(app):
#app.errorhandler(404)
def page_not_found(e):
return render_template('404.html'), 404
That way if you have more custom error handlers you want to register (403, 405, 500) you can define them inside the register_error_pages function instead of your create_app function.

Related

Optional url segment pattern in a Pyramid route

I'm trying to create a website with optional url sub-paths:
/user - Returns general information on users
/user/edit - Edits the user
I've tried setting:
config.add_route('user', '/user/{action}')
#view_defaults(route_name="user")
class UserViews():
# not sure what (if anything) to put in #view_config here...
def user_general(self):
return Response("General User Info"
#view_config(match_param="action=edit")
def edit(self):
return Response("Editing user")
However while this works for /user/edit, it returns a 404 for /user
It also fails in the same way if I set 2 explicit routes with a shared path - e.g.:
config.add_route('login', '/user')
config.add_route('edit_user', '/user/edit')
I've tried things like setting match_params="action=" but can't get it to work.
Any ideas on how this can be achieved?
user_general inherits the default route configuration of the class, which requires an {action} match param. When you do not supply that in the request, the route for that view will never match, returning a 404 not found response.
You need to add a decorator with the route_name argument to user_general to override the default route for the view.
#view_config(
route_name="user"
)
def user_general(self):
The following works for me as a complete example with some minor explicit naming conventions.
from wsgiref.simple_server import make_server
from pyramid.config import Configurator
from pyramid.response import Response
from pyramid.view import view_config, view_defaults
#view_defaults(route_name="user_action")
class UserViews():
def __init__(self, context, request):
self.request = request
self.context = context
#view_config(
route_name="user_get",
request_method="GET"
)
def get_user(request):
return Response("I got you, Babe!")
#view_config(
match_param="action=edit"
)
def edit(self):
return Response("Don't ever change, Babe!")
if __name__ == "__main__":
with Configurator() as config:
config.add_route("user_get", "/user")
config.add_route('user_action', '/user/{action}')
config.scan()
app = config.make_wsgi_app()
server = make_server("0.0.0.0", 6543, app)
server.serve_forever()

Overwrite route in flask blueprint

There is a blueprint with a lot of useful routes defined, but I have no control over it (can not change it's code in any way)
Trying to reuse it in a different app but one of the blueprint's endpoints must be overloaded. How can I achieve that?
I tried just adding a new route to blueprint on top of the existing one:
#blueprint.route('/my/route', methods=['PUT', 'POST'])
def my_new_view_func(program, project):
# some new behavior for the endpoint
As the result there is duplicate url_rule in app.url_map.iter_rules():
<Rule '/my/route' (PUT, POST) -> my_view_func>,
<Rule '/my/route' (PUT, POST) -> my_new_view_func>,
and when requesting /my/route old viewer my_view_func gets executed
Can I somehow get rid of the old url rule? Or maybe there is a better way to overwrite the route?
There are 2 solutions which I found. First:
from flask import Flask, Blueprint
simple_page = Blueprint('simple_page', __name__, )
#simple_page.route('/my/route/')
def my():
# for example it's a registered route somewhere...
return 'default'
#simple_page.route('/my/route/')
def new_my():
# new endpoint / should works instead my()
return 'new'
# map of views which we won't register in Flask app
# you can store this somewhere in settings
SKIP_VIEWS = (
# route, view function
('/my/route/', my, ),
)
class CustomFlask(Flask):
def add_url_rule(self, rule, endpoint=None, view_func=None, **options):
# Flask registers views when an application starts
# do not add view from SKIP_VIEWS
for rule_, view_func_ in SKIP_VIEWS: # type: str, func
if rule_ == rule and view_func == view_func_:
return
return super(CustomFlask, self).add_url_rule(rule, endpoint, view_func, **options)
app = CustomFlask(__name__)
app.register_blueprint(simple_page)
app.run(debug=True)
Second way:
two.py - default blueprint with endpoint
from flask import Blueprint
bp_two = Blueprint('simple_page2', __name__, )
#bp_two.route('/my/route/')
def default():
return 'default'
test.py - your blueprint + app
from flask import Flask, Blueprint
from two import bp_two
your_bp = Blueprint('simple_page', __name__, )
#your_bp.route('/my/route/')
def new_route():
return 'new'
app = Flask(__name__)
# register blueprint and turn off '/my/route/' endpoint
app.register_blueprint(bp_two, **{'url_defaults': {'/my/route/': None}})
app.register_blueprint(your_bp)
app.run(debug=True)
Run app. Open /my/route/. You will see that default endpoint wasn't add/works.
Hope this helps.

Generate URLs for Flask test client with url_for function

I'm trying to write unit tests for a Flask app using pytest. I have an app factory:
def create_app():
from flask import Flask
app = Flask(__name__)
app.config.from_object('config')
import os
app.secret_key = os.urandom(24)
from models import db
db.init_app(app)
return app
And a test class:
class TestViews(object):
#classmethod
def setup_class(cls):
cls.app = create_app()
cls.app.testing = True
cls.client = cls.app.test_client()
#classmethod
def teardown_class(cls):
cls.app_context.pop()
def test_create_user(self):
"""
Tests the creation of a new user.
"""
view = TestViews.client.get(url_for('create_users')).status_code == 200
but when I run my tests I get the following error:
RuntimeError: Attempted to generate a URL without the application context being pushed. This has to be executed when application context is available.
Googling this tells me (I think) that using the test client should create an automatic application context. What am I missing?
Making requests with the test client does indeed push an app context (indirectly). However, you're confusing the fact that url_for is visually inside the test request call with the idea that it is actually called inside. The url_for call is evaluated first, the result is passed to client.get.
url_for is typically for generating URLs within the app, unit tests are external. Typically, you just write exactly the URL you're trying to test in the request instead of generating it.
self.client.get('/users/create')
If you really want to use url_for here, you must do it in an app context. Note that when you're in an app context but not a request context, you must set the SERVER_NAME config and also pass _external=False. But again, you should probably just write out the URL you're trying to test.
app.config['SERVER_NAME'] = 'localhost'
with self.app.app_context():
url = url_for(..., _external=False)
self.client.get(url, ...)
You can call url_for() in test request context that created with app.test_request_context() method. There are three methods to achieve this.
With setup and teardown
Since you have created the setup and teardown method, just like what I normally do with unittest, you can just push a test request context in setup method then pop it in teardown method:
class TestViews(object):
#classmethod
def setup_class(cls):
cls.app = create_app()
cls.app.testing = True
cls.client = cls.app.test_client()
cls.context = cls.app.test_request_context() # create the context object
cls.context.push() # push the context
#classmethod
def teardown_class(cls):
cls.context.pop() # pop the context
def test_create_user(self):
"""
Tests the creation of a new user.
"""
view = TestViews.client.get(url_for('create_users')).status_code == 200
With pytest-flask
Besides, you can also just use pytest-flask. With pytest-flask, you can access to context bound objects (url_for, request, session) without context managers:
def test_app(client):
assert client.get(url_for('myview')).status_code == 200
With autouse fixture
If you don't want to install the plugin, you can just use the following fixtures to do similar things (stolen from the source of pytest-flask):
#pytest.fixture
def app():
app = create_app('testing')
return app
#pytest.fixture(autouse=True)
def _push_request_context(request, app):
ctx = app.test_request_context() # create context
ctx.push() # push
def teardown():
ctx.pop() # pop
request.addfinalizer(teardown) # set teardown

Flask and React routing

I'm building the Flask app with React, I ended up having a problem with routing.
The backend is responsible to be an API, hence some routes look like:
#app.route('/api/v1/do-something/', methods=["GET"])
def do_something():
return something()
and the main route which leads to the React:
#app.route('/')
def index():
return render_template('index.html')
I'm using react-router in the React app, everything works fine, react-router takes me to /something and I get the rendered view, but when I refresh the page on /something then Flask app takes care of this call and I get Not Found error.
What is the best solution? I was thinking about redirecting all calls which are not calling /api/v1/... to / it's not ideal as I will get back the home page of my app, not rendered React view.
We used catch-all URLs for this.
from flask import Flask
app = Flask(__name__)
#app.route('/', defaults={'path': ''})
#app.route('/<path:path>')
def catch_all(path):
return 'You want path: %s' % path
if __name__ == '__main__':
app.run()
You can also go an extra mile and reuse the Flask routing system to match path to the same routes as client so you can embed the data client will need as JSON inside the HTML response.
Maybe as extension to the answers before. This solved the problem for me:
from flask import send_from_directory
#app.route('/', defaults={'path': ''})
#app.route('/<path:path>')
def serve(path):
path_dir = os.path.abspath("../build") #path react build
if path != "" and os.path.exists(os.path.join(path_dir, path)):
return send_from_directory(os.path.join(path_dir), path)
else:
return send_from_directory(os.path.join(path_dir),'index.html')
For some reason, the catch-all URLs did not work for me. I found that using the flask 404 handler results in the exact same thing. It sees the url and passes it down to react where your router will handle it.
#app.errorhandler(404)
def not_found(e):
return app.send_static_file('index.html')
Just to inform handle error 404 and render_template works perfectly for me.
#app.errorhandler(404)
def not_found(e):
return render_template("index.html")
I have to combine both catch-all and 404 handler for it to work properly. I am hosting a react-app in a subpath with its own redirection handler from react-router.
#app.route('/sub-path', defaults={'path': 'index.html'})
#app.route('/sub-path/<path:path>')
def index(path):
return send_from_directory('../react-dir/build', path)
#app.errorhandler(404)
def not_found(e):
return send_from_directory('../react-dir/build','index.html')

Blueprint 404 errorhandler doesn't activate under blueprint's url prefix

I created a blueprint with a 404 error handler. However, when I go to non-existent urls under the blueprint's prefix, the standard 404 page is shown rather than my custom one. How can I make the blueprint handle 404 errors correctly?
The following is a short app that demonstrates the problem. Navigating to http://localhost:5000/simple/asdf will not show the blueprint's error page.
#!/usr/local/bin/python
# coding: utf-8
from flask import *
from config import PORT, HOST, DEBUG
simplepage = Blueprint('simple', __name__, url_prefix='/simple')
#simplepage.route('/')
def simple_root():
return 'This simple page'
#simplepage.errorhandler(404)
def error_simple(err):
return 'This simple error 404', err
app = Flask(__name__)
app.config.from_pyfile('config.py')
app.register_blueprint(simplepage)
#app.route('/', methods=['GET'])
def api_get():
return render_template('index.html')
if __name__ == '__main__':
app.run(host=HOST,
port=PORT,
debug=DEBUG)
The documentation mentions that 404 error handlers will not behave as expected on blueprints. The app handles routing and raises a 404 before the request gets to the blueprint. The 404 handler will still activate for abort(404) because that is happening after routing at the blueprint level.
This is something that could possibly be fixed in Flask (there's an open issue about it). As a workaround, you can do your own error routing within the top-level 404 handler.
from flask import request, render_template
#app.errorhandler(404)
def handle_404(e):
path = request.path
# go through each blueprint to find the prefix that matches the path
# can't use request.blueprint since the routing didn't match anything
for bp_name, bp in app.blueprints.items():
if path.startswith(bp.url_prefix):
# get the 404 handler registered by the blueprint
handler = app.error_handler_spec.get(bp_name, {}).get(404)
if handler is not None:
# if a handler was found, return it's response
return handler(e)
# return a default response
return render_template('404.html'), 404

Categories

Resources