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
Selenium-python but lighter: Helium is the best Python library for web automation.

Selenium-python but lighter: Helium Selenium-python is great for web automation. Helium makes it easier to use. For example: Under the hood, Helium fo

Michael Herrmann 3.2k Dec 31, 2022
Divide full port scan results and use it for targeted Nmap runs

Divide Et Impera And Scan (and also merge the scan results) DivideAndScan is used to efficiently automate port scanning routine by splitting it into 3

snovvcrash 226 Dec 30, 2022
A test fixtures replacement for Python

factory_boy factory_boy is a fixtures replacement based on thoughtbot's factory_bot. As a fixtures replacement tool, it aims to replace static, hard t

FactoryBoy project 3k Jan 05, 2023
A pure Python script to easily get a reverse shell

easy-shell A pure Python script to easily get a reverse shell. How it works? After sending a request, it generates a payload with different commands a

Cristian Souza 48 Dec 12, 2022
A browser automation framework and ecosystem.

Selenium Selenium is an umbrella project encapsulating a variety of tools and libraries enabling web browser automation. Selenium specifically provide

Selenium 25.5k Jan 01, 2023
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
Pytest-rich - Pytest + rich integration (proof of concept)

pytest-rich Leverage rich for richer test session output. This plugin is not pub

Bruno Oliveira 170 Dec 02, 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
WEB PENETRATION TESTING TOOL 💥

N-WEB ADVANCE WEB PENETRATION TESTING TOOL Features 🎭 Admin Panel Finder Admin Scanner Dork Generator Advance Dork Finder Extract Links No Redirect H

56 Dec 23, 2022
自动化爬取并自动测试所有swagger-ui.html显示的接口

swagger-hack 在测试中偶尔会碰到swagger泄露 常见的泄露如图: 有的泄露接口特别多,每一个都手动去试根本试不过来 于是用python写了个脚本自动爬取所有接口,配置好传参发包访问 原理是首先抓取http://url/swagger-resources 获取到有哪些标准及对应的文档地

jayus 534 Dec 29, 2022
The source code and slide for my talk about the subject: unittesing in python

PyTest Talk This talk give you some ideals about the purpose of unittest? how to write good unittest? how to use pytest framework? and show you the ba

nguyenlm 3 Jan 18, 2022
Doing dirty (but extremely useful) things with equals.

Doing dirty (but extremely useful) things with equals. Documentation: dirty-equals.helpmanual.io Source Code: github.com/samuelcolvin/dirty-equals dir

Samuel Colvin 602 Jan 05, 2023
HTTP client mocking tool for Python - inspired by Fakeweb for Ruby

HTTPretty 1.0.5 HTTP Client mocking tool for Python created by Gabriel Falcão . It provides a full fake TCP socket module. Inspired by FakeWeb Github

Gabriel Falcão 2k Jan 06, 2023
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
Generate random test credit card numbers for testing, validation and/or verification purposes.

Generate random test credit card numbers for testing, validation and/or verification purposes.

Dark Hunter 141 5 Nov 14, 2022
Yet another python home automation project. Because a smart light is more than just on or off

Automate home Yet another home automation project because a smart light is more than just on or off. Overview When talking about home automation there

Maja Massarini 62 Oct 10, 2022
A Modular Penetration Testing Framework

fsociety A Modular Penetration Testing Framework Install pip install fsociety Update pip install --upgrade fsociety Usage usage: fsociety [-h] [-i] [-

fsociety-team 802 Dec 31, 2022
Voip Open Linear Testing Suite

VOLTS Voip Open Linear Tester Suite Functional tests for VoIP systems based on voip_patrol and docker 10'000 ft. view System is designed to run simple

Igor Olhovskiy 17 Dec 30, 2022
a socket mock framework - for all kinds of socket animals, web-clients included

mocket /mɔˈkɛt/ A socket mock framework for all kinds of socket animals, web-clients included - with gevent/asyncio/SSL support ...and then MicroPytho

Giorgio Salluzzo 249 Dec 14, 2022
The best, free, all in one, multichecking, pentesting utility

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

Mickey 58 Jan 03, 2023