Python library that leverages the __eq__ method to make unit tests more declarative and readable through flexible comparison classes.
—
String and byte sequence validation with support for length constraints, case sensitivity, and regular expression matching. These types enable comprehensive validation of text and binary data with flexible pattern matching and formatting constraints.
Base class for string and bytes comparison providing common functionality for length constraints, case handling, and regular expression matching. Supports both string and bytes objects.
class IsAnyStr(DirtyEquals):
"""
Base string/bytes comparison with configurable constraints.
Provides length validation, case handling, and regex matching
for both string and bytes objects.
"""
def __init__(
self,
*,
min_length: Optional[int] = None,
max_length: Optional[int] = None,
case: Optional[str] = None,
regex: Optional[Union[str, Pattern]] = None,
regex_flags: Optional[int] = None
):
"""
Initialize string/bytes validator.
Args:
min_length: Minimum string/bytes length
max_length: Maximum string/bytes length
case: Case constraint ('upper', 'lower', 'title', 'capitalize')
regex: Regular expression pattern to match
regex_flags: Regex flags (e.g., re.IGNORECASE, re.MULTILINE)
"""
expected_types: ClassVar[Tuple[type, ...]] = (str, bytes)
def equals(self, other: Any) -> bool:
"""
Check if value matches string/bytes constraints.
Args:
other: String or bytes value to validate
Returns:
bool: True if value satisfies all constraints
"""from dirty_equals import IsAnyStr
import re
# Basic string or bytes validation
assert "hello" == IsAnyStr
assert b"hello" == IsAnyStr
# Length constraints
assert "hello world" == IsAnyStr(min_length=5, max_length=20)
assert b"data" == IsAnyStr(min_length=1, max_length=10)
# Case constraints
assert "HELLO" == IsAnyStr(case='upper')
assert "hello" == IsAnyStr(case='lower')
assert "Hello World" == IsAnyStr(case='title')
assert "Hello" == IsAnyStr(case='capitalize')
# Regex matching
assert "test@example.com" == IsAnyStr(regex=r'.+@.+\..+')
assert "123-456-7890" == IsAnyStr(regex=r'\d{3}-\d{3}-\d{4}')
# Regex with flags
assert "HELLO world" == IsAnyStr(
regex=r'hello world',
regex_flags=re.IGNORECASE
)
# Combined constraints
assert "Hello World!" == IsAnyStr(
min_length=5,
max_length=20,
regex=r'^[A-Za-z\s!]+$' # Letters, spaces, exclamation only
)
# API response validation
api_data = {
'username': 'john_doe',
'email': 'john@example.com',
'status': 'ACTIVE',
'description': b'binary data here'
}
assert api_data == {
'username': IsAnyStr(min_length=3, max_length=20),
'email': IsAnyStr(regex=r'.+@.+\..+'),
'status': IsAnyStr(case='upper'),
'description': IsAnyStr(min_length=1) # Any non-empty bytes
}String-specific validation that inherits all functionality from IsAnyStr but restricts validation to string objects only, excluding bytes.
class IsStr(IsAnyStr):
"""
String comparison with constraints (excludes bytes).
Inherits all functionality from IsAnyStr but only
accepts str objects, not bytes.
"""
expected_types: ClassVar[Tuple[type, ...]] = (str,)from dirty_equals import IsStr
import re
# Basic string validation
assert "hello world" == IsStr
assert "12345" == IsStr
# Bytes won't match
# assert b"hello" == IsStr # Would fail
# Length validation
assert "username" == IsStr(min_length=3, max_length=20)
assert "short" == IsStr(min_length=1, max_length=10)
# Case validation
assert "HELLO" == IsStr(case='upper')
assert "hello" == IsStr(case='lower')
assert "Hello World" == IsStr(case='title')
assert "Hello" == IsStr(case='capitalize')
# Pattern matching
assert "john@example.com" == IsStr(regex=r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$')
assert "+1-555-123-4567" == IsStr(regex=r'^\+\d{1,3}-\d{3}-\d{3}-\d{4}$')
# URL validation
assert "https://www.example.com/path" == IsStr(regex=r'^https?://.+')
# Username validation
assert "user_name123" == IsStr(
min_length=3,
max_length=20,
regex=r'^[a-zA-Z0-9_]+$' # Alphanumeric and underscore only
)
# Password strength validation
assert "MyPassword123!" == IsStr(
min_length=8,
regex=r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*]).*$'
)
# Form data validation
form_input = {
'first_name': 'John',
'last_name': 'Doe',
'email': 'john.doe@company.com',
'phone': '555-0123',
'zip_code': '12345',
'country': 'US'
}
assert form_input == {
'first_name': IsStr(min_length=1, max_length=50, case='capitalize'),
'last_name': IsStr(min_length=1, max_length=50, case='capitalize'),
'email': IsStr(regex=r'^[^@]+@[^@]+\.[^@]+$'),
'phone': IsStr(regex=r'^\d{3}-\d{4}$'),
'zip_code': IsStr(regex=r'^\d{5}$'),
'country': IsStr(case='upper', min_length=2, max_length=2)
}
# Configuration validation
config = {
'app_name': 'MyApplication',
'environment': 'production',
'log_level': 'INFO',
'secret_key': 'abc123def456ghi789'
}
assert config == {
'app_name': IsStr(min_length=1),
'environment': IsStr(regex=r'^(development|staging|production)$'),
'log_level': IsStr(regex=r'^(DEBUG|INFO|WARNING|ERROR|CRITICAL)$'),
'secret_key': IsStr(min_length=16)
}
# File path validation
file_paths = {
'config_file': '/etc/myapp/config.json',
'log_file': '/var/log/myapp.log',
'temp_dir': '/tmp/myapp/'
}
assert file_paths == {
'config_file': IsStr(regex=r'^/.+\.json$'),
'log_file': IsStr(regex=r'^/.+\.log$'),
'temp_dir': IsStr(regex=r'^/.+/$') # Ends with slash
}
# Social media validation
social_handles = {
'twitter': '@johndoe',
'instagram': 'john.doe.photography',
'linkedin': 'john-doe-engineer'
}
assert social_handles == {
'twitter': IsStr(regex=r'^@[a-zA-Z0-9_]{1,15}$'),
'instagram': IsStr(regex=r'^[a-zA-Z0-9_.]{1,30}$'),
'linkedin': IsStr(regex=r'^[a-zA-Z0-9-]{3,100}$')
}
# API token validation
tokens = {
'api_key': 'sk-1234567890abcdef1234567890abcdef',
'session_token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
'refresh_token': 'rt_1234567890abcdef'
}
assert tokens == {
'api_key': IsStr(regex=r'^sk-[a-f0-9]{32}$'),
'session_token': IsStr(min_length=100), # JWT tokens are long
'refresh_token': IsStr(regex=r'^rt_[a-f0-9]{16}$')
}Bytes-specific validation that inherits all functionality from IsAnyStr but restricts validation to bytes objects only, excluding strings.
class IsBytes(IsAnyStr):
"""
Bytes comparison with constraints (excludes strings).
Inherits all functionality from IsAnyStr but only
accepts bytes objects, not strings.
"""
expected_types: ClassVar[Tuple[type, ...]] = (bytes,)from dirty_equals import IsBytes
import re
# Basic bytes validation
assert b"hello world" == IsBytes
assert b"\x00\x01\x02\x03" == IsBytes
# Strings won't match
# assert "hello" == IsBytes # Would fail
# Length validation
assert b"binary_data" == IsBytes(min_length=5, max_length=50)
assert b"short" == IsBytes(min_length=1, max_length=10)
# Pattern matching on bytes
assert b"Content-Type: application/json" == IsBytes(regex=rb'Content-Type: \w+/\w+')
assert b"HTTP/1.1 200 OK" == IsBytes(regex=rb'HTTP/\d\.\d \d{3} \w+')
# File signature validation (magic numbers)
png_header = b'\x89PNG\r\n\x1a\n'
jpeg_header = b'\xff\xd8\xff'
pdf_header = b'%PDF-'
assert png_header == IsBytes(regex=rb'^\x89PNG\r\n\x1a\n')
assert jpeg_header == IsBytes(regex=rb'^\xff\xd8\xff')
assert pdf_header == IsBytes(regex=rb'^%PDF-')
# Network packet validation
http_request = b'GET /api/users HTTP/1.1\r\nHost: example.com\r\n\r\n'
assert http_request == IsBytes(
regex=rb'^GET .+ HTTP/\d\.\d\r\n.*Host: .+\r\n\r\n$'
)
# Binary protocol validation
protocol_message = b'\x02\x00\x10Hello World!\x03' # STX + length + data + ETX
assert protocol_message == IsBytes(
min_length=4, # At least header + footer
regex=rb'^\x02.+\x03$' # Starts with STX, ends with ETX
)
# Cryptographic data validation
encrypted_data = {
'iv': b'\x12\x34\x56\x78' * 4, # 16 bytes IV
'ciphertext': b'encrypted_payload_here_with_padding',
'tag': b'\xaa\xbb\xcc\xdd' * 4 # 16 bytes auth tag
}
assert encrypted_data == {
'iv': IsBytes(min_length=16, max_length=16), # Exactly 16 bytes
'ciphertext': IsBytes(min_length=1), # Non-empty
'tag': IsBytes(min_length=16, max_length=16) # Exactly 16 bytes
}
# File content validation
file_data = {
'image': b'\x89PNG\r\n\x1a\n' + b'image_data_here',
'document': b'%PDF-1.4\n' + b'pdf_content_here',
'archive': b'PK\x03\x04' + b'zip_content_here'
}
assert file_data == {
'image': IsBytes(regex=rb'^\x89PNG'), # PNG file
'document': IsBytes(regex=rb'^%PDF-'), # PDF file
'archive': IsBytes(regex=rb'^PK\x03\x04') # ZIP file
}
# Database blob validation
blob_data = {
'profile_picture': b'binary_image_data_here',
'document_content': b'binary_document_data',
'metadata': b'{"created": "2023-01-01"}'
}
assert blob_data == {
'profile_picture': IsBytes(min_length=1, max_length=5*1024*1024), # Max 5MB
'document_content': IsBytes(min_length=1),
'metadata': IsBytes(regex=rb'^\{.*\}$') # JSON-like structure
}
# Message queue payloads
queue_messages = [
b'{"type": "user_created", "data": {...}}',
b'{"type": "order_processed", "data": {...}}',
b'{"type": "email_sent", "data": {...}}'
]
message_pattern = IsBytes(regex=rb'^\{"type": "\w+", "data": .*\}$')
for message in queue_messages:
assert message == message_pattern
# Network response validation
api_response_bytes = b'{"status": "success", "data": [...]}'
assert api_response_bytes == IsBytes(
min_length=10, # Minimum JSON structure
regex=rb'^\{.*"status".*\}$' # Contains status field
)
# Base64 encoded data (still bytes before decoding)
base64_data = b'SGVsbG8gV29ybGQh' # "Hello World!" encoded
assert base64_data == IsBytes(regex=rb'^[A-Za-z0-9+/]+=*$')
# Binary log entries
log_entry = b'\x00\x00\x00\x20' + b'2023-01-01 12:00:00 INFO: App started'
assert log_entry == IsBytes(
min_length=8, # Header + some content
regex=rb'^\x00\x00\x00.+INFO:' # Starts with header, contains INFO
)from typing import ClassVar, Optional, Pattern, Tuple, Union
import re
# All string types inherit from IsAnyStr which inherits from DirtyEquals
# They work with Python's standard str and bytes types
# Common regex flags that can be used with regex_flags parameter:
# re.IGNORECASE, re.MULTILINE, re.DOTALL, re.VERBOSE, etc.Install with Tessl CLI
npx tessl i tessl/pypi-dirty-equals