CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-syrupy

Pytest snapshot testing utility that enables developers to write tests asserting immutability of computed results.

Overall
score

80%

Overview
Eval results
Files

matchers.mddocs/

Matchers

Value transformation system for handling dynamic data in snapshots. Matchers allow replacing or transforming specific values during snapshot comparison, essential for testing data with timestamps, IDs, or other non-deterministic values.

Capabilities

Path-Type Matching

Transform values based on their path location and data type, replacing dynamic values with stable placeholders.

def path_type(mapping: Optional[Dict[str, Tuple[PropertyValueType, ...]]] = None, *, types: Tuple[PropertyValueType, ...] = (), strict: bool = True, regex: bool = False, replacer: Replacer = ...) -> PropertyMatcher:
    """
    Create matcher that replaces values based on path and type.
    
    Parameters:
    - mapping: Dictionary mapping path patterns to (type, replacement) tuples
    - strict: Whether to raise error on type mismatches
    
    Returns:
    PropertyMatcher: Function that transforms matching values
    
    Raises:
    PathTypeError: When path exists but type doesn't match (strict=True)
    """

Usage examples:

def test_dynamic_timestamp(snapshot):
    from syrupy.matchers import path_type
    
    result = {
        "user_id": 12345,  # Dynamic ID
        "timestamp": "2023-12-01T10:30:45Z",  # Dynamic timestamp
        "message": "Hello world",  # Static content
        "metadata": {
            "request_id": "req_abc123",  # Dynamic nested ID
            "version": "1.0"  # Static nested content
        }
    }
    
    # Replace dynamic values with stable placeholders
    assert result == snapshot(matcher=path_type({
        "user_id": (int, "<user_id>"),
        "timestamp": (str, "<timestamp>"),
        "metadata.request_id": (str, "<request_id>")
    }))

def test_list_with_dynamic_ids(snapshot):
    from syrupy.matchers import path_type
    
    users = [
        {"id": 101, "name": "Alice", "created": "2023-12-01"},
        {"id": 102, "name": "Bob", "created": "2023-12-02"}
    ]
    
    # Replace all ID and created fields regardless of list position
    assert users == snapshot(matcher=path_type({
        "*.id": (int, "<id>"),
        "*.created": (str, "<created>")
    }))

def test_strict_type_checking(snapshot):
    from syrupy.matchers import path_type, PathTypeError
    
    data = {"age": "25"}  # String instead of expected int
    
    try:
        # This will raise PathTypeError because age is str, not int
        assert data == snapshot(matcher=path_type({
            "age": (int, "<age>")
        }, strict=True))
    except PathTypeError:
        # Handle type mismatch
        pass
    
    # With strict=False, mismatched types are ignored
    assert data == snapshot(matcher=path_type({
        "age": (int, "<age>")
    }, strict=False))

Path-Value Matching

Transform values based on regex patterns matched against their string representation.

def path_value(mapping: Dict[str, Any]) -> PropertyMatcher:
    """
    Create matcher that replaces values based on path regex patterns.
    
    Parameters:
    - mapping: Dictionary mapping path regex patterns to replacement values
    
    Returns:
    PropertyMatcher: Function that transforms matching string representations
    """

Usage examples:

def test_regex_patterns(snapshot):
    from syrupy.matchers import path_value
    
    data = {
        "email": "user@example.com",
        "phone": "+1-555-123-4567", 
        "website": "https://example.com/path",
        "static_field": "keep this"
    }
    
    # Use regex patterns to match and replace values
    assert data == snapshot(matcher=path_value({
        r"email": "<email>",  # Exact path match
        r"phone": "<phone>",  # Replace phone number
        r".*url.*": "<url>"   # Pattern matching paths containing 'url'
    }))

def test_complex_patterns(snapshot):
    from syrupy.matchers import path_value
    
    response = {
        "auth_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
        "session_id": "sess_1234567890abcdef",
        "api_key": "ak_live_1234567890abcdef1234567890abcdef",
        "user_data": {"name": "Alice"}
    }
    
    # Replace tokens and IDs with patterns
    assert response == snapshot(matcher=path_value({
        r".*token.*": "<token>",      # Any path containing 'token'
        r".*session.*": "<session>",  # Any path containing 'session'  
        r".*key.*": "<api_key>"       # Any path containing 'key'
    }))

Matcher Composition

Combine multiple matchers for complex transformation scenarios.

def compose_matchers(*matchers: PropertyMatcher) -> PropertyMatcher:
    """
    Compose multiple matchers into a single matcher function.
    
    Parameters:
    - *matchers: Variable number of matcher functions to compose
    
    Returns:
    PropertyMatcher: Combined matcher applying all transformations in sequence
    """

Usage examples:

def test_composed_matchers(snapshot):
    from syrupy.matchers import path_type, path_value, compose_matchers
    
    data = {
        "user_id": 12345,  # Handle by type
        "email": "user@domain.com",  # Handle by regex pattern
        "timestamp": "2023-12-01T10:30:45.123Z",  # Handle by type
        "session_token": "tok_abc123def456",  # Handle by regex pattern
        "name": "Alice"  # Leave unchanged
    }
    
    # Combine type-based and regex-based matching
    combined_matcher = compose_matchers(
        path_type({
            "user_id": (int, "<user_id>"),
            "timestamp": (str, "<timestamp>")
        }),
        path_value({
            r".*email.*": "<email>",
            r".*token.*": "<token>"
        })
    )
    
    assert data == snapshot(matcher=combined_matcher)

Custom Matcher Development

Create custom matcher functions for specialized transformation needs.

PropertyMatcher = Callable[[SerializableData, PropertyPath], SerializableData]

# PropertyPath structure
PropertyPathEntry = Tuple[PropertyName, PropertyValueType]
PropertyPath = Tuple[PropertyPathEntry, ...]

Usage examples:

def test_custom_matcher(snapshot):
    def uuid_matcher(data, path):
        """Custom matcher for UUID fields"""
        import re
        
        # Get the property name from the path
        if path and len(path) > 0:
            prop_name = path[-1][0]  # Last entry's property name
            
            # Check if it looks like a UUID field and value
            if "id" in str(prop_name).lower() and isinstance(data, str):
                uuid_pattern = r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
                if re.match(uuid_pattern, data, re.IGNORECASE):
                    return "<uuid>"
        
        return data  # Return unchanged if not a UUID
    
    result = {
        "user_id": "550e8400-e29b-41d4-a716-446655440000",
        "name": "Alice",
        "session_id": "123e4567-e89b-12d3-a456-426614174000"
    }
    
    assert result == snapshot(matcher=uuid_matcher)

def test_date_normalizer(snapshot):
    from datetime import datetime
    
    def date_normalizer(data, path):
        """Normalize various date formats to ISO format"""
        if isinstance(data, str):
            try:
                # Try parsing common date formats
                for fmt in ["%Y-%m-%d", "%m/%d/%Y", "%d-%m-%Y"]:
                    try:
                        parsed = datetime.strptime(data, fmt)
                        return parsed.strftime("%Y-%m-%d")
                    except ValueError:
                        continue
            except:
                pass
        return data
    
    data = {
        "start_date": "12/25/2023",  # MM/DD/YYYY format
        "end_date": "25-12-2023",    # DD-MM-YYYY format
        "created": "2023-12-25"      # Already ISO format
    }
    
    # All dates will be normalized to YYYY-MM-DD format
    assert data == snapshot(matcher=date_normalizer)

Exception Handling

Matchers provide specific exceptions for error handling:

class PathTypeError(Exception):
    """Raised when path type matching fails"""

class StrictPathTypeError(PathTypeError):
    """Raised when strict path type matching fails"""

Usage examples:

def test_matcher_error_handling(snapshot):
    from syrupy.matchers import path_type, PathTypeError
    
    data = {"count": "not_a_number"}
    
    try:
        # This will raise PathTypeError in strict mode
        assert data == snapshot(matcher=path_type({
            "count": (int, "<count>")
        }, strict=True))
    except PathTypeError as e:
        # Handle the type mismatch error
        print(f"Type mismatch: {e}")
        
        # Use non-strict mode as fallback
        assert data == snapshot(matcher=path_type({
            "count": (int, "<count>")
        }, strict=False))

Install with Tessl CLI

npx tessl i tessl/pypi-syrupy

docs

cli-integration.md

core-assertions.md

extensions.md

filters.md

index.md

matchers.md

tile.json