Doing dirty (but extremely useful) things with equals.

Overview

dirty-equals

Doing dirty (but extremely useful) things with equals.

CI Coverage pypi versions license


Documentation: dirty-equals.helpmanual.io

Source Code: github.com/samuelcolvin/dirty-equals


dirty-equals is a python library that (mis)uses the __eq__ method to make python code (generally unit tests) more declarative and therefore easier to read and write.

dirty-equals can be used in whatever context you like, but it comes into its own when writing unit tests for applications where you're commonly checking the response to API calls and the contents of a database.

Usage

Here's a trivial example of what dirty-equals can do:

from dirty_equals import IsPositive

assert 1 == IsPositive
assert -2 == IsPositive  # this will fail!

That doesn't look very useful yet!, but consider the following unit test code using dirty-equals:

from dirty_equals import IsJson, IsNow, IsPositiveInt, IsStr

...

# user_data is a dict returned from a database or API which we want to test
assert user_data == {
    # we want to check that id is a positive int
    'id': IsPositiveInt,
    # we know avatar_file should be a string, but we need a regex as we don't know whole value
    'avatar_file': IsStr(regex=r'/[a-z0-9\-]{10}/example\.png'),
    # settings_json is JSON, but it's more robust to compare the value it encodes, not strings
    'settings_json': IsJson({'theme': 'dark', 'language': 'en'}),
    # created_ts is datetime, we don't know the exact value, but we know it should be close to now
    'created_ts': IsNow(delta=3),
}

Without dirty-equals, you'd have to compare individual fields and/or modify some fields before comparison - the test would not be declarative or as clear.

dirty-equals can do so much more than that, for example:

  • IsPartialDict lets you compare a subset of a dictionary
  • IsStrictDict lets you confirm order in a dictionary
  • IsList and IsTuple lets you compare partial lists and tuples, with or without order constraints
  • nesting any of these types inside any others
  • IsInstance lets you simply confirm the type of an object
  • You can even use boolean operators | and & to combine multiple conditions
  • and much more...

Installation

Simply:

pip install dirty-equals

dirty-equals requires Python 3.7+.

