Python library that leverages the __eq__ method to make unit tests more declarative and readable through flexible comparison classes.
—
List, tuple, and general sequence validation with support for partial matching, order constraints, containment checking, and length validation. These types enable flexible validation of sequential data structures with various matching strategies.
Length validation for any object that supports the len() function, with support for exact length, minimum length, or length ranges.
class HasLen(DirtyEquals):
"""
Length checking for any object supporting len().
Validates that an object has a specific length, minimum length,
or length within a specified range.
"""
def __init__(self, min_length: int, max_length: Optional[int] = None):
"""
Initialize length checker (overloaded constructor).
Can be called as:
- HasLen(exact_length) - exact length match
- HasLen(min_length, max_length) - length range
Args:
min_length: Minimum length (or exact if max_length is None)
max_length: Maximum length (None for exact match)
"""
def equals(self, other: Any) -> bool:
"""
Check if object length matches constraints.
Args:
other: Object to check length of (must support len())
Returns:
bool: True if length satisfies constraints
"""from dirty_equals import HasLen
# Exact length checking
assert [1, 2, 3] == HasLen(3)
assert "hello" == HasLen(5)
assert {"a": 1, "b": 2} == HasLen(2)
# Length range checking
assert [1, 2, 3, 4] == HasLen(2, 5) # Between 2 and 5 items
assert "hello world" == HasLen(5, 15) # Between 5 and 15 characters
# Minimum length (no maximum)
assert [1, 2, 3, 4, 5] == HasLen(3, None) # At least 3 items
# API response validation
api_results = {
'users': [{'id': 1}, {'id': 2}, {'id': 3}],
'message': 'Success',
'errors': []
}
assert api_results == {
'users': HasLen(1, 10), # 1-10 users expected
'message': HasLen(1, 100), # Non-empty message
'errors': HasLen(0) # No errors
}
# Form validation
form_data = {
'username': 'john_doe',
'password': 'secret123',
'tags': ['python', 'web', 'api']
}
assert form_data == {
'username': HasLen(3, 20), # Username length constraints
'password': HasLen(8), # Exact password length
'tags': HasLen(1, 5) # 1-5 tags allowed
}
# Database query results
query_results = [
{'id': 1, 'name': 'Alice'},
{'id': 2, 'name': 'Bob'}
]
assert query_results == HasLen(2) # Expecting exactly 2 results
# File content validation
file_lines = ['line 1', 'line 2', 'line 3', 'line 4']
assert file_lines == HasLen(1, 100) # File should have content but not be hugeContainment checking that validates whether one or more values are present in a collection using Python's in operator.
class Contains(DirtyEquals):
"""
Containment checking using 'in' operator.
Validates that all specified values are contained
within the target collection.
"""
def __init__(self, contained_value: Any, *more_contained_values: Any):
"""
Initialize containment checker.
Args:
contained_value: First value that must be contained
*more_contained_values: Additional values that must be contained
"""
def equals(self, other: Any) -> bool:
"""
Check if all specified values are contained in other.
Args:
other: Collection to check containment in
Returns:
bool: True if all values are contained in other
"""from dirty_equals import Contains
# Basic containment checking
assert [1, 2, 3, 4, 5] == Contains(3)
assert "hello world" == Contains("world")
assert {"a": 1, "b": 2, "c": 3} == Contains("b")
# Multiple values must all be contained
assert [1, 2, 3, 4, 5] == Contains(2, 4, 5)
assert "hello world" == Contains("hello", "world")
assert {"a": 1, "b": 2, "c": 3} == Contains("a", "c")
# String containment
text = "The quick brown fox jumps over the lazy dog"
assert text == Contains("quick", "fox", "lazy")
assert text == Contains("brown")
# List containment
shopping_list = ['bread', 'milk', 'eggs', 'cheese', 'butter']
assert shopping_list == Contains('milk', 'eggs')
assert shopping_list == Contains('bread')
# Dict key containment
config = {
'database_url': 'postgresql://localhost/db',
'secret_key': 'super-secret',
'debug': True,
'port': 5000
}
assert config == Contains('database_url', 'secret_key')
assert config == Contains('debug')
# API response validation
api_response = {
'data': [
{'id': 1, 'name': 'Alice', 'roles': ['admin', 'user']},
{'id': 2, 'name': 'Bob', 'roles': ['user', 'moderator']}
]
}
# Check that admin role exists somewhere
user_roles = api_response['data'][0]['roles']
assert user_roles == Contains('admin')
# Check for required permissions
required_permissions = ['read', 'write', 'delete', 'admin']
user_permissions = ['read', 'write', 'admin', 'moderate', 'create']
assert user_permissions == Contains('read', 'write', 'admin')
# Tag validation
blog_post = {
'title': 'Python Tips',
'content': '...',
'tags': ['python', 'programming', 'tutorial', 'beginner']
}
# Must contain essential tags
assert blog_post['tags'] == Contains('python', 'programming')
# Search results validation
search_results = [
'python-guide.pdf',
'advanced-python-tricks.txt',
'python-best-practices.md',
'django-tutorial.html'
]
# Must contain files with 'python' in name
filenames_with_python = [f for f in search_results if 'python' in f]
assert filenames_with_python == Contains('python-guide.pdf', 'advanced-python-tricks.txt')
# Set operations
available_features = {'auth', 'logging', 'caching', 'monitoring', 'backup'}
required_features = {'auth', 'logging'}
for feature in required_features:
assert available_features == Contains(feature)Base class for list and tuple validation with flexible matching strategies including item validation, position constraints, order checking, and length validation.
class IsListOrTuple(DirtyEquals):
"""
List/tuple comparison with flexible matching constraints.
Supports item validation, position-specific checks, order enforcement,
and length constraints for both lists and tuples.
"""
def __init__(
self,
*items: Any,
positions: Optional[Dict[int, Any]] = None,
check_order: bool = True,
length: Optional[int] = None
):
"""
Initialize list/tuple validator.
Args:
*items: Expected items (order matters if check_order=True)
positions: Dict mapping positions to expected values
check_order: Whether to enforce item order
length: Expected exact length
"""
allowed_type: ClassVar[Tuple[type, ...]] = (list, tuple)
def equals(self, other: Any) -> bool:
"""
Check if sequence matches constraints.
Args:
other: List or tuple to validate
Returns:
bool: True if sequence satisfies all constraints
"""from dirty_equals import IsListOrTuple, IsPositive, IsStr
# Basic sequence matching - exact order
assert [1, 2, 3] == IsListOrTuple(1, 2, 3)
assert (1, 2, 3) == IsListOrTuple(1, 2, 3)
# With validators
assert ['hello', 42, True] == IsListOrTuple(IsStr, IsPositive, bool)
# Position-specific validation
data = [10, 'name', 3.14, True, 'end']
assert data == IsListOrTuple(
positions={
0: IsPositive, # First item must be positive
1: IsStr, # Second item must be string
2: float, # Third item must be float
4: 'end' # Last item must be 'end'
}
)
# Ignore order - just check that items exist somewhere
unordered_list = [3, 1, 2]
assert unordered_list == IsListOrTuple(1, 2, 3, check_order=False)
# Length validation
assert [1, 2, 3, 4, 5] == IsListOrTuple(length=5)
assert ('a', 'b') == IsListOrTuple(length=2)
# Combined constraints
api_response = ['success', 200, {'data': 'result'}, True]
assert api_response == IsListOrTuple(
'success', # First item exact match
IsPositive, # Second item positive number
dict, # Third item is dict
bool, # Fourth item is boolean
length=4 # Exactly 4 items
)
# Flexible API result validation
results = [
{'id': 1, 'name': 'Alice'},
{'id': 2, 'name': 'Bob'},
{'id': 3, 'name': 'Charlie'}
]
# Check specific positions and overall structure
assert results == IsListOrTuple(
positions={
0: {'id': 1, 'name': IsStr}, # First user
2: {'id': IsPositive, 'name': 'Charlie'} # Last user
},
length=3
)
# Configuration tuple validation
config_tuple = ('production', 8080, True, '/var/log/app.log')
assert config_tuple == IsListOrTuple(
IsStr, # Environment name
IsPositive, # Port number
bool, # Debug flag
IsStr, # Log file path
check_order=True,
length=4
)List-specific validation that inherits all functionality from IsListOrTuple but restricts validation to list objects only.
class IsList(IsListOrTuple):
"""
List-specific comparison with constraints.
Inherits all functionality from IsListOrTuple but only
accepts list objects, not tuples.
"""
allowed_type: ClassVar[type] = listfrom dirty_equals import IsList, IsPositive, IsStr
# Basic list validation
assert [1, 2, 3] == IsList(1, 2, 3)
assert ['a', 'b', 'c'] == IsList('a', 'b', 'c')
# Tuples won't match
# assert (1, 2, 3) == IsList(1, 2, 3) # Would fail
# API response list validation
user_list = [
{'id': 1, 'name': 'Alice', 'active': True},
{'id': 2, 'name': 'Bob', 'active': False}
]
assert user_list == IsList(
{'id': IsPositive, 'name': IsStr, 'active': bool},
{'id': IsPositive, 'name': IsStr, 'active': bool}
)
# Dynamic list validation
numbers = [1, 2, 3, 4, 5]
assert numbers == IsList(*range(1, 6)) # Unpack range
# Shopping cart validation
cart_items = [
{'product_id': 101, 'quantity': 2, 'price': 29.99},
{'product_id': 202, 'quantity': 1, 'price': 15.50},
{'product_id': 303, 'quantity': 3, 'price': 8.75}
]
cart_item_schema = {
'product_id': IsPositive,
'quantity': IsPositive,
'price': IsPositive
}
assert cart_items == IsList(
cart_item_schema,
cart_item_schema,
cart_item_schema,
length=3
)
# Log entries validation
log_entries = [
'INFO: Application started',
'DEBUG: Loading configuration',
'ERROR: Database connection failed'
]
assert log_entries == IsList(
positions={
0: IsStr, # Any string for first entry
2: Contains('ERROR') # Last entry must contain ERROR
},
length=3
)
# Nested list validation
matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
row_pattern = IsList(IsPositive, IsPositive, IsPositive)
assert matrix == IsList(row_pattern, row_pattern, row_pattern)
# File processing results
processed_files = [
'document1.txt',
'image2.jpg',
'data3.csv',
'config4.json'
]
assert processed_files == IsList(
Contains('.txt'),
Contains('.jpg'),
Contains('.csv'),
Contains('.json'),
check_order=False # Files might be processed in any order
)Tuple-specific validation that inherits all functionality from IsListOrTuple but restricts validation to tuple objects only.
class IsTuple(IsListOrTuple):
"""
Tuple-specific comparison with constraints.
Inherits all functionality from IsListOrTuple but only
accepts tuple objects, not lists.
"""
allowed_type: ClassVar[type] = tuplefrom dirty_equals import IsTuple, IsPositive, IsStr
# Basic tuple validation
assert (1, 2, 3) == IsTuple(1, 2, 3)
assert ('a', 'b', 'c') == IsTuple('a', 'b', 'c')
# Lists won't match
# assert [1, 2, 3] == IsTuple(1, 2, 3) # Would fail
# Named tuple-like validation
person_tuple = ('John Doe', 25, 'Engineer', True)
assert person_tuple == IsTuple(
IsStr, # Name
IsPositive, # Age
IsStr, # Job title
bool # Active status
)
# Coordinate validation
point_2d = (3.14, 2.71)
point_3d = (1.0, 2.0, 3.0)
assert point_2d == IsTuple(float, float)
assert point_3d == IsTuple(float, float, float)
# Database record as tuple
db_record = (123, 'Alice Johnson', 'alice@example.com', '2023-01-15')
assert db_record == IsTuple(
IsPositive, # ID
IsStr, # Name
Contains('@'), # Email
IsStr, # Date string
length=4
)
# Function return value validation
def get_user_info(user_id):
return (user_id, 'John Doe', 'john@example.com', True)
result = get_user_info(123)
assert result == IsTuple(
123, # Expected user ID
IsStr, # Name
IsStr, # Email
True # Active status
)
# Configuration tuple with position validation
config = ('production', 443, True, 30, '/var/log/')
assert config == IsTuple(
positions={
0: 'production', # Environment
1: IsPositive, # Port
2: True, # SSL enabled
3: IsPositive, # Timeout
4: Contains('/') # Log directory
},
length=5
)
# API response tuple format
api_result = ('success', 200, {'user_id': 123}, None)
assert api_result == IsTuple(
'success', # Status
200, # HTTP code
dict, # Data payload
None, # Error (should be None for success)
check_order=True,
length=4
)
# Mathematical vector validation
vector = (1.0, 0.0, -1.0, 2.5)
assert vector == IsTuple(
float, float, float, float, # All components must be floats
length=4
)
# RGB color tuple
rgb_color = (255, 128, 0)
assert rgb_color == IsTuple(
positions={
0: IsRange(0, 255), # Red component
1: IsRange(0, 255), # Green component
2: IsRange(0, 255) # Blue component
},
length=3
)from typing import Any, ClassVar, Dict, Optional, Tuple, Union
# All sequence types inherit from DirtyEquals
# IsListOrTuple is the base class for IsList and IsTuple
# They work with Python's standard list and tuple typesInstall with Tessl CLI
npx tessl i tessl/pypi-dirty-equals