CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-devtools

Python's missing debug print command and development tools with enhanced debugging, pretty printing, timing, and ANSI styling capabilities.

Pending
Overview
Eval results
Files

pytest-plugin.mddocs/

Pytest Plugin

Pytest integration for enhanced testing workflows with automatic assert statement insertion, test failure reporting, and development-time debugging utilities. The plugin provides insert_assert() functionality for dynamic test generation and integrates devtools debugging capabilities into the pytest environment.

Capabilities

Insert Assert Function

Dynamically insert assert statements during test development for rapid test creation and debugging.

def insert_assert(value) -> int:
    """
    Insert assert statement for testing (requires Python 3.8+).
    
    Analyzes the calling code and generates an assert statement comparing
    the provided value with its current runtime value. The generated assert
    is formatted and can be written to the source file.
    
    Parameters:
    - value: Value to create assert statement for
    
    Returns:
    Number of insert_assert calls in the current test
    
    Raises:
    RuntimeError: If Python version < 3.8 or code inspection fails
    """

Usage examples:

def test_calculation():
    result = calculate_something(10, 20)
    
    # During development, use insert_assert to capture expected values
    insert_assert(result)  # Generates: assert result == 42
    
    # After running tests, the insert_assert calls are replaced with actual asserts
    assert result == 42

def test_data_processing():
    data = process_data([1, 2, 3, 4, 5])
    
    # Multiple insert_assert calls in one test
    insert_assert(len(data))     # Generates: assert len(data) == 5
    insert_assert(data[0])       # Generates: assert data[0] == 2
    insert_assert(sum(data))     # Generates: assert sum(data) == 15
    
    # After processing:
    assert len(data) == 5
    assert data[0] == 2
    assert sum(data) == 15

def test_complex_object():
    obj = create_complex_object()
    
    # Works with complex expressions
    insert_assert(obj.property.method())
    # Generates: assert obj.property.method() == expected_value

Pytest Configuration Options

Command-line options for controlling insert_assert behavior.

# Print generated assert statements instead of writing to files
pytest --insert-assert-print

# Fail tests that contain insert_assert calls (for CI/CD)
pytest --insert-assert-fail

Pytest Fixtures

Built-in fixtures for accessing insert_assert functionality and integration.

@pytest.fixture(name='insert_assert')
def insert_assert_fixture() -> Callable[[Any], int]:
    """
    Pytest fixture providing access to insert_assert function.
    
    Returns:
    insert_assert function for use in test functions
    """

@pytest.fixture(scope='session', autouse=True)
def insert_assert_add_to_builtins() -> None:
    """
    Automatically add insert_assert and debug to builtins for all tests.
    
    Makes insert_assert and debug available without imports in test files.
    """

@pytest.fixture(autouse=True)
def insert_assert_maybe_fail(pytestconfig: pytest.Config) -> Generator[None, None, None]:
    """
    Auto-use fixture that optionally fails tests containing insert_assert calls.
    
    Controlled by --insert-assert-fail command line option.
    """

@pytest.fixture(scope='session', autouse=True) 
def insert_assert_session(pytestconfig: pytest.Config) -> Generator[None, None, None]:
    """
    Session-level fixture handling insert_assert code generation and file writing.
    
    Manages the actual insertion of assert statements into source files.
    """

Usage with fixtures:

def test_with_fixture(insert_assert):
    # Use insert_assert fixture explicitly
    result = some_calculation()
    count = insert_assert(result)
    assert count == 1  # First insert_assert call in this test

def test_builtin_access():
    # insert_assert available without imports due to auto-fixture
    data = get_test_data()
    insert_assert(data['status'])  # Works without explicit import
    
    # debug also available
    debug(data)  # Works without explicit import

Pytest Hooks

Plugin hooks for customizing test behavior and reporting.

def pytest_addoption(parser) -> None:
    """
    Add command-line options for insert_assert functionality.
    
    Adds:
    - --insert-assert-print: Print statements instead of writing files
    - --insert-assert-fail: Fail tests with insert_assert calls
    """

def pytest_report_teststatus(report: pytest.TestReport, config: pytest.Config):
    """
    Custom test status reporting for insert_assert failures.
    
    Shows 'INSERT ASSERT' status for tests failed due to insert_assert calls.
    
    Returns:
    Tuple of (outcome, letter, verbose) for insert_assert failures
    """

def pytest_terminal_summary() -> None:
    """
    Print summary of insert_assert operations at end of test session.
    
    Shows statistics about generated assert statements and file modifications.
    """

Development Workflow Integration

Typical workflow for using insert_assert in test development:

# 1. Initial test development
def test_new_feature():
    result = new_feature_function()
    
    # Use insert_assert to capture actual values during development
    insert_assert(result.status)
    insert_assert(result.data)
    insert_assert(len(result.items))

# 2. Run tests with print mode to see generated asserts
# pytest test_file.py --insert-assert-print

# 3. Review printed assert statements:
# --------------------------------------------------------------------------------
# test_file.py - 5:
# --------------------------------------------------------------------------------
# # insert_assert(result.status)
# assert result.status == 'success'
# --------------------------------------------------------------------------------

# 4. Run tests normally to write asserts to file
# pytest test_file.py

# 5. Final test after insert_assert replacement:
def test_new_feature():
    result = new_feature_function()
    
    # insert_assert calls replaced with actual assertions
    assert result.status == 'success'
    assert result.data == {'key': 'value'}
    assert len(result.items) == 3

Error Handling and Edge Cases

The plugin handles various edge cases gracefully:

def test_error_handling():
    # Works with complex expressions
    obj = get_complex_object()
    insert_assert(obj.nested.property[0].method())
    
    # Handles exceptions during formatting
    problematic_value = get_problematic_value()
    insert_assert(problematic_value)  # Won't crash even if repr() fails
    
    # Works with custom objects
    custom = CustomClass()
    insert_assert(custom)  # Uses appropriate representation

def test_multiple_calls_same_line():
    x, y = 1, 2
    # Multiple insert_assert calls - handles duplicates
    insert_assert(x); insert_assert(y)
    # Second call on same line will be skipped in file writing

Integration with Devtools Debug

Seamless integration with devtools debug functionality:

def test_with_debug():
    # Both debug and insert_assert available in test environment
    data = process_test_data()
    
    # Debug for inspection during development
    debug(data)
    
    # insert_assert for generating test assertions
    insert_assert(data['result'])
    insert_assert(data['error_count'])

def test_debug_in_fixtures(insert_assert):
    # Debug works in pytest fixtures and test functions
    @pytest.fixture
    def test_data():
        data = create_test_data()
        debug(data)  # Available without import
        return data

File Modification and Code Generation

The plugin automatically handles source file modification:

  • Black Integration: Generated code is formatted using Black configuration from pyproject.toml
  • Duplicate Handling: Prevents multiple asserts on the same line
  • Reversible Operation: Original insert_assert calls are replaced, not deleted
  • Safe Writing: Files are only modified when tests pass
# Configuration via pyproject.toml affects generated code formatting
[tool.black]
line-length = 88
target-version = ['py38']

# Generated assert statements respect Black configuration:
# assert very_long_variable_name_here == {
#     "key1": "value1", 
#     "key2": "value2"
# }

Testing Insert Assert Itself

For testing the insert_assert functionality:

def test_insert_assert_behavior():
    # Test that insert_assert returns correct count
    count1 = insert_assert(42)
    count2 = insert_assert("hello")
    
    assert count1 == 1
    assert count2 == 2

def test_insert_assert_with_expressions():
    data = [1, 2, 3]
    
    # Test with expressions
    insert_assert(len(data))
    insert_assert(sum(data))
    insert_assert(data[0])
    
    # These will generate appropriate assert statements

Types

@dataclass
class ToReplace:
    """
    Represents a code replacement operation for insert_assert.
    """
    file: Path         # Source file to modify
    start_line: int    # Starting line number
    end_line: int | None  # Ending line number
    code: str          # Replacement code

class PlainRepr(str):
    """
    String class where repr() doesn't include quotes.
    Used for custom representation in generated assert statements.
    """
    def __repr__(self) -> str: ...

# Context variables for tracking insert_assert state
insert_assert_calls: ContextVar[int]        # Count of calls in current test
insert_assert_summary: ContextVar[list[str]]  # Session summary information

# Global state
to_replace: list[ToReplace]  # List of pending code replacements

Install with Tessl CLI

npx tessl i tessl/pypi-devtools

docs

ansi-styling.md

debug.md

index.md

pretty-printing.md

pytest-plugin.md

timing.md

tile.json