Slide 1

Slide 1 text

Demystifying Flask's Application and Request Contexts with pytest Patrick Kennedy FlaskCon 2020

Slide 2

Slide 2 text

I’m Patrick Kennedy 2 Software Engineer: ● Real-time Applications ● Web Applications Languages: ● C++, Python, JavaScript, HTML/CSS Author at TestDriven.io: ● Learn Vue by Building and Deploying a CRUD App ○ https://testdriven.io/courses/learn-vue/ - released in January 2020 ● Learn Flask by Building and Deploying a Stock Portfolio App ○ https://testdriven.io/courses/learn-flask/ - upcoming release in August 2020

Slide 3

Slide 3 text

Outline ● How the Application and Request Contexts are processed when a request is handled in Flask ● Example of the Application Context using pytest 3

Slide 4

Slide 4 text

Contexts in Flask 4

Slide 5

Slide 5 text

What is a Context? ● Contexts are used to keep track of the data needed to execute a software operation ● In Flask, contexts provide the necessary data when processing requests or CLI* commands 5 * CLI - Command-Line Interface

Slide 6

Slide 6 text

First point Web Server Flask Application Web Browser (Firefox, Chrome, etc.) Request: GET ‘/’ 200 (OK) Status Code Response: “Hello World!” Request Processing in Flask 6

Slide 7

Slide 7 text

When a request is received, there are two contexts: 1. Application context - keeps track of the application-level data (configuration variables, logger, etc.) 2. Request context - keeps track of the request-level data (URL, HTTP method, headers, request data, session, etc.) Both of these contexts get created when a Flask application receives a request Contexts 7

Slide 8

Slide 8 text

Request Handling - Overview 8

Slide 9

Slide 9 text

Request Handling - Step 1 9 A request is received by the Web Server

Slide 10

Slide 10 text

Request Handling - Step 2 10 A worker is spawned to handle the request ● Worker can be a thread, process, or coroutine ● Worker handles one request at a time

Slide 11

Slide 11 text

Request Handling - Step 3 11 The Flask application pushes the application context and request context onto their respective stacks

Slide 12

Slide 12 text

Request Handling - Steps 4 and 5 12 Proxies are made available to the view function Proxies are unique to each worker thanks to the stacks being context-local objects!

Slide 13

Slide 13 text

Proxies @app.route('/add_item', methods=['GET', 'POST']) def add_item(application_context, request_context): if request_context.method == 'POST': # Save the form data to the database ... application_context.logger.info(f"Added new item ({ request_context.form['item_name'] })!") ... from flask import current_app, request @app.route('/add_item', methods=['GET', 'POST']) def add_item(): if request.method == 'POST': # Save the form data to the database ... current_app.logger.info(f"Added new item ({ request.form['item_name'] })!") ... 13 Instead of needing to pass in the contexts into each view function Flask provides proxies to the application and request contexts!

Slide 14

Slide 14 text

Request Handling - Step 6 14 After the response is generated, the request context and application context are popped from their respective stacks

Slide 15

Slide 15 text

Example of the Application Context using pytest 15

Slide 16

Slide 16 text

pytest Example def test_index_page(test_client): """ GIVEN a Flask application WHEN the '/' page is requested (GET) THEN check the response is valid """ response = test_client.get('/') assert response.status_code == 200 assert b'Welcome!' in response.data 16 test_app.py “Python Testing with pytest” - Brian Okken: ● https://pragprog.com/titles/bopytest/

Slide 17

Slide 17 text

Fixture def test_index_page(test_client): """ GIVEN a Flask application WHEN the '/' page is requested (GET) THEN check the response is valid """ response = test_client.get('/') assert response.status_code == 200 assert b'Welcome!' in response.data import pytest from project import create_app @pytest.fixture(scope='module') def test_client(): flask_app = create_app() # Create a test client using the Flask application # configured for testing testing_client = flask_app.test_client() return testing_client 17 test_app.py conftest.py Fixtures initialize tests to a known state in order to run tests in a predictable and repeatable manner.

Slide 18

Slide 18 text

Adding a log message... from flask import current_app @pytest.fixture(scope='module') def test_client(): flask_app = create_app() # Create a test client using the Flask application configured for testing testing_client = flask_app.test_client() current_app.logger.info('In the test_client fixture...') return testing_client 18 conftest.py Adding a log message to show when this fixture executes...

Slide 19

Slide 19 text

Error! (venv)$ pytest … RuntimeError: Working outside of application context. 19 shell

Slide 20

Slide 20 text

What went wrong? 20 There is no application context on the stack!

Slide 21

Slide 21 text

How can this error be fixed? @pytest.fixture(scope='module') def test_client(): flask_app = create_app() # Create a test client using the Flask application configured for testing testing_client = flask_app.test_client() # Establish an application context ctx = flask_app.app_context() # Push the application context to the application context stack ctx.push() current_app.logger.info('In the test_client fixture...') # Pop the application context from the stack ctx.pop() return testing_client 21 Push the application context to the stack BEFORE accessing the logger Pop the application context

Slide 22

Slide 22 text

Better Approach… Context Manager @pytest.fixture(scope='module') def test_client(): flask_app = create_app() # Create a test client using the Flask application configured for testing with flask_app.test_client() as testing_client: # Establish an application context before accessing the logger with flask_app.app_context(): current_app.logger.info('In the test_client fixture...') yield testing_client # this is where the testing happens! 22 Context manager

Slide 23

Slide 23 text

Conclusion ● The Application and Request Contexts provide the necessary data when processing requests or CLI commands ● Flask provides the current_app and request proxies to avoid needing to pass in the contexts into each view or CLI function 23

Slide 24

Slide 24 text

Thank you! 24 Patrick Kennedy Email: [email protected] Twitter: patkennedy79 Personal Blog: https://www.patricksoftwareblog.com Learn Flask by Building and Deploying a Stock Portfolio App: https://testdriven.io/courses/learn-flask/

Slide 25

Slide 25 text

Reference Material 25

Slide 26

Slide 26 text

Reference Material 26 Context Locals (Werkzeug Documentation): ● https://werkzeug.palletsprojects.com/en/1.0.x/local/ Application Context (Flask Documentation): ● https://flask.palletsprojects.com/en/1.1.x/appcontext/ Request Context (Flask Documentation): ● https://flask.palletsprojects.com/en/1.1.x/reqcontext/ pytest Documentation: ● https://docs.pytest.org