Slide 1

Slide 1 text

Demystifying Flask's Application and Request Contexts with pytest Patrick Kennedy SF Python Meetup - October 2020

Slide 2

Slide 2 text

I’m Patrick Kennedy 2 Email: pkennedy@hey.com | Twitter: patkennedy79 Personal Blog: https://www.patricksoftwareblog.com Software Engineer: ● Real-time Applications ● Web Applications Programming 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 ● Developing Web Applications with Python and Flask ○ https://testdriven.io/courses/learn-flask/ - released 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

Flask - Introduction ● Web framework written in Python ○ Author: Armin Ronacher ○ Core Maintainer: David Lord ○ Current release: 1.1.2 ● “Micro” Framework ○ Provides core functionality for developing web applications ○ Additional functionality is available via extensions: ■ SQL database interface: Flask-SQLAlchemy ■ Form validation: Flask-WTF 4 https://palletsprojects.com/p/flask/

Slide 5

Slide 5 text

Contexts in Flask 5

Slide 6

Slide 6 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 6 * CLI - Command-Line Interface

Slide 7

Slide 7 text

Request Processing in Flask 7 Flask Application Web Browser (Firefox, Chrome, etc.) Request: GET ‘/’ Status Code: 200 (OK) Response (HTML, CSS) WSGI Server Web Server * WSGI - Web Server Gateway Interface

Slide 8

Slide 8 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 8

Slide 9

Slide 9 text

Request Handling - Overview 9

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

Request Handling - Step 4 13 Proxies are made available to the view function Proxies are unique to each worker thanks to the stacks being context-local objects!

Slide 14

Slide 14 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'] })!") ... 14 Instead of needing to pass in the contexts into each view function Flask provides proxies to the application and request contexts!

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

Request Handling - Recap 16

Slide 17

Slide 17 text

Example of the Application Context using pytest 17

Slide 18

Slide 18 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 18 test_app.py “Python Testing with pytest” - Brian Okken: ● https://pragprog.com/titles/bopytest/

Slide 19

Slide 19 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 19 test_app.py conftest.py Fixtures initialize tests to a known state in order to run tests in a predictable and repeatable manner.

Slide 20

Slide 20 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 20 conftest.py Adding a log message to show when this fixture executes...

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 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 23 Push the application context to the stack BEFORE accessing the logger Pop the application context

Slide 24

Slide 24 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! 24 Context manager

Slide 25

Slide 25 text

Conclusion ● Application and Request Contexts provide the necessary data when processing requests or CLI commands in Flask: ○ Flask framework handles the processing of these contexts, but... ○ It’s important to know how to use these contexts to solve issues during development 25

Slide 26

Slide 26 text

Thank you! 26 https://testdriven.io/blog/flask-contexts/ https://testdriven.io/blog/flask-contexts-advanced/

Slide 27

Slide 27 text

Reference Material 27

Slide 28

Slide 28 text

Reference Material 28 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