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
Metadata
Release files for flexible-matchers 0.1.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| flexible_matchers-0.1.2.tar.gz | 13.7 kB | Without provenance | Details for flexible_matchers-0.1.2.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| flexible_matchers-0.1.2-py3-none-any.whl | Python 3 | none | any | Details for flexible_matchers-0.1.2-py3-none-any.whl |
Total release size: 21.7 kB
Release files / flexible_matchers-0.1.2.tar.gz
| Download URL | flexible_matchers-0.1.2.tar.gz |
|---|---|
| Size | 13.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
98a1d6e4fc297655fd7a43d62a34fc3e08d2d5164f33a9b81c8dbcad709d04a3
|
|
BLAKE2b-256 checksum How to use checksums |
c573238ca4743c389619ad9a2eb75bd828aa6d4bd875291345d5dd67a3c96fc1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.4
|
Release files / flexible_matchers-0.1.2-py3-none-any.whl
| Download URL | flexible_matchers-0.1.2-py3-none-any.whl |
|---|---|
| Size | 8.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ccca787a7a4b3bd30e6813dc777fa644cbf4593d975cf83862bddd071179ad8b
|
|
BLAKE2b-256 checksum How to use checksums |
f9ae5991d889c08482d79aa10a91efa5ed5c20890442a5498ff9e5d74a7d456f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.4
|
Security for flexible-matchers 0.1.2
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.