Lightweight, zero-dependency mock assertion helpers with flexible numeric and string matching
Project description
flexible-matchers
Lightweight, zero-dependency mock assertion helpers with flexible numeric and string matching.
flexible-matchers provides intuitive matcher objects for use with Python's unittest.mock and general test assertions. Unlike other matcher libraries, it uses Python's native equality operators, making it work seamlessly with standard assertions and mock calls.
Key Features
- Zero Dependencies - No external packages required
- Numeric Matchers - Range-based and tolerance-based number matching
- String Matchers - Flexible length constraints for string validation
- Collection Matchers - List validation with length constraints
- None Handling - Special matcher for non-None values
- Pythonic API - Uses standard
==operator, works with any assertion library - Lightweight - Simple, focused implementation
- Well-Tested - Comprehensive test suite with 100% coverage
- Type-Hinted - Full type annotations for better IDE support
Installation
pip install flexible-matchers
Quick Start
from flexible_matchers import NUMBER, STRING, IS_NUMBER, ANY_NOT_NONE
# In mock assertions
mock_api.assert_called_with(
user_id=NUMBER(min_value=1),
name=STRING(min_length=1),
age=NUMBER(min_value=0, max_value=150),
)
# In data structure comparisons
response = {"id": 123, "name": "Alice", "created_at": "2024-01-01"}
assert response == {
"id": IS_NUMBER,
"name": STRING(min_length=1),
"created_at": ANY_NOT_NONE,
}
Documentation
NUMBER
Matches numeric values (int or float) with optional min/max constraints.
from flexible_matchers import NUMBER, IS_NUMBER
# Match any number
assert 42 == NUMBER()
assert 3.14 == NUMBER()
# Match with minimum value
assert 42 == NUMBER(min_value=0)
assert -5 != NUMBER(min_value=0)
# Match with maximum value
assert 42 == NUMBER(max_value=100)
assert 150 != NUMBER(max_value=100)
# Match within range
assert 50 == NUMBER(min_value=0, max_value=100)
assert -1 != NUMBER(min_value=0, max_value=100)
# Pre-instantiated matcher for any number
assert 42 == IS_NUMBER
CLOSE_NUMBER
Matches numbers within a tolerance range (useful for floating-point comparisons).
from flexible_matchers import CLOSE_NUMBER
# Default tolerance of 0.5
assert 42.3 == CLOSE_NUMBER(42)
assert 41.7 == CLOSE_NUMBER(42)
assert 42.6 != CLOSE_NUMBER(42)
# Custom tolerance
assert 3.14 == CLOSE_NUMBER(3.1, tolerance=0.1)
assert 100 == CLOSE_NUMBER(99, tolerance=1)
Unique Feature: Unlike other libraries, CLOSE_NUMBER provides tolerance-based matching which is essential for floating-point comparisons in scientific computing and financial applications.
STRING
Matches strings with optional length constraints.
from flexible_matchers import STRING, IS_STRING
# Match any string
assert "hello" == STRING()
assert "" == STRING()
# Match exact length
assert "hello" == STRING(length=5)
assert "hi" != STRING(length=5)
# Match minimum length
assert "hello" == STRING(min_length=3)
assert "hi" != STRING(min_length=3)
# Match maximum length
assert "hello" == STRING(max_length=10)
assert "very long string" != STRING(max_length=10)
# Match length range
assert "hello" == STRING(min_length=3, max_length=10)
# Pre-instantiated matcher for any string
assert "hello" == IS_STRING
Unique Feature: Flexible string length constraints (min_length, max_length) are not available in most other matcher libraries.
LIST
Matches lists with optional length constraint.
from flexible_matchers import LIST, IS_LIST
# Match any list
assert [1, 2, 3] == LIST()
assert [] == LIST()
# Match exact length
assert [1, 2, 3] == LIST(3)
assert [1, 2] != LIST(3)
# Pre-instantiated matcher for any list
assert [1, 2, 3] == IS_LIST
ANY_NOT_NONE
Matches any value except None.
from flexible_matchers import ANY_NOT_NONE
# Matches any non-None value
assert 42 == ANY_NOT_NONE
assert "hello" == ANY_NOT_NONE
assert [] == ANY_NOT_NONE
assert 0 == ANY_NOT_NONE
assert False == ANY_NOT_NONE
# Does not match None
assert None != ANY_NOT_NONE
Use Case: Perfect for API responses where you want to ensure a field exists but don't care about its specific value.
Comparison with Other Libraries
vs. unittest.mock.ANY
from unittest.mock import ANY
from flexible_matchers import NUMBER, STRING
# unittest.mock.ANY - too permissive
assert {"age": -100} == {"age": ANY} # Passes, but age is invalid!
# flexible-matchers - precise validation
assert {"age": 30} == {"age": NUMBER(min_value=0, max_value=150)} # [x]
assert {"age": -100} == {"age": NUMBER(min_value=0, max_value=150)} # [ ]
vs. PyHamcrest
# PyHamcrest - requires special syntax
from hamcrest import assert_that, instance_of, greater_than
assert_that(value, instance_of(int))
assert_that(value, greater_than(0))
# flexible-matchers - natural Python syntax
from flexible_matchers import NUMBER
assert value == NUMBER(min_value=0)
vs. dirty-equals
# dirty-equals - close, but missing key features
from dirty_equals import IsPositiveInt
assert 42 == IsPositiveInt
# flexible-matchers - more flexible with ranges and tolerance
from flexible_matchers import NUMBER, CLOSE_NUMBER
assert 42 == NUMBER(min_value=0, max_value=100)
assert 3.14 == CLOSE_NUMBER(3.1, tolerance=0.1) # Not available in dirty-equals
vs. pychoir
# pychoir - similar approach, but less intuitive
from pychoir import LessThan, GreaterThan, And
assert value == And(GreaterThan(0), LessThan(100))
# flexible-matchers - simpler, more intuitive
from flexible_matchers import NUMBER
assert value == NUMBER(min_value=0, max_value=100)
Real-World Examples
API Testing
from flexible_matchers import NUMBER, STRING, ANY_NOT_NONE
def test_create_user_api():
response = api.create_user(name="Alice", email="alice@example.com")
assert response == {
"id": NUMBER(min_value=1),
"name": STRING(min_length=1, max_length=100),
"email": STRING(min_length=5),
"created_at": ANY_NOT_NONE,
"updated_at": ANY_NOT_NONE,
"is_active": True,
}
Mock Assertions
from unittest.mock import Mock
from flexible_matchers import NUMBER, STRING
def test_user_service():
mock_db = Mock()
service = UserService(mock_db)
service.create_user(name="Alice", age=30)
mock_db.insert.assert_called_once_with(
table="users",
data={
"name": STRING(min_length=1),
"age": NUMBER(min_value=0, max_value=150),
"created_at": ANY_NOT_NONE,
},
)
Nested Data Structures
from flexible_matchers import NUMBER, STRING, LIST, IS_NUMBER
def test_complex_response():
response = {
"users": [
{"id": 1, "name": "Alice", "scores": [95, 87, 92]},
{"id": 2, "name": "Bob", "scores": [88, 91, 85]},
],
"total": 2,
"page": 1,
}
assert response == {
"users": [
{"id": IS_NUMBER, "name": STRING(min_length=1), "scores": LIST(3)},
{"id": IS_NUMBER, "name": STRING(min_length=1), "scores": LIST(3)},
],
"total": NUMBER(min_value=0),
"page": NUMBER(min_value=1),
}
Floating-Point Comparisons
from flexible_matchers import CLOSE_NUMBER
def test_scientific_calculation():
result = calculate_pi()
assert result == CLOSE_NUMBER(3.14159, tolerance=0.00001)
def test_financial_calculation():
total = calculate_total([10.10, 20.20, 30.30])
assert total == CLOSE_NUMBER(60.60, tolerance=0.01)
Development
Setup
# Clone the repository
git clone https://github.com/skippdot/flexible-matchers.git
cd flexible-matchers
# Install development dependencies
pip install -e ".[dev]"
Running Tests
# Run all tests
pytest
# Run with coverage
pytest --cov=flexible_matchers --cov-report=html
# Run specific test file
pytest tests/test_matchers.py
# Run specific test
pytest tests/test_matchers.py::TestNUMBER::test_range
Code Quality
# Format code
black src tests
isort src tests
# Lint code
ruff check src tests
flake8 src tests
pylint src tests
# Type checking
mypy src
Running All Checks
# Format
black src tests && isort src tests
# Lint
ruff check src tests && flake8 src tests && pylint src tests
# Test
pytest --cov=flexible_matchers --cov-report=term-missing
# Type check
mypy src
Requirements
- Python >= 3.8 (tested on 3.8-3.14 across Linux, macOS, and Windows)
- No runtime dependencies!
License
MIT License - see LICENSE file for details.
Contributing
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Acknowledgments
Inspired by:
- pychoir - Modern matcher library
- dirty-equals - Flexible equality testing
- PyHamcrest - Mature matcher framework
- callee - Argument matchers (now abandoned)
Project Stats
- Zero Dependencies: No external packages required
- 100% Test Coverage: Comprehensive test suite
- Type Hinted: Full type annotations
- Python 3.8+: Modern Python support
- Active Maintenance: Regular updates and improvements
Links
Made with by Stepan Shamaiev
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file flexible_matchers-0.1.2.tar.gz.
File metadata
- Download URL: flexible_matchers-0.1.2.tar.gz
- Upload date:
- Size: 13.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
98a1d6e4fc297655fd7a43d62a34fc3e08d2d5164f33a9b81c8dbcad709d04a3
|
|
| MD5 |
f6aa57c8062084c8c590da8ff4dcbdad
|
|
| BLAKE2b-256 |
c573238ca4743c389619ad9a2eb75bd828aa6d4bd875291345d5dd67a3c96fc1
|
File details
Details for the file flexible_matchers-0.1.2-py3-none-any.whl.
File metadata
- Download URL: flexible_matchers-0.1.2-py3-none-any.whl
- Upload date:
- Size: 8.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ccca787a7a4b3bd30e6813dc777fa644cbf4593d975cf83862bddd071179ad8b
|
|
| MD5 |
1be653161bbba47165a1e936ba747253
|
|
| BLAKE2b-256 |
f9ae5991d889c08482d79aa10a91efa5ed5c20890442a5498ff9e5d74a7d456f
|