The comprehensive WSGI web application library providing essential utilities and components for building Python web applications.
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Complete test client and utilities for testing WSGI applications including request simulation, cookie handling, redirect following, and response validation. These tools provide everything needed to thoroughly test web applications without running a server.
The Client class provides a high-level interface for making HTTP requests to WSGI applications in test environments.
class Client:
def __init__(self, application, response_wrapper=None, use_cookies=True, allow_subdomain_redirects=False):
"""
Create a test client for a WSGI application.
Parameters:
- application: WSGI application to test
- response_wrapper: Response class to wrap results (defaults to TestResponse)
- use_cookies: Whether to persist cookies between requests
- allow_subdomain_redirects: Allow following redirects to subdomains
"""
def open(self, *args, **kwargs):
"""
Make a request to the application.
Can be called with:
- open(path, method='GET', **kwargs)
- open(EnvironBuilder, **kwargs)
- open(Request, **kwargs)
Parameters:
- path: URL path to request
- method: HTTP method
- data: Request body data (string, bytes, dict, or file)
- json: JSON data to send (sets Content-Type automatically)
- headers: Request headers (dict or Headers object)
- query_string: URL parameters (string or dict)
- content_type: Content-Type header value
- auth: Authorization (username, password) tuple or Authorization object
- follow_redirects: Whether to follow HTTP redirects automatically
- buffered: Whether to buffer the response
- environ_base: Base WSGI environ values
- environ_overrides: WSGI environ overrides
Returns:
TestResponse object
"""
def get(self, *args, **kwargs):
"""Make a GET request. Same parameters as open() except method='GET'."""
def post(self, *args, **kwargs):
"""Make a POST request. Same parameters as open() except method='POST'."""
def put(self, *args, **kwargs):
"""Make a PUT request. Same parameters as open() except method='PUT'."""
def delete(self, *args, **kwargs):
"""Make a DELETE request. Same parameters as open() except method='DELETE'."""
def patch(self, *args, **kwargs):
"""Make a PATCH request. Same parameters as open() except method='PATCH'."""
def options(self, *args, **kwargs):
"""Make an OPTIONS request. Same parameters as open() except method='OPTIONS'."""
def head(self, *args, **kwargs):
"""Make a HEAD request. Same parameters as open() except method='HEAD'."""
def trace(self, *args, **kwargs):
"""Make a TRACE request. Same parameters as open() except method='TRACE'."""
# Cookie management
def get_cookie(self, key, domain="localhost", path="/"):
"""
Get a cookie by key, domain, and path.
Parameters:
- key: Cookie name
- domain: Cookie domain (default: 'localhost')
- path: Cookie path (default: '/')
Returns:
Cookie object or None if not found
"""
def set_cookie(self, key, value="", domain="localhost", origin_only=True, path="/", **kwargs):
"""
Set a cookie for subsequent requests.
Parameters:
- key: Cookie name
- value: Cookie value
- domain: Cookie domain
- origin_only: Whether domain must match exactly
- path: Cookie path
- **kwargs: Additional cookie parameters
"""
def delete_cookie(self, key, domain="localhost", path="/"):
"""
Delete a cookie.
Parameters:
- key: Cookie name
- domain: Cookie domain
- path: Cookie path
"""EnvironBuilder creates WSGI environment dictionaries for testing, providing fine-grained control over request parameters.
class EnvironBuilder:
def __init__(self, path="/", base_url=None, query_string=None, method="GET", input_stream=None, content_type=None, content_length=None, errors_stream=None, multithread=False, multiprocess=True, run_once=False, headers=None, data=None, environ_base=None, environ_overrides=None, mimetype=None, json=None, auth=None):
"""
Build a WSGI environment for testing.
Parameters:
- path: Request path (PATH_INFO in WSGI)
- base_url: Base URL for scheme, host, and script root
- query_string: URL parameters (string or dict)
- method: HTTP method (GET, POST, etc.)
- input_stream: Request body stream
- content_type: Content-Type header
- content_length: Content-Length header
- errors_stream: Error stream for wsgi.errors
- multithread: WSGI multithread flag
- multiprocess: WSGI multiprocess flag
- run_once: WSGI run_once flag
- headers: Request headers (list, dict, or Headers)
- data: Request body data (string, bytes, dict, or file)
- environ_base: Base environ values
- environ_overrides: Environ overrides
- mimetype: MIME type for data
- json: JSON data (sets Content-Type automatically)
- auth: Authorization (username, password) or Authorization object
"""
# Properties for accessing parsed data
form: MultiDict # Parsed form data
files: FileMultiDict # Uploaded files
args: MultiDict # Query string parameters
def get_environ(self):
"""
Build and return the WSGI environment dictionary.
Returns:
Complete WSGI environ dict
"""
def get_request(self, cls=None):
"""
Get a Request object from the environment.
Parameters:
- cls: Request class to use (defaults to werkzeug.wrappers.Request)
Returns:
Request object
"""Enhanced response object with additional testing utilities and properties.
class TestResponse(Response):
def __init__(self, response, status, headers, request, history, auto_to_bytes):
"""
Test-specific response wrapper.
Parameters:
- response: Response iterable
- status: HTTP status
- headers: Response headers
- request: Original request
- history: Redirect history
- auto_to_bytes: Whether to convert response to bytes
"""
# Additional properties for testing
request: Request # The original request
history: list[TestResponse] # Redirect history
text: str # Response body as text (decoded)
@property
def json(self):
"""
Parse response body as JSON.
Returns:
Parsed JSON data
Raises:
ValueError: If response is not valid JSON
"""Represents a cookie with domain, path, and other attributes for testing.
@dataclasses.dataclass
class Cookie:
key: str # Cookie name
value: str # Cookie value
domain: str # Cookie domain
path: str # Cookie path
origin_only: bool # Whether domain must match exactly
def should_send(self, server_name, path):
"""
Check if cookie should be sent with a request.
Parameters:
- server_name: Request server name
- path: Request path
Returns:
True if cookie should be included
"""Helper functions for creating environments and running WSGI applications.
def create_environ(*args, **kwargs):
"""
Create a WSGI environ dict. Shortcut for EnvironBuilder(...).get_environ().
Parameters:
Same as EnvironBuilder constructor
Returns:
WSGI environ dictionary
"""
def run_wsgi_app(app, environ, buffered=False):
"""
Run a WSGI application and capture the response.
Parameters:
- app: WSGI application callable
- environ: WSGI environment dictionary
- buffered: Whether to buffer the response
Returns:
Tuple of (app_iter, status, headers)
"""
def stream_encode_multipart(data, use_tempfile=True, threshold=1024*500, boundary=None):
"""
Encode form data as multipart/form-data stream.
Parameters:
- data: Dict of form fields and files
- use_tempfile: Use temp file for large data
- threshold: Size threshold for temp file
- boundary: Multipart boundary string
Returns:
Tuple of (stream, length, content_type)
"""
def encode_multipart(data, boundary=None, charset="utf-8"):
"""
Encode form data as multipart/form-data bytes.
Parameters:
- data: Dict of form fields and files
- boundary: Multipart boundary string
- charset: Character encoding
Returns:
Tuple of (data_bytes, content_type)
"""Testing-specific exceptions.
class ClientRedirectError(Exception):
"""
Raised when redirect following fails or creates a loop.
"""from werkzeug.test import Client
from werkzeug.wrappers import Request, Response
# Simple WSGI application
def app(environ, start_response):
request = Request(environ)
if request.path == '/':
response = Response('Hello World!')
elif request.path == '/json':
response = Response('{"message": "Hello JSON"}', mimetype='application/json')
else:
response = Response('Not Found', status=404)
return response(environ, start_response)
# Test the application
def test_basic_requests():
client = Client(app)
# Test GET request
response = client.get('/')
assert response.status_code == 200
assert response.text == 'Hello World!'
# Test JSON endpoint
response = client.get('/json')
assert response.status_code == 200
assert response.json == {"message": "Hello JSON"}
# Test 404
response = client.get('/notfound')
assert response.status_code == 404
assert response.text == 'Not Found'from werkzeug.test import Client, EnvironBuilder
from werkzeug.datastructures import FileStorage
from io import BytesIO
def form_app(environ, start_response):
request = Request(environ)
if request.method == 'POST':
name = request.form.get('name', 'Anonymous')
email = request.form.get('email', '')
uploaded_file = request.files.get('avatar')
response_data = {
'name': name,
'email': email,
'file_uploaded': uploaded_file is not None,
'filename': uploaded_file.filename if uploaded_file else None
}
response = Response(str(response_data))
else:
response = Response('Send POST with form data')
return response(environ, start_response)
def test_form_submission():
client = Client(form_app)
# Test form data
response = client.post('/', data={
'name': 'John Doe',
'email': 'john@example.com'
})
assert 'John Doe' in response.text
assert 'john@example.com' in response.text
# Test file upload
response = client.post('/', data={
'name': 'Jane',
'avatar': (BytesIO(b'fake image data'), 'avatar.png')
})
assert 'file_uploaded": True' in response.text
assert 'avatar.png' in response.text
def test_with_environ_builder():
# More control with EnvironBuilder
builder = EnvironBuilder(
path='/upload',
method='POST',
data={
'description': 'Test file',
'file': FileStorage(
stream=BytesIO(b'test content'),
filename='test.txt',
content_type='text/plain'
)
}
)
environ = builder.get_environ()
request = Request(environ)
assert request.method == 'POST'
assert request.form['description'] == 'Test file'
assert request.files['file'].filename == 'test.txt'import json
from werkzeug.test import Client
def api_app(environ, start_response):
request = Request(environ)
if request.path == '/api/data' and request.method == 'POST':
if request.is_json:
data = request.get_json()
response_data = {
'received': data,
'status': 'success'
}
response = Response(
json.dumps(response_data),
mimetype='application/json'
)
else:
response = Response(
'{"error": "Content-Type must be application/json"}',
status=400,
mimetype='application/json'
)
else:
response = Response('{"error": "Not found"}', status=404)
return response(environ, start_response)
def test_json_api():
client = Client(api_app)
# Test JSON request
test_data = {'name': 'Test', 'value': 123}
response = client.post(
'/api/data',
json=test_data # Automatically sets Content-Type
)
assert response.status_code == 200
assert response.json['status'] == 'success'
assert response.json['received'] == test_data
# Test non-JSON request
response = client.post(
'/api/data',
data='not json',
content_type='text/plain'
)
assert response.status_code == 400
assert 'Content-Type must be application/json' in response.json['error']def cookie_app(environ, start_response):
request = Request(environ)
if request.path == '/set-cookie':
response = Response('Cookie set')
response.set_cookie('user_id', '12345', max_age=3600)
response.set_cookie('theme', 'dark', path='/settings')
elif request.path == '/check-cookie':
user_id = request.cookies.get('user_id')
theme = request.cookies.get('theme')
response = Response(f'User ID: {user_id}, Theme: {theme}')
else:
response = Response('Hello')
return response(environ, start_response)
def test_cookies():
client = Client(cookie_app, use_cookies=True)
# Set cookies
response = client.get('/set-cookie')
assert response.status_code == 200
# Cookies should be sent automatically
response = client.get('/check-cookie')
assert 'User ID: 12345' in response.text
# Manual cookie management
client.set_cookie('custom', 'value', domain='localhost')
cookie = client.get_cookie('custom', domain='localhost')
assert cookie.value == 'value'
# Check theme cookie with specific path
client.set_cookie('theme', 'light', path='/settings')
response = client.get('/settings/check-cookie')
# Theme cookie should be sent because path matchesfrom werkzeug.datastructures import Authorization
def auth_app(environ, start_response):
request = Request(environ)
if request.authorization:
if (request.authorization.username == 'admin' and
request.authorization.password == 'secret'):
response = Response(f'Welcome {request.authorization.username}!')
else:
response = Response('Invalid credentials', status=401)
else:
response = Response('Authentication required', status=401)
response.www_authenticate.set_basic('Test Realm')
return response(environ, start_response)
def test_authentication():
client = Client(auth_app)
# Test without auth
response = client.get('/protected')
assert response.status_code == 401
assert 'Authentication required' in response.text
# Test with basic auth (tuple shortcut)
response = client.get('/protected', auth=('admin', 'secret'))
assert response.status_code == 200
assert 'Welcome admin!' in response.text
# Test with Authorization object
auth = Authorization('basic', {'username': 'admin', 'password': 'secret'})
response = client.get('/protected', auth=auth)
assert response.status_code == 200
# Test invalid credentials
response = client.get('/protected', auth=('admin', 'wrong'))
assert response.status_code == 401def redirect_app(environ, start_response):
request = Request(environ)
if request.path == '/redirect':
response = Response('Redirecting...', status=302)
response.location = '/target'
elif request.path == '/target':
response = Response('You made it!')
else:
response = Response('Not found', status=404)
return response(environ, start_response)
def test_redirects():
client = Client(redirect_app)
# Don't follow redirects
response = client.get('/redirect')
assert response.status_code == 302
assert response.location == '/target'
# Follow redirects automatically
response = client.get('/redirect', follow_redirects=True)
assert response.status_code == 200
assert response.text == 'You made it!'
# Check redirect history
assert len(response.history) == 1
assert response.history[0].status_code == 302def header_app(environ, start_response):
request = Request(environ)
user_agent = request.headers.get('User-Agent', 'Unknown')
custom_header = request.headers.get('X-Custom-Header', 'None')
response = Response(f'UA: {user_agent}, Custom: {custom_header}')
response.headers['X-Response-ID'] = '12345'
return response(environ, start_response)
def test_custom_headers():
client = Client(header_app)
response = client.get('/', headers={
'User-Agent': 'TestBot/1.0',
'X-Custom-Header': 'test-value',
'Accept': 'application/json'
})
assert 'UA: TestBot/1.0' in response.text
assert 'Custom: test-value' in response.text
assert response.headers['X-Response-ID'] == '12345'
def test_environ_customization():
# Custom WSGI environ values
response = client.get('/', environ_base={
'REMOTE_ADDR': '192.168.1.100',
'HTTP_HOST': 'testserver.com'
})
# Test with EnvironBuilder for more control
builder = EnvironBuilder(
path='/api/test',
method='PUT',
headers={'Authorization': 'Bearer token123'},
data='{"update": true}',
content_type='application/json'
)
response = client.open(builder)def test_error_handling():
client = Client(app)
# Test malformed requests
try:
# This should handle gracefully
response = client.post('/', data=b'\xff\xfe\invalid')
except Exception as e:
# Check specific error types
pass
# Test large uploads
large_data = b'x' * (1024 * 1024) # 1MB
response = client.post('/', data={'file': (BytesIO(large_data), 'big.txt')})
# Test cookies without cookie support
no_cookie_client = Client(app, use_cookies=False)
try:
no_cookie_client.get_cookie('test') # Should raise TypeError
except TypeError as e:
assert 'Cookies are disabled' in str(e)Install with Tessl CLI
npx tessl i tessl/pypi-werkzeug