Skip to main content

Lightweight, zero-dependency mock assertion helpers with flexible numeric and string matching

Project description

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

Links


Made with by Stepan Shamaiev

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

flexible_matchers-0.1.2.tar.gz (13.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

flexible_matchers-0.1.2-py3-none-any.whl (8.0 kB view details)

Uploaded Python 3

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

Hashes for flexible_matchers-0.1.2.tar.gz
Algorithm Hash digest
SHA256 98a1d6e4fc297655fd7a43d62a34fc3e08d2d5164f33a9b81c8dbcad709d04a3
MD5 f6aa57c8062084c8c590da8ff4dcbdad
BLAKE2b-256 c573238ca4743c389619ad9a2eb75bd828aa6d4bd875291345d5dd67a3c96fc1

See more details on using hashes here.

File details

Details for the file flexible_matchers-0.1.2-py3-none-any.whl.

File metadata

File hashes

Hashes for flexible_matchers-0.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 ccca787a7a4b3bd30e6813dc777fa644cbf4593d975cf83862bddd071179ad8b
MD5 1be653161bbba47165a1e936ba747253
BLAKE2b-256 f9ae5991d889c08482d79aa10a91efa5ed5c20890442a5498ff9e5d74a7d456f

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page