Search code examples
pythontestingpydantic

Pretty print why two objects are not equal


When using pytest, I get nice pretty printing when two objects are not equivalent:

Expected :Foo(id='red', other_thing='green')
Actual   :Foo(id='red', other_thing='blue')
<Click to see difference>

def test_baz():
    
        oneFoo = Foo(id="red", other_thing="blue")
        twoFoo = Foo(id="red", other_thing="green")
    
>       assert oneFoo == twoFoo
E       AssertionError: assert Foo(id='red', other_thing='blue') == Foo(id='red', other_thing='green')
E         
E         Full diff:
E         - Foo(id='red', other_thing='green')
E         ?                            ^^ --
E         + Foo(id='red', other_thing='blue')
E         ?                            ^^^

baz.py:22: AssertionError

If I use an assert directly in my code, I just get an AssertionError and a stacktrace.

I am writing some integration tests right now that are NOT driven by pytest but would like to pretty print when two items (specifically Pydantic dataclasses) are not equal.


Solution

  • This works pretty well:

    from difflib import Differ
    
    import structlog
    from colorama import Fore
    from pydantic import BaseModel
    
    logger = structlog.getLogger(__name__)
    
    def check_equals(expected: BaseModel, actual: BaseModel) -> None:
        if not expected == actual:
    
            logger.error("Objects are not equal", expected=expected, actual=actual)
    
            diff_output = Differ().compare(
                _get_lines(expected),
                _get_lines(actual)
            )
    
            color_diff_output = _color_diff(diff_output)
    
            print("\n".join(color_diff_output))
    
            raise ValueError("Objects not equal")
    
    def _get_lines(item: BaseModel) -> str:
        item_json = item.model_dump_json(indent=4)
        lines = item_json.splitlines()
        return lines
    
    def _color_diff(diff):
        """
        Found this nice function here: https://chezsoi.org/lucas/blog/colored-diff-output-with-python.html
        """
        for line in diff:
            if line.startswith('+'):
                yield Fore.GREEN + line + Fore.RESET
            elif line.startswith('-'):
                yield Fore.RED + line + Fore.RESET
            elif line.startswith('^'):
                yield Fore.BLUE + line + Fore.RESET
            else:
                yield line
    

    Prints:

    enter image description here

    When you run this code:

    from pydantic import BaseModel
    
    from util.integration_test_validation.check_equals import check_equals
    
    class Bar(BaseModel):
        beaches: int
        oranges: int
    
    class Foo(BaseModel):
        id: str
        other_thing: str
        count: int
        bar: Bar
    
    
    
    oneFoo = Foo(id="red", other_thing="blue", count=3, bar=Bar(beaches=5, oranges=7))
    twoFoo = Foo(id="red", other_thing="green", count=3, bar=Bar(beaches=5, oranges=7))
    
    check_equals(oneFoo, twoFoo)