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
Fills out the container extension form automatically. (Specific to IIT Ropar)

automated_container_extension Fills out the container extension form automatically. (Specific to IIT Ropar) Download the chrome driver from the websit

Abhishek Singh Sambyal 1 Dec 24, 2021
HTTP traffic mocking and testing made easy in Python

pook Versatile, expressive and hackable utility library for HTTP traffic mocking and expectations made easy in Python. Heavily inspired by gock. To ge

Tom 305 Dec 23, 2022
A configurable set of panels that display various debug information about the current request/response.

Django Debug Toolbar The Django Debug Toolbar is a configurable set of panels that display various debug information about the current request/respons

Jazzband 7.3k Jan 02, 2023
This is a Python script for Github Bot which uses Selenium to Automate things.

github-follow-unfollow-bot This is a Python script for Github Bot which uses Selenium to Automate things. Pre-requisites :- Python A Github Account Re

Chaudhary Hamdan 10 Jul 01, 2022
It's a simple script to generate a mush on code forces, the script will accept the public problem urls only or polygon problems.

Codeforces-Sheet-Generator It's a simple script to generate a mushup on code forces, the script will accept the public problem urls only or polygon pr

Ahmed Hossam 10 Aug 02, 2022
Switch among Guest VMs organized by Resource Pool

Proxmox PCI Switcher Switch among Guest VMs organized by Resource Pool. main features: ONE GPU card, N OS (at once) Guest VM command client Handler po

Rosiney Gomes Pereira 111 Dec 27, 2022
Automated mouse clicker script using PyAutoGUI and Typer.

clickpy Automated mouse clicker script using PyAutoGUI and Typer. This app will randomly click your mouse between 1 second and 3 minutes, to prevent y

Joe Fitzgibbons 0 Dec 01, 2021
Descriptor Vector Exchange

Descriptor Vector Exchange This repo provides code for learning dense landmarks without supervision. Our approach is described in the ICCV 2019 paper

James Thewlis 74 Nov 29, 2022
a plugin for py.test that changes the default look and feel of py.test (e.g. progressbar, show tests that fail instantly)

pytest-sugar pytest-sugar is a plugin for pytest that shows failures and errors instantly and shows a progress bar. Requirements You will need the fol

Teemu 963 Dec 28, 2022
Test python asyncio-based code with ease.

aiounittest Info The aiounittest is a helper library to ease of your pain (and boilerplate), when writing a test of the asynchronous code (asyncio). Y

Krzysztof Warunek 55 Oct 30, 2022
API Test Automation with Requests and Pytest

api-testing-requests-pytest Install Make sure you have Python 3 installed on your machine. Then: 1.Install pipenv sudo apt-get install pipenv 2.Go to

Sulaiman Haque 2 Nov 21, 2021
The best, free, all in one, multichecking, pentesting utility

The best, free, all in one, multichecking, pentesting utility

Mickey 58 Jan 03, 2023
An Instagram bot that can mass text users, receive and read a text, and store it somewhere with user details.

Instagram Bot 🤖 July 14, 2021 Overview 👍 A multifunctionality automated instagram bot that can mass text users, receive and read a message and store

Abhilash Datta 14 Dec 06, 2022
Pytest plugin for testing the idempotency of a function.

pytest-idempotent Pytest plugin for testing the idempotency of a function. Usage pip install pytest-idempotent Documentation Suppose we had the follo

Tyler Yep 3 Dec 14, 2022
Silky smooth profiling for Django

Silk Silk is a live profiling and inspection tool for the Django framework. Silk intercepts and stores HTTP requests and database queries before prese

Jazzband 3.7k Jan 04, 2023
This is a pytest plugin, that enables you to test your code that relies on a running MongoDB database

This is a pytest plugin, that enables you to test your code that relies on a running MongoDB database. It allows you to specify fixtures for MongoDB process and client.

Clearcode 19 Oct 21, 2022
Fi - A simple Python 3.9+ command-line application for managing Fidelity portfolios

fi fi is a simple Python 3.9+ command-line application for managing Fidelity por

Darik Harter 2 Feb 26, 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
hyppo is an open-source software package for multivariate hypothesis testing.

hyppo (HYPothesis Testing in PythOn, pronounced "Hippo") is an open-source software package for multivariate hypothesis testing.

neurodata 137 Dec 18, 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