Comments
  • Installing from git (or a git snapshot) results in package version being 0

    Installing from git (or a git snapshot) results in package version being 0

    I've just noticed that when using git snapshots or the git repository itself, dirty-equals ends up being installed as version "0", e.g.:

    $ pip install git+https://github.com/samuelcolvin/dirty-equals
    Collecting git+https://github.com/samuelcolvin/dirty-equals
      Cloning https://github.com/samuelcolvin/dirty-equals to /tmp/pip-req-build-a0xri30s
      Running command git clone --filter=blob:none --quiet https://github.com/samuelcolvin/dirty-equals /tmp/pip-req-build-a0xri30s
      Resolved https://github.com/samuelcolvin/dirty-equals to commit 593bcccf738ab8b724d7cb860881d74344171f5f
      Installing build dependencies ... done
      Getting requirements to build wheel ... done
      Preparing metadata (pyproject.toml) ... done
    Requirement already satisfied: pytz>=2021.3 in ./.venv/lib/python3.11/site-packages (from dirty-equals==0) (2022.1)
    Building wheels for collected packages: dirty-equals
      Building wheel for dirty-equals (pyproject.toml) ... done
      Created wheel for dirty-equals: filename=dirty_equals-0-py3-none-any.whl size=23380 sha256=96b6eecea7fe5c51bc14f23929d3f6146e22344aff7c82749c96fb92c414218e
      Stored in directory: /tmp/pip-ephem-wheel-cache-qjkkipja/wheels/b9/a6/72/32198aa9ff0bfe14230160fbf92821af4a22179c61d55c7070
    Successfully built dirty-equals
    Installing collected packages: dirty-equals
    Successfully installed dirty-equals-0
    
    opened by mgorny 13
  • Feat add IsHash

    Feat add IsHash

    I've kept very simple since this is all I'd want to use to save time in tests and imagined I'd probably want to define a custom regex for most other hashes using IsStr. Just my two cents though.

    opened by osintalex 7
  • Numerous test failures with pypy3.9

    Numerous test failures with pypy3.9

    The following tests fail on pypy3.9 (7.3.9):

    FAILED tests/test_base.py::test_not_repr - Failed: DID NOT RAISE <class 'AssertionError'>
    FAILED tests/test_boolean.py::test_dirty_not_equals - Failed: DID NOT RAISE <class 'AssertionError'>
    FAILED tests/test_dict.py::test_is_dict[input_value16-expected16] - AssertionError: assert {'a': 1, 'b': None} == IsIgnoreDict(a=1)
    FAILED tests/test_dict.py::test_is_dict[input_value17-expected17] - assert {1: 10, 2: None} == IsIgnoreDict(1=10)
    FAILED tests/test_dict.py::test_is_dict[input_value20-expected20] - assert {1: 10, 2: False} == IsIgnoreDict[ignore={False}](1=10)
    FAILED tests/test_dict.py::test_callable_ignore - AssertionError: assert {'a': 1, 'b': 42} == IsDict[ignore=ignore_42](a=1)
    FAILED tests/test_dict.py::test_ignore - AssertionError: assert {'a': 1, 'b': 2, 'c': 3, 'd': 4} == IsDict[ignore=custom_ignore](a=1...
    FAILED tests/test_dict.py::test_ignore_with_is_str - AssertionError: assert {'dob': None, 'id': 123, 'street_address': None, 'token'...
    FAILED tests/test_dict.py::test_unhashable_value - AssertionError: assert {'b': {'a': 1}, 'c': None} == IsIgnoreDict(b={'a': 1})
    FAILED tests/test_docs.py::test_docs_examples[dirty_equals/_inspection.py:172-189] - AssertionError: assert <_inspection_172_189.Foo...
    FAILED tests/test_docs.py::test_docs_examples[dirty_equals/_dict.py:186-204] - AssertionError: assert {'a': 1, 'b': 2, 'c': None} ==...
    FAILED tests/test_inspection.py::test_has_attributes[-HasAttributes(a=IsInt, b=IsStr)] - assert <tests.test_inspection.Foo object at...
    

    Full output:

    ========================================================= test session starts =========================================================
    platform linux -- Python 3.9.12[pypy-7.3.9-final], pytest-7.1.2, pluggy-1.0.0
    rootdir: /tmp/dirty-equals, configfile: pyproject.toml, testpaths: tests
    plugins: forked-1.4.0, xdist-2.5.0, xprocess-0.18.1, anyio-3.5.0
    collected 484 items                                                                                                                   
    
    tests/test_base.py ......F....................                                                                                  [  5%]
    tests/test_boolean.py ..........................F................                                                               [ 14%]
    tests/test_datetime.py .................................................                                                        [ 24%]
    tests/test_dict.py ................FF..F................F.................FFF                                                   [ 36%]
    tests/test_docs.py ..........................F...F..................                                                            [ 46%]
    tests/test_inspection.py ................F...........                                                                           [ 52%]
    tests/test_list_tuple.py ..............................................................................                         [ 68%]
    tests/test_numeric.py ..........................................................                                                [ 80%]
    tests/test_other.py ...................................                                                                         [ 87%]
    tests/test_strings.py ...........................................................                                               [100%]
    
    ============================================================== FAILURES ===============================================================
    ____________________________________________________________ test_not_repr ____________________________________________________________
    
        def test_not_repr():
            v = ~IsInt
            assert str(v) == '~IsInt'
        
            with pytest.raises(AssertionError):
    >           assert 1 == v
    E           Failed: DID NOT RAISE <class 'AssertionError'>
    
    tests/test_base.py:66: Failed
    ________________________________________________________ test_dirty_not_equals ________________________________________________________
    
        def test_dirty_not_equals():
            with pytest.raises(AssertionError):
    >           assert 0 != IsFalseLike
    E           Failed: DID NOT RAISE <class 'AssertionError'>
    
    tests/test_boolean.py:48: Failed
    _______________________________________________ test_is_dict[input_value16-expected16] ________________________________________________
    
    input_value = {'a': 1, 'b': None}, expected = IsIgnoreDict(a=1)
    
        @pytest.mark.parametrize(
            'input_value,expected',
            [
                ({}, IsDict),
                ({}, IsDict()),
                ({'a': 1}, IsDict(a=1)),
                ({1: 2}, IsDict({1: 2})),
                ({'a': 1, 'b': 2}, IsDict(a=1, b=2)),
                ({'b': 2, 'a': 1}, IsDict(a=1, b=2)),
                ({'a': 1, 'b': None}, IsDict(a=1, b=None)),
                ({'a': 1, 'b': 3}, ~IsDict(a=1, b=2)),
                # partial dict
                ({1: 10, 2: 20}, IsPartialDict({1: 10})),
                ({1: 10}, IsPartialDict({1: 10})),
                ({1: 10, 2: 20}, IsPartialDict({1: 10})),
                ({1: 10, 2: 20}, IsDict({1: 10}).settings(partial=True)),
                ({1: 10}, ~IsPartialDict({1: 10, 2: 20})),
                ({1: 10, 2: None}, ~IsPartialDict({1: 10, 2: 20})),
                # ignore dict
                ({}, IsIgnoreDict()),
                ({'a': 1, 'b': 2}, IsIgnoreDict(a=1, b=2)),
                ({'a': 1, 'b': None}, IsIgnoreDict(a=1)),
                ({1: 10, 2: None}, IsIgnoreDict({1: 10})),
                ({'a': 1, 'b': 2}, ~IsIgnoreDict(a=1)),
                ({1: 10, 2: False}, ~IsIgnoreDict({1: 10})),
                ({1: 10, 2: False}, IsIgnoreDict({1: 10}).settings(ignore={False})),
                # strict dict
                ({}, IsStrictDict()),
                ({'a': 1, 'b': 2}, IsStrictDict(a=1, b=2)),
                ({'a': 1, 'b': 2}, ~IsStrictDict(b=2, a=1)),
                ({1: 10, 2: 20}, IsStrictDict({1: 10, 2: 20})),
                ({1: 10, 2: 20}, ~IsStrictDict({2: 20, 1: 10})),
                ({1: 10, 2: 20}, ~IsDict({2: 20, 1: 10}).settings(strict=True)),
                # combining types
                ({'a': 1, 'b': 2, 'c': 3}, IsStrictDict(a=1, c=3).settings(partial=True)),
                ({'a': 1, 'b': 2, 'c': 3}, IsStrictDict(a=1, b=2).settings(partial=True)),
                ({'a': 1, 'b': 2, 'c': 3}, IsStrictDict(b=2, c=3).settings(partial=True)),
                ({'a': 1, 'c': 3, 'b': 2}, ~IsStrictDict(b=2, c=3).settings(partial=True)),
            ],
        )
        def test_is_dict(input_value, expected):
    >       assert input_value == expected
    E       AssertionError: assert {'a': 1, 'b': None} == IsIgnoreDict(a=1)
    
    tests/test_dict.py:47: AssertionError
    _______________________________________________ test_is_dict[input_value17-expected17] ________________________________________________
    
    input_value = {1: 10, 2: None}, expected = IsIgnoreDict(1=10)
    
        @pytest.mark.parametrize(
            'input_value,expected',
            [
                ({}, IsDict),
                ({}, IsDict()),
                ({'a': 1}, IsDict(a=1)),
                ({1: 2}, IsDict({1: 2})),
                ({'a': 1, 'b': 2}, IsDict(a=1, b=2)),
                ({'b': 2, 'a': 1}, IsDict(a=1, b=2)),
                ({'a': 1, 'b': None}, IsDict(a=1, b=None)),
                ({'a': 1, 'b': 3}, ~IsDict(a=1, b=2)),
                # partial dict
                ({1: 10, 2: 20}, IsPartialDict({1: 10})),
                ({1: 10}, IsPartialDict({1: 10})),
                ({1: 10, 2: 20}, IsPartialDict({1: 10})),
                ({1: 10, 2: 20}, IsDict({1: 10}).settings(partial=True)),
                ({1: 10}, ~IsPartialDict({1: 10, 2: 20})),
                ({1: 10, 2: None}, ~IsPartialDict({1: 10, 2: 20})),
                # ignore dict
                ({}, IsIgnoreDict()),
                ({'a': 1, 'b': 2}, IsIgnoreDict(a=1, b=2)),
                ({'a': 1, 'b': None}, IsIgnoreDict(a=1)),
                ({1: 10, 2: None}, IsIgnoreDict({1: 10})),
                ({'a': 1, 'b': 2}, ~IsIgnoreDict(a=1)),
                ({1: 10, 2: False}, ~IsIgnoreDict({1: 10})),
                ({1: 10, 2: False}, IsIgnoreDict({1: 10}).settings(ignore={False})),
                # strict dict
                ({}, IsStrictDict()),
                ({'a': 1, 'b': 2}, IsStrictDict(a=1, b=2)),
                ({'a': 1, 'b': 2}, ~IsStrictDict(b=2, a=1)),
                ({1: 10, 2: 20}, IsStrictDict({1: 10, 2: 20})),
                ({1: 10, 2: 20}, ~IsStrictDict({2: 20, 1: 10})),
                ({1: 10, 2: 20}, ~IsDict({2: 20, 1: 10}).settings(strict=True)),
                # combining types
                ({'a': 1, 'b': 2, 'c': 3}, IsStrictDict(a=1, c=3).settings(partial=True)),
                ({'a': 1, 'b': 2, 'c': 3}, IsStrictDict(a=1, b=2).settings(partial=True)),
                ({'a': 1, 'b': 2, 'c': 3}, IsStrictDict(b=2, c=3).settings(partial=True)),
                ({'a': 1, 'c': 3, 'b': 2}, ~IsStrictDict(b=2, c=3).settings(partial=True)),
            ],
        )
        def test_is_dict(input_value, expected):
    >       assert input_value == expected
    E       assert {1: 10, 2: None} == IsIgnoreDict(1=10)
    
    tests/test_dict.py:47: AssertionError
    _______________________________________________ test_is_dict[input_value20-expected20] ________________________________________________
    
    input_value = {1: 10, 2: False}, expected = IsIgnoreDict[ignore={False}](1=10)
    
        @pytest.mark.parametrize(
            'input_value,expected',
            [
                ({}, IsDict),
                ({}, IsDict()),
                ({'a': 1}, IsDict(a=1)),
                ({1: 2}, IsDict({1: 2})),
                ({'a': 1, 'b': 2}, IsDict(a=1, b=2)),
                ({'b': 2, 'a': 1}, IsDict(a=1, b=2)),
                ({'a': 1, 'b': None}, IsDict(a=1, b=None)),
                ({'a': 1, 'b': 3}, ~IsDict(a=1, b=2)),
                # partial dict
                ({1: 10, 2: 20}, IsPartialDict({1: 10})),
                ({1: 10}, IsPartialDict({1: 10})),
                ({1: 10, 2: 20}, IsPartialDict({1: 10})),
                ({1: 10, 2: 20}, IsDict({1: 10}).settings(partial=True)),
                ({1: 10}, ~IsPartialDict({1: 10, 2: 20})),
                ({1: 10, 2: None}, ~IsPartialDict({1: 10, 2: 20})),
                # ignore dict
                ({}, IsIgnoreDict()),
                ({'a': 1, 'b': 2}, IsIgnoreDict(a=1, b=2)),
                ({'a': 1, 'b': None}, IsIgnoreDict(a=1)),
                ({1: 10, 2: None}, IsIgnoreDict({1: 10})),
                ({'a': 1, 'b': 2}, ~IsIgnoreDict(a=1)),
                ({1: 10, 2: False}, ~IsIgnoreDict({1: 10})),
                ({1: 10, 2: False}, IsIgnoreDict({1: 10}).settings(ignore={False})),
                # strict dict
                ({}, IsStrictDict()),
                ({'a': 1, 'b': 2}, IsStrictDict(a=1, b=2)),
                ({'a': 1, 'b': 2}, ~IsStrictDict(b=2, a=1)),
                ({1: 10, 2: 20}, IsStrictDict({1: 10, 2: 20})),
                ({1: 10, 2: 20}, ~IsStrictDict({2: 20, 1: 10})),
                ({1: 10, 2: 20}, ~IsDict({2: 20, 1: 10}).settings(strict=True)),
                # combining types
                ({'a': 1, 'b': 2, 'c': 3}, IsStrictDict(a=1, c=3).settings(partial=True)),
                ({'a': 1, 'b': 2, 'c': 3}, IsStrictDict(a=1, b=2).settings(partial=True)),
                ({'a': 1, 'b': 2, 'c': 3}, IsStrictDict(b=2, c=3).settings(partial=True)),
                ({'a': 1, 'c': 3, 'b': 2}, ~IsStrictDict(b=2, c=3).settings(partial=True)),
            ],
        )
        def test_is_dict(input_value, expected):
    >       assert input_value == expected
    E       assert {1: 10, 2: False} == IsIgnoreDict[ignore={False}](1=10)
    
    tests/test_dict.py:47: AssertionError
    ________________________________________________________ test_callable_ignore _________________________________________________________
    
        def test_callable_ignore():
            assert {'a': 1} == IsDict(a=1).settings(ignore=ignore_42)
    >       assert {'a': 1, 'b': 42} == IsDict(a=1).settings(ignore=ignore_42)
    E       AssertionError: assert {'a': 1, 'b': 42} == IsDict[ignore=ignore_42](a=1)
    E        +  where IsDict[ignore=ignore_42](a=1) = <bound method IsDict.settings of IsDict(a=1)>(ignore=ignore_42)
    E        +    where <bound method IsDict.settings of IsDict(a=1)> = IsDict(a=1).settings
    E        +      where IsDict(a=1) = IsDict(a=1)
    
    tests/test_dict.py:95: AssertionError
    _____________________________________________________________ test_ignore _____________________________________________________________
    
        def test_ignore():
            def custom_ignore(v: int) -> bool:
                return v % 2 == 0
        
    >       assert {'a': 1, 'b': 2, 'c': 3, 'd': 4} == IsDict(a=1, c=3).settings(ignore=custom_ignore)
    E       AssertionError: assert {'a': 1, 'b': 2, 'c': 3, 'd': 4} == IsDict[ignore=custom_ignore](a=1, c=3)
    E        +  where IsDict[ignore=custom_ignore](a=1, c=3) = <bound method IsDict.settings of IsDict(a=1, c=3)>(ignore=<function test_ignore.<locals>.custom_ignore at 0x00007f313d03a020>)
    E        +    where <bound method IsDict.settings of IsDict(a=1, c=3)> = IsDict(a=1, c=3).settings
    E        +      where IsDict(a=1, c=3) = IsDict(a=1, c=3)
    
    tests/test_dict.py:129: AssertionError
    _______________________________________________________ test_ignore_with_is_str _______________________________________________________
    
        def test_ignore_with_is_str():
            api_data = {'id': 123, 'token': 't-abc123', 'dob': None, 'street_address': None}
        
            token_is_str = IsStr(regex=r't\-.+')
    >       assert api_data == IsIgnoreDict(id=IsPositiveInt, token=token_is_str)
    E       AssertionError: assert {'dob': None, 'id': 123, 'street_address': None, 'token': 't-abc123'} == IsIgnoreDict(id=IsPositiveInt, token=IsStr(regex='t\\-.+'))
    E        +  where IsIgnoreDict(id=IsPositiveInt, token=IsStr(regex='t\\-.+')) = IsIgnoreDict(id=IsPositiveInt, token=IsStr(regex='t\\-.+'))
    
    tests/test_dict.py:136: AssertionError
    ________________________________________________________ test_unhashable_value ________________________________________________________
    
        def test_unhashable_value():
            a = {'a': 1}
            api_data = {'b': a, 'c': None}
    >       assert api_data == IsIgnoreDict(b=a)
    E       AssertionError: assert {'b': {'a': 1}, 'c': None} == IsIgnoreDict(b={'a': 1})
    E        +  where IsIgnoreDict(b={'a': 1}) = IsIgnoreDict(b={'a': 1})
    
    tests/test_dict.py:143: AssertionError
    _______________________________________ test_docs_examples[dirty_equals/_inspection.py:172-189] _______________________________________
    
    module_name = '_inspection_172_189'
    source_code = '\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\...IsStr)\nassert Foo(1, 2) != HasAttributes(a=1, b=2, c=3)\nassert Foo(1, 2) == HasAttributes(a=1, b=2, spam=AnyThing)\n'
    import_execute = <function import_execute.<locals>._import_execute at 0x00007f313da302a0>
    
        def test_docs_examples(module_name, source_code, import_execute):
    >       import_execute(module_name, source_code, True)
    
    tests/test_docs.py:69: 
    _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
    tests/test_docs.py:25: in _import_execute
        spec.loader.exec_module(module)
    /usr/lib/pypy3.9/site-packages/_pytest/assertion/rewrite.py:168: in exec_module
        exec(co, module.__dict__)
    _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
    
        from dirty_equals import HasAttributes, IsInt, IsStr, AnyThing
        
        class Foo:
            def __init__(self, a, b):
                self.a = a
                self.b = b
        
            def spam(self):
                pass
        
        assert Foo(1, 2) == HasAttributes(a=1, b=2)
        assert Foo(1, 2) == HasAttributes(a=1)
    >   assert Foo(1, 's') == HasAttributes(a=IsInt, b=IsStr)
    E   AssertionError: assert <_inspection_172_189.Foo object at 0x00007f313d70b750> == HasAttributes(a=IsInt, b=IsStr)
    E    +  where <_inspection_172_189.Foo object at 0x00007f313d70b750> = <class '_inspection_172_189.Foo'>(1, 's')
    E    +  and   HasAttributes(a=IsInt, b=IsStr) = HasAttributes(a=IsInt, b=IsStr)
    
    ../pytest-of-mgorny/pytest-15/test_docs_examples_dirty_equal26/_inspection_172_189.py:185: AssertionError
    __________________________________________ test_docs_examples[dirty_equals/_dict.py:186-204] __________________________________________
    
    module_name = '_dict_186_204'
    source_code = "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\...(a=1, c=3).settings(strict=True)\nassert {'b': None, 'c': 3, 'a': 1} != IsIgnoreDict(a=1, c=3).settings(strict=True)\n"
    import_execute = <function import_execute.<locals>._import_execute at 0x00007f313f1be2a0>
    
        def test_docs_examples(module_name, source_code, import_execute):
    >       import_execute(module_name, source_code, True)
    
    tests/test_docs.py:69: 
    _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
    tests/test_docs.py:25: in _import_execute
        spec.loader.exec_module(module)
    /usr/lib/pypy3.9/site-packages/_pytest/assertion/rewrite.py:168: in exec_module
        exec(co, module.__dict__)
    _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
    
        from dirty_equals import IsIgnoreDict
        
    >   assert {'a': 1, 'b': 2, 'c': None} == IsIgnoreDict(a=1, b=2)
    E   AssertionError: assert {'a': 1, 'b': 2, 'c': None} == IsIgnoreDict(a=1, b=2)
    E    +  where IsIgnoreDict(a=1, b=2) = IsIgnoreDict(a=1, b=2)
    
    ../pytest-of-mgorny/pytest-15/test_docs_examples_dirty_equal30/_dict_186_204.py:189: AssertionError
    ________________________________________ test_has_attributes[-HasAttributes(a=IsInt, b=IsStr)] ________________________________________
    
    value = <tests.test_inspection.Foo object at 0x00007f313eb0bc20>, dirty = HasAttributes(a=IsInt, b=IsStr)
    
        @pytest.mark.parametrize(
            'value,dirty',
            [
                (Foo(1, 2), HasAttributes(a=1, b=2)),
                (Foo(1, 's'), HasAttributes(a=IsInt, b=IsStr)),
                (Foo(1, 2), ~HasAttributes(a=IsInt, b=IsStr)),
                (Foo(1, 2), ~HasAttributes(a=1, b=2, c=3)),
                (Foo(1, 2), HasAttributes(a=1, b=2, spam=AnyThing)),
                (Foo(1, 2), ~HasAttributes(a=1, b=2, missing=AnyThing)),
            ],
            ids=dirty_repr,
        )
        def test_has_attributes(value, dirty):
    >       assert value == dirty
    E       assert <tests.test_inspection.Foo object at 0x00007f313eb0bc20> == HasAttributes(a=IsInt, b=IsStr)
    
    tests/test_inspection.py:86: AssertionError
    ======================================================= short test summary info =======================================================
    FAILED tests/test_base.py::test_not_repr - Failed: DID NOT RAISE <class 'AssertionError'>
    FAILED tests/test_boolean.py::test_dirty_not_equals - Failed: DID NOT RAISE <class 'AssertionError'>
    FAILED tests/test_dict.py::test_is_dict[input_value16-expected16] - AssertionError: assert {'a': 1, 'b': None} == IsIgnoreDict(a=1)
    FAILED tests/test_dict.py::test_is_dict[input_value17-expected17] - assert {1: 10, 2: None} == IsIgnoreDict(1=10)
    FAILED tests/test_dict.py::test_is_dict[input_value20-expected20] - assert {1: 10, 2: False} == IsIgnoreDict[ignore={False}](1=10)
    FAILED tests/test_dict.py::test_callable_ignore - AssertionError: assert {'a': 1, 'b': 42} == IsDict[ignore=ignore_42](a=1)
    FAILED tests/test_dict.py::test_ignore - AssertionError: assert {'a': 1, 'b': 2, 'c': 3, 'd': 4} == IsDict[ignore=custom_ignore](a=1...
    FAILED tests/test_dict.py::test_ignore_with_is_str - AssertionError: assert {'dob': None, 'id': 123, 'street_address': None, 'token'...
    FAILED tests/test_dict.py::test_unhashable_value - AssertionError: assert {'b': {'a': 1}, 'c': None} == IsIgnoreDict(b={'a': 1})
    FAILED tests/test_docs.py::test_docs_examples[dirty_equals/_inspection.py:172-189] - AssertionError: assert <_inspection_172_189.Foo...
    FAILED tests/test_docs.py::test_docs_examples[dirty_equals/_dict.py:186-204] - AssertionError: assert {'a': 1, 'b': 2, 'c': None} ==...
    FAILED tests/test_inspection.py::test_has_attributes[-HasAttributes(a=IsInt, b=IsStr)] - assert <tests.test_inspection.Foo object at...
    =================================================== 12 failed, 472 passed in 3.99s ====================================================
    
    opened by mgorny 5
  • feat Add IsIP

    feat Add IsIP

    Here's a stab at IsIP.

    I'm not sure how useful the netmask keyword argument is but thought I'd include it so you can see. Should be easy to take it out if you don't think it's a good idea.

    opened by osintalex 4
  • Make IsNow relative to current moment of time

    Make IsNow relative to current moment of time

    Issue: https://github.com/samuelcolvin/dirty-equals/issues/39

    Implemented logic that updates theapprox value of IsNow for every comparison. Other ideas, that I've considered:

    • set the approx value to None and generate now on every comparison. Downside -- hard to make repr
    • set the approx value to the current time on __ini__ and do not use it for comparison. Downside -- repr will be lying.
    • Remove internal state from IsNow and on every compassion create an instance of class DateTime and use it for comparing with other value. More work, so I started from the easiest one.

    The downside of my implementation is that the internal state is changing for every comparison, instead of not having a state at all.

    opened by hyzyla 4
  • Depend on typing-extensions only for Python < 3.8

    Depend on typing-extensions only for Python < 3.8

    typing.Literal is part of stdlib since Python 3.8, and since this is the only type where fallback to typing-extensions is used, this package is never used in Python 3.8 and newer. Update the dependency accordingly.

    opened by mgorny 3
  • Idea: generate is now for IsNow only at moment when comparing

    Idea: generate is now for IsNow only at moment when comparing

    My use case is pytest with parametrize method. In such situation, IsNow() will compare time generated when pytest will collect tests and now time when tests were run.

    @pytest.mark.parametrize(
        "params, expected",
        [
            (
               {'some_key': 'some_value'},
               IsNow(),
            ),
            ...,
        ],
    )
    def test_handler(
        client,
        params
        expected,
    ):
        response = make_request(params)
        response['time'] = expected
    
    opened by hyzyla 3
  • Include documentation on how this works

    Include documentation on how this works

    I find this functionality quite interesting and think it could be useful. But I'm a bit hesitant to include a library with "magical" behavior that I don't understand.

    My main question is, why is the __eq__ method of the right operand used? The python documentation seems to suggest that x == y should call x.__eq__(y).

    If the operands are of different types, and right operand’s type is a direct or indirect subclass of the left operand’s type, the reflected method of the right operand has priority, otherwise the left operand’s method has priority.

    The last paragraph in the docs above point me to the __eq__ implementation in the DirtyEqualsMeta class. But I'm not sure what is going on.

    opened by Marco-Kaulea 3
  • IsDict - Allow ignoring keys

    IsDict - Allow ignoring keys

    Hi,

    I am loving this library 😍

    I am using it to test my FastAPI endpoint and I want to ignore the default columns like is_active (also id) because I have something like below:

    assert [{'id': 1, 'name': 'john'}] == IsList(IsPartialDict(name='john', is_active = True})
    

    Certainly, I can use .settings(ignore={True}) but I fear it might ignore other columns with the value True in the future.

    Right now, the above assertion will fail but with ignore_keys we will be able to ignore keys safely.

    P.S. I love to make a PR but I checked the source code and I think I shouldn't touch it.

    opened by jd-solanki 3
  • Some ideas about partial lists

    Some ideas about partial lists

    Hi @samuelcolvin

    This looks really exciting, I think a library like this should have been created a long time ago :slightly_smiling_face:

    Some time ago I played around with similar idea of partial data equality checks to simplify test assertions and wrote a small library for this. I just wanted to share a couple of thoughts that you might find interesting. (Or maybe you already considered this, in that case - sorry for bothering)

    In my experience a very common and annoying problem is when you need to ensure that a list contains all expected elements but their order is unknown and you don't care about order anyway. When elements are not hashable and not comparable you can't just sort two lists or turn them into sets. For example:

    expected_result = [{"id": 1, "name": "John"}, {"id": 2, "name": "Jane"}]
    assert result == expected_result # can't just do that
    

    Possible workaround might be something like this:

    sorted(result, key=itemgetter("id")) == sorted(expected_result, key=itemgetter("id"))
    

    Or perhaps a helper function:

    def unordered_equals(first: list, second: list) -> bool:
        return all(el in second for el in first) and all(el in first for el in second)
    

    I think it would be great to have a simple way for doing such checks.

    Also, sometimes I need to check only specific element(s) in a collection. I often notice that I want to be able to do something like this:

    [1, 2, 3] == StartsWith([1, 2])
    [1, 2, 3] == Contains(3)
    
    # We only need to check that a user has a post with id=42
    response == IsPartialDict({
        "username": "John",
        "posts": Contains(IsPartialDict(id=42)),
    })
    
    opened by qweeze 3
  • is it possible to hide `AnyThing` diffs?

    is it possible to hide `AnyThing` diffs?

    I don't know if this is feasible, but I would love it if dirty-equals would make pytest -vv ignore diffs in AnyThing values. Example:

    from dirty_equals import AnyThing
    
    
    def test_dirty():
        x = {
            "foo": [1, 2, 3, 4, 5, 6, 7, 87, 8, 45, 3, 3, 2, 35, 4, 6, 56, 56, 34],
            "bar": "zbr",
        }
        assert x == {"foo": AnyThing, "bar": "zbr..."}
    

    Result:

    ================================================================================================= FAILURES ==================================================================================================
    ________________________________________________________________________________________________ test_dirty _________________________________________________________________________________________________
    
        def test_dirty():
            x = {
                "foo": [1, 2, 3, 4, 5, 6, 7, 87, 8, 45, 3, 3, 2, 35, 4, 6, 56, 56, 34],
                "bar": "zbr",
            }
    >       assert x == {"foo": AnyThing, "bar": "zbr..."}
    E       AssertionError: assert {'bar': 'zbr',\n 'foo': [1, 2, 3, 4, 5, 6, 7, 87, 8, 45, 3, 3, 2, 35, 4, 6, 56, 56, 34]} == {'bar': 'zbr...', 'foo': AnyThing}
    E         Common items:
    E         {'foo': [1, 2, 3, 4, 5, 6, 7, 87, 8, 45, 3, 3, 2, 35, 4, 6, 56, 56, 34]}
    E         Differing items:
    E         {'bar': 'zbr'} != {'bar': 'zbr...'}
    E         Full diff:
    E           {
    E         -  'bar': 'zbr...',
    E         ?             ---
    E         +  'bar': 'zbr',
    E         -  'foo': AnyThing,
    E         +  'foo': [1,
    E         +          2,
    E         +          3,
    E         +          4,
    E         +          5,
    E         +          6,
    E         +          7,
    E         +          87,
    E         +          8,
    E         +          45,
    E         +          3,
    E         +          3,
    E         +          2,
    E         +          35,
    E         +          4,
    E         +          6,
    E         +          56,
    E         +          56,
    E         +          34],
    E           }
    
    /tmp/test.py:9: AssertionError
    ========================================================================================== short test summary info ==========================================================================================
    FAILED ../../tmp/test.py::test_dirty - AssertionError: assert {'bar': 'zbr',\n 'foo': [1, 2, 3, 4, 5, 6, 7, 87, 8, 45, 3, 3, 2, 35, 4, 6, 56, 56, 34]} == {'bar': 'zbr...', 'foo': AnyThing}
    !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! stopping after 1 failures !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
    ============================================================================================= 1 failed in 0.01s =============================================================================================
    

    I don't really care to show the "foo" content in the diff.

    question 
    opened by gjcarneiro 2
  • PyPy3.9 7.3.10 test regression: tests/test_inspection.py::test_has_attributes[-HasAttributes(a=1, b=2, spam=AnyThing)]

    PyPy3.9 7.3.10 test regression: tests/test_inspection.py::test_has_attributes[-HasAttributes(a=1, b=2, spam=AnyThing)]

    It seems that PyPy3.9 7.3.10 brings a test regression, compared to 7.3.9:

    $ /tmp/pypy3.9-v7.3.10-linux64/bin/pypy3 -m pytest
    ========================================================= test session starts =========================================================
    platform linux -- Python 3.9.15[pypy-7.3.10-final], pytest-7.2.0, pluggy-1.0.0
    rootdir: /tmp/dirty-equals, configfile: pyproject.toml, testpaths: tests
    collected 544 items                                                                                                                   
    
    tests/test_base.py ......s......................                                                                                [  5%]
    tests/test_boolean.py ..........................s.................                                                              [ 13%]
    tests/test_datetime.py .................................................                                                        [ 22%]
    tests/test_dict.py ..........................................................                                                   [ 33%]
    tests/test_docs.py ssssssssssssssssssssssssssssssssssssssssssssssssssss                                                         [ 42%]
    tests/test_inspection.py ...................F........                                                                           [ 47%]
    tests/test_list_tuple.py ..............................................................................                         [ 62%]
    tests/test_numeric.py ..........................................................                                                [ 72%]
    tests/test_other.py .........................................................................................                   [ 89%]
    tests/test_strings.py ...........................................................                                               [100%]
    
    ============================================================== FAILURES ===============================================================
    ____________________________________ test_has_attributes[-HasAttributes(a=1, b=2, spam=AnyThing)] _____________________________________
    
    value = <tests.test_inspection.Foo object at 0x00007f0502d59050>, dirty = HasAttributes(a=1, b=2, spam=AnyThing)
    
        @pytest.mark.parametrize(
            'value,dirty',
            [
                (Foo(1, 2), HasAttributes(a=1, b=2)),
                (Foo(1, 's'), HasAttributes(a=IsInt(), b=IsStr())),
                (Foo(1, 2), ~HasAttributes(a=IsInt(), b=IsStr())),
                (Foo(1, 2), ~HasAttributes(a=1, b=2, c=3)),
                (Foo(1, 2), HasAttributes(a=1, b=2, spam=AnyThing)),
                (Foo(1, 2), ~HasAttributes(a=1, b=2, missing=AnyThing)),
            ],
            ids=dirty_repr,
        )
        def test_has_attributes(value, dirty):
    >       assert value == dirty
    E       assert <tests.test_inspection.Foo object at 0x00007f0502d59050> == HasAttributes(a=1, b=2, spam=AnyThing)
    
    tests/test_inspection.py:86: AssertionError
    ======================================================= short test summary info =======================================================
    FAILED tests/test_inspection.py::test_has_attributes[-HasAttributes(a=1, b=2, spam=AnyThing)] - assert <tests.test_inspection.Foo object at 0x00007f0502d59050> == HasAttributes(a=1, b=2, spam=AnyThing)
    ============================================== 1 failed, 489 passed, 54 skipped in 3.09s ==============================================
    

    It seems that value != expected_value is True for the pair of <bound method Foo.spam of <tests.test_inspection.Foo object at 0x00007fd043e47600>> and AnyThing. For some reason, AnyThing.equals() isn't being used in 7.3.10. I'm afraid I can't debug beyond that because I don't know how it's supposed to work in the first place.

    opened by mgorny 2
  • Add assert_equal(..., reple={

    Add assert_equal(..., reple={"selector": value"})

    I took a stab at implementing this. Here are some of the blockers I've found:

    1. The dirty equals types don't respect the property of eval(repr(item)) == item.
    2. Imports and reprs: if users do from datetime import datetime the repr will still be datetime.datetime(...). And obviously we can't support repl={'["id"]': uuid4()} because the repr is going to be UUID(...) but that's expected.
    3. Unused imports: if the user does from dirty_equals import IsNow then when we comment out the insert_assert(...) line linters will complain about IsNow being unused. Do we comment out imports as well? I'm somewhat at a loss on this one.

    Maybe the answer is to not comment out the insert_assert line but instead just make it a no-op as long as the generated expected_value matches the current one. This would fix all of the above issues but it also means that if you uninstall dirty-equals all of your tests will now be broken and you'd have to go and delete all of the insert_assert statements. I thought commenting that line out was nice because the end result can run without even having dirty-equals installed or knowing how insert_assert works...

    opened by adriangb 0
  • adding `insert_assert` function and make pytest-plugin

    adding `insert_assert` function and make pytest-plugin

    See https://gist.github.com/samuelcolvin/625a1655c2a73e02469fc3c27285ca42

    To use now:

    install:

    pip install git+https://github.com/samuelcolvin/[email protected]_assert
    

    With that you should be able to add insert_assert() into your tests and it should "just work ™",

    Example code:

    def test_string():
        thing = 'foobar'
        insert_assert(thing)
    
    def test_list_callable():
        def foobar():
            return ['foo', 1, b'bytes']
        insert_assert(foobar())
    
    def test_comprehension():
        insert_assert([f'x{i}' for i in range(10)])
    

    Still to do:

    • [x] tests, or at least ignore where impossible
    • [ ] docs
    opened by samuelcolvin 3
  • fix `IsNow`

    fix `IsNow`

    The following should pass

    assert '2022-07-15T10:56:38.311Z' == IsNow(delta=10, tz='utc', format_string='%Y-%m-%dT%H:%M:%S.%fZ', enforce_tz=False)
    

    (ignoring that that's not "now" any longer obvisouly)

    opened by samuelcolvin 0
  • Wrong repr?

    Wrong repr?

    I have a items array, and then the problem is that _id is different, but the repr of price is not 79. Should this be supported?

    E         -  'items': [{'_id': '5f3dbd00fa942fcc2d1053bc',
    E         +  'items': [{'_id': ObjectId('629778b83406c5b07c9b0eb7'),
    E         -             'price': IsPositiveInt(),
    E         +             'price': 79,
    
    opened by Kludex 1
Releases(v0.5.0)
  • v0.5.0(Aug 30, 2022)

    • Document how the dirty __eq__ is called by @Marco-Kaulea in #41
    • Make IsNow relative to current moment of time by @hyzyla in #40
    • correct version in pyproject.toml, #46
    • feat Add IsIP by @osintalex in #43
    • Remove Poetry and transition to hatchling, #49

    Full Changelog: https://github.com/samuelcolvin/dirty-equals/compare/v0.4...v0.5

    Source code(tar.gz)
    Source code(zip)
  • v0.4(Apr 28, 2022)

  • v0.3(Apr 18, 2022)

  • v0.2.1(Mar 30, 2022)

  • v0.2(Mar 29, 2022)

    • Corrected grammar by @barnabywalters, #17
    • Upgrade actions/setup-python by @cclauss, #19
    • Try upgrading mkdocstrings, #21
    • Add IsDate and IsToday by @cinarizasyon, #20
    • fix build, uprev dependencies, #24
    • feat: add IsFalseLike by @osintalex, #23
    • add IsTrueLike type, #25
    • Inspection types - HasName, HasRepr, HasAttributes, #26

    Full Changelog: https://github.com/samuelcolvin/dirty-equals/compare/v0.1...v0.2

    Source code(tar.gz)
    Source code(zip)
  • v0.1(Feb 25, 2022)

  • v0.0.5(Feb 24, 2022)

  • v0.0.4(Feb 23, 2022)

  • v0.0.3(Feb 22, 2022)

  • v0.0.2(Jan 28, 2022)

  • v0.0.1(Jan 27, 2022)

Owner
Samuel Colvin
Python & Rust developer with a hint of TypeScript, maintainer of pydantic and other libraries. he/him.
Samuel Colvin
A single module to link Python ecosystem to the Web

A single module to link Python ecosystem to the Web. Have a quick look at the Gallery first to get convinced ! FAQ For any questions, please use Stack

66 Dec 21, 2022
Simple frontend TypeScript testing utility

TSFTest Simple frontend TypeScript testing utility. Installation Install webpack in your project directory: npm install --save-dev webpack webpack-cli

2 Nov 09, 2021
Code for "SUGAR: Subgraph Neural Network with Reinforcement Pooling and Self-Supervised Mutual Information Mechanism"

SUGAR Code for "SUGAR: Subgraph Neural Network with Reinforcement Pooling and Self-Supervised Mutual Information Mechanism" Overview train.py: the cor

41 Nov 08, 2022
A command-line tool and Python library and Pytest plugin for automated testing of RESTful APIs, with a simple, concise and flexible YAML-based syntax

1.0 Release See here for details about breaking changes with the upcoming 1.0 release: https://github.com/taverntesting/tavern/issues/495 Easier API t

909 Dec 15, 2022
pytest plugin that let you automate actions and assertions with test metrics reporting executing plain YAML files

pytest-play pytest-play is a codeless, generic, pluggable and extensible automation tool, not necessarily test automation only, based on the fantastic

pytest-dev 67 Dec 01, 2022
A grab-bag of nifty pytest plugins

A goody-bag of nifty plugins for pytest OS Build Coverage Plugin Description Supported OS pytest-server-fixtures Extensible server-running framework w

Man Group 492 Jan 03, 2023
Aioresponses is a helper for mock/fake web requests in python aiohttp package.

aioresponses Aioresponses is a helper to mock/fake web requests in python aiohttp package. For requests module there are a lot of packages that help u

402 Jan 06, 2023
A wrapper for webdriver that is a jumping off point for web automation.

Webdriver Automation Plus ===================================== Description: Tests the user can save messages then find them in search and Saved items

1 Nov 08, 2021
A collection of benchmarking tools.

Benchmark Utilities About A collection of benchmarking tools. PYPI Package Table of Contents Using the library Installing and using the library Manual

Kostas Georgiou 2 Jan 28, 2022
A twitter bot that simply replies with a beautiful screenshot of the tweet, powered by poet.so

Poet this! Replies with a beautiful screenshot of the tweet, powered by poet.so Installation git clone https://github.com/dhravya/poet-this.git cd po

Dhravya Shah 30 Dec 04, 2022
:game_die: Pytest plugin to randomly order tests and control random.seed

pytest-randomly Pytest plugin to randomly order tests and control random.seed. Features All of these features are on by default but can be disabled wi

pytest-dev 471 Dec 30, 2022
CNE-OVS-SIT - OVS System Integration Test Suite

CNE-OVS-SIT - OVS System Integration Test Suite Introduction User guide Discussion Introduction CNE-OVS-SIT is a test suite for OVS end-to-end functio

4 Jan 09, 2022
nose is nicer testing for python

On some platforms, brp-compress zips man pages without distutils knowing about it. This results in an error when building an rpm for nose. The rpm bui

1.4k Dec 12, 2022
Data App Performance Tests

Data App Performance Tests My hypothesis is that The different architectures of

Marc Skov Madsen 6 Dec 14, 2022
Just a small test with lists in cython

Test for lists in cython Algorithm create a list of 10^4 lists each with 10^4 floats values (namely: 0.1) - 2 nested for iterate each list and compute

Federico Simonetta 32 Jul 23, 2022
A web scraping using Selenium Webdriver

Savee - Images Downloader Project using Selenium Webdriver to download images from someone's profile on https:www.savee.it website. Usage The project

Caio Eduardo Lobo 1 Dec 17, 2021
A modern API testing tool for web applications built with Open API and GraphQL specifications.

Schemathesis Schemathesis is a modern API testing tool for web applications built with Open API and GraphQL specifications. It reads the application s

Schemathesis.io 1.6k Dec 30, 2022
UUM Merit Form Filler is a web automation which helps automate entering a matric number to the UUM system in order for participants to obtain a merit

About UUM Merit Form Filler UUM Merit Form Filler is a web automation which helps automate entering a matric number to the UUM system in order for par

Ilham Rachmat 3 May 31, 2022
Automated Penetration Testing Framework

Automated Penetration Testing Framework

OWASP 2.1k Jan 01, 2023
Object factory for Django

Model Bakery: Smart fixtures for better tests Model Bakery offers you a smart way to create fixtures for testing in Django. With a simple and powerful

Model Bakers 632 Jan 08, 2023