CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-werkzeug

The comprehensive WSGI web application library providing essential utilities and components for building Python web applications.

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

testing.mddocs/

Testing Utilities

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.

Capabilities

Test Client

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
        """

Environment Builder

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
        """

Test Response

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
        """

Cookie

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
        """

Utility Functions

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)
    """

Exceptions

Testing-specific exceptions.

class ClientRedirectError(Exception):
    """
    Raised when redirect following fails or creates a loop.
    """

Usage Examples

Basic Application Testing

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'

Form Data and File Uploads

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'

JSON API Testing

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']

Cookie Testing

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 matches

Authentication Testing

from 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 == 401

Redirect Following

def 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 == 302

Custom Headers and Advanced Options

def 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)

Error Handling and Edge Cases

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

docs

data-structures.md

dev-server.md

exceptions.md

http-utilities.md

index.md

middleware.md

request-response.md

routing.md

security.md

testing.md

url-wsgi-utils.md

tile.json