Skip to main content

flexible-matchers

PyPI version Python Support License: MIT Code style: black Imports: isort Linting: ruff CI codecov

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.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

Acknowledgments

Inspired by:

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

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)

Source distribution for flexible-matchers 0.1.2
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)

Table of built distributions (wheels) for flexible-matchers 0.1.2
File Interpreter ABI Platform Actions
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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page