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
CNE-OVS-SIT - OVS System Integration Test Suite

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

4 Jan 09, 2022
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
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
Subprocesses for Humans 2.0.

Delegator.py — Subprocesses for Humans 2.0 Delegator.py is a simple library for dealing with subprocesses, inspired by both envoy and pexpect (in fact

Amit Tripathi 1.6k Jan 04, 2023
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
Fully functioning price detector built with selenium and python

Fully functioning price detector built with selenium and python

mark sikaundi 4 Mar 30, 2022
Selects tests affected by changed files. Continous test runner when used with pytest-watch.

This is a pytest plug-in which automatically selects and re-executes only tests affected by recent changes. How is this possible in dynamic language l

Tibor Arpas 614 Dec 30, 2022
Percy visual testing for Python Selenium

percy-selenium-python Percy visual testing for Python Selenium. Installation npm install @percy/cli: $ npm install --save-dev @percy/cli pip install P

Percy 9 Mar 24, 2022
Um scraper feito em python que gera arquivos de excel baseados nas tier lists do site LoLalytics.

LoLalytics-scraper Um scraper feito em python que gera arquivos de excel baseados nas tier lists do site LoLalytics. Começando por um único script com

Kevin Souza 1 Feb 19, 2022
The definitive testing tool for Python. Born under the banner of Behavior Driven Development (BDD).

mamba: the definitive test runner for Python mamba is the definitive test runner for Python. Born under the banner of behavior-driven development. Ins

Néstor Salceda 502 Dec 30, 2022
Hypothesis is a powerful, flexible, and easy to use library for property-based testing.

Hypothesis Hypothesis is a family of testing libraries which let you write tests parametrized by a source of examples. A Hypothesis implementation the

Hypothesis 6.4k Jan 05, 2023
Ward is a modern test framework for Python with a focus on productivity and readability.

Ward is a modern test framework for Python with a focus on productivity and readability.

Darren Burns 1k Dec 31, 2022
Auto-hms-action - Automation of NU Health Management System

🦾 Automation of NU Health Management System 🤖 長崎大学 健康管理システムの自動化 🏯 Usage / 使い方

k5-mot 3 Mar 04, 2022
No longer maintained, please migrate to model_bakery

Model Mommy: Smart fixtures for better tests IMPORTANT: Model Mommy is no longer maintained and was replaced by Model Bakery. Please, consider migrati

Bernardo Fontes 917 Oct 04, 2022
Just for testing video streaming using pytgcalls.

tgvc-video-tests Just for testing video streaming using pytgcalls. Note: The features used in this repository is highly experimental and you might not

wrench 34 Dec 27, 2022
PyBuster A directory busting tool for web application penetration tester, written in python

PyBuster A directory busting tool for web application penetration tester, written in python. Supports custom wordlist,recursive search. Screenshots Pr

Anukul Pandey 4 Jan 30, 2022
Tools for test driven data-wrangling and data validation.

datatest: Test driven data-wrangling and data validation Datatest helps to speed up and formalize data-wrangling and data validation tasks. It impleme

269 Dec 16, 2022
This package is a python library with tools for the Molecular Simulation - Software Gromos.

This package is a python library with tools for the Molecular Simulation - Software Gromos. It allows you to easily set up, manage and analyze simulations in python.

14 Sep 28, 2022
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 modified env

Pytest plugin to fail a test if it leaves modified os.environ afterwards.

wemake.services 7 Sep 11, 2022