Giter Site home page Giter Site logo

samuelcolvin / dirty-equals Goto Github PK

View Code? Open in Web Editor NEW
789.0 12.0 33.0 1.23 MB

Doing dirty (but extremely useful) things with equals.

Home Page: https://dirty-equals.helpmanual.io

License: MIT License

Makefile 1.25% Python 98.75%
python pytest unit-testing testing-tools

dirty-equals's Introduction

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.8+.

dirty-equals's People

Contributors

alexmojaki avatar barnabywalters avatar browniebroke avatar cclauss avatar cinarizasyon avatar edwardbetts avatar evstratbg avatar fbruzzesi avatar hyzyla avatar john-g-g avatar marco-kaulea avatar meggycal avatar mgorny avatar mishaga avatar osintalex avatar samuelcolvin avatar silas avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

dirty-equals's Issues

dirty isinstance

I find myself using this library more and more and I love it.
However most of the time I prefer checking instances using isinstance built-in over ==.

This would be possible with the library types by hijacking __instancecheck__ method both in DirtyEqualsMeta and DirtyEquals to:

def __instancecheck__(self, other) -> bool:
    return self == other

If that is too dirty, I could suggest the implementation of an utility function:

def is_instance(value, cls_or_instance):
    return value == cls_or_instance

Happy to discuss and eventually work on it!

DeprecationWarning for datetime.utcfromtimestamp() in Python 3.12

python3.12 -m venv _e
. _e/bin/activate
pip install -e .[pydantic]
pip install -r requirements/tests.in
python -m pytest
==================================== ERRORS ====================================
___________________ ERROR collecting tests/test_datetime.py ____________________
_e/lib64/python3.12/site-packages/_pytest/runner.py:341: in from_call
    result: Optional[TResult] = func()
_e/lib64/python3.12/site-packages/_pytest/runner.py:372: in <lambda>
    call = CallInfo.from_call(lambda: list(collector.collect()), "collect")
_e/lib64/python3.12/site-packages/_pytest/python.py:531: in collect
    self._inject_setup_module_fixture()
_e/lib64/python3.12/site-packages/_pytest/python.py:545: in _inject_setup_module_fixture
    self.obj, ("setUpModule", "setup_module")
_e/lib64/python3.12/site-packages/_pytest/python.py:310: in obj
    self._obj = obj = self._getobj()
_e/lib64/python3.12/site-packages/_pytest/python.py:528: in _getobj
    return self._importtestmodule()
_e/lib64/python3.12/site-packages/_pytest/python.py:617: in _importtestmodule
    mod = import_path(self.path, mode=importmode, root=self.config.rootpath)
_e/lib64/python3.12/site-packages/_pytest/pathlib.py:565: in import_path
    importlib.import_module(module_name)
/usr/lib64/python3.12/importlib/__init__.py:90: in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
<frozen importlib._bootstrap>:1293: in _gcd_import
    ???
<frozen importlib._bootstrap>:1266: in _find_and_load
    ???
<frozen importlib._bootstrap>:1237: in _find_and_load_unlocked
    ???
<frozen importlib._bootstrap>:841: in _load_unlocked
    ???
_e/lib64/python3.12/site-packages/_pytest/assertion/rewrite.py:178: in exec_module
    exec(co, module.__dict__)
tests/test_datetime.py:5: in <module>
    import pytz
_e/lib64/python3.12/site-packages/pytz/__init__.py:20: in <module>
    from pytz.tzinfo import unpickler, BaseTzInfo
_e/lib64/python3.12/site-packages/pytz/tzinfo.py:27: in <module>
    _epoch = datetime.utcfromtimestamp(0)
E   DeprecationWarning: datetime.utcfromtimestamp() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.fromtimestamp(timestamp, datetime.UTC).

This will probably be fixed in pytz at some point; the issue is stub42/pytz#105.

In the longer term, it may be desirable to migrate away from pytz, as summarized in the introductory paragraph of the description for https://pypi.org/project/pytz-deprecation-shim/.

custom `pytest_assertrepr_compare`

Approach:

  • new method on some types, e.g. __inequality_details__(op) should return a list of strings
  • add a utility function to check for __inequality_details__ and call it if required,
  • instructions on calling that function from pytest_assertrepr_compare

rough prototype for Contains:

def pytest_assertrepr_compare(config, op, left, right):
    if isinstance(right, Contains):
        word = 'not' if op == '==' else 'unexpectedly'
        if len(right.contained_values) == 1:
            descr = f'Container: {right.contained_values[0]!r} {word} found in'
        else:
            descr = f'Container: {right.contained_values!r} all {word} found in'
        return [
            f'[dirty-equals] "{op}" failed',
            descr,
            *pformat(left).split('\n'),
        ]

IsUrl seems to be broken with pydantic-2

After upgrading to pydantic-2.0.2, I'm getting the following test failures:

$ python -m pytest -Wignore
========================================================= test session starts =========================================================
platform linux -- Python 3.11.4, pytest-7.4.0, pluggy-1.2.0
rootdir: /tmp/dirty-equals
configfile: pyproject.toml
testpaths: tests
plugins: examples-0.0.9
collected 599 items                                                                                                                   

tests/test_base.py .............................                                                                                [  4%]
tests/test_boolean.py ............................................                                                              [ 12%]
tests/test_datetime.py .................................................                                                        [ 20%]
tests/test_dict.py ..........................................................                                                   [ 30%]
tests/test_docs.py ...........F...............................................                                                  [ 39%]
tests/test_inspection.py ............................                                                                           [ 44%]
tests/test_list_tuple.py ..............................................................................                         [ 57%]
tests/test_numeric.py ...................................................................................                       [ 71%]
tests/test_other.py ...............................................................................FFF......................... [ 89%]
.....                                                                                                                           [ 90%]
tests/test_strings.py ...........................................................                                               [100%]

============================================================== FAILURES ===============================================================
___________________________________________ test_docstrings[dirty_equals/_other.py:197-206] ___________________________________________

example = CodeExample(source="from dirty_equals import IsUrl\n\nassert 'https://example.com' == IsUrl\nassert 'https://example.c...'ee58e62f-4c92-48e3-acd1-f765815e3bd5'), test_id='tests/test_docs.py::test_docstrings[dirty_equals/_other.py:197-206]')
eval_example = <pytest_examples.eval_example.EvalExample object at 0x7fa2a1104510>

    @pytest.mark.skipif(platform.python_implementation() == 'PyPy', reason='PyPy does not allow metaclass dunder methods')
    @pytest.mark.parametrize('example', find_examples('dirty_equals', 'docs'), ids=str)
    def test_docstrings(example: CodeExample, eval_example: EvalExample):
        prefix_settings = example.prefix_settings()
        # E711 and E712 refer to `== True` and `== None` and need to be ignored
        # I001 refers is a problem with black and ruff disagreeing about blank lines :shrug:
        eval_example.set_config(ruff_ignore=['E711', 'E712', 'I001'])
    
        if prefix_settings.get('lint') != 'skip':
            if eval_example.update_examples:
                eval_example.format(example)
            else:
                eval_example.lint(example)
    
        if prefix_settings.get('test') != 'skip':
            if eval_example.update_examples:
                eval_example.run_print_update(example)
            else:
>               eval_example.run_print_check(example)

tests/test_docs.py:25: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

>   assert 'https://example.com' == IsUrl
    assert 'https://example.com' == IsUrl(tld='com')
    assert 'https://example.com' == IsUrl(scheme='https')
    assert 'https://example.com' != IsUrl(scheme='http')
    assert 'postgres://user:pass@localhost:5432/app' == IsUrl(postgres_dsn=True)
    assert 'postgres://user:pass@localhost:5432/app' != IsUrl(http_url=True)
    ```
    """
E   AssertionError: assert 'https://example.com' == IsUrl

dirty_equals/_other.py:200: AssertionError
_____________________________________________ test_is_url_true[https://example.com-IsUrl] _____________________________________________

other = 'https://example.com', dirty = IsUrl

    @pytest.mark.parametrize(
        'other,dirty',
        [
            ('https://example.com', IsUrl),
            ('https://example.com', IsUrl(scheme='https')),
            ('postgres://user:pass@localhost:5432/app', IsUrl(postgres_dsn=True)),
        ],
    )
    def test_is_url_true(other, dirty):
>       assert other == dirty
E       AssertionError: assert 'https://example.com' == IsUrl

tests/test_other.py:275: AssertionError
____________________________________________ test_is_url_true[https://example.com-dirty1] _____________________________________________

other = 'https://example.com', dirty = IsUrl(<class 'pydantic_core._pydantic_core.Url'>)

    @pytest.mark.parametrize(
        'other,dirty',
        [
            ('https://example.com', IsUrl),
            ('https://example.com', IsUrl(scheme='https')),
            ('postgres://user:pass@localhost:5432/app', IsUrl(postgres_dsn=True)),
        ],
    )
    def test_is_url_true(other, dirty):
>       assert other == dirty
E       AssertionError: assert 'https://example.com' == IsUrl(<class 'pydantic_core._pydantic_core.Url'>)

tests/test_other.py:275: AssertionError
__________________________________ test_is_url_true[postgres://user:pass@localhost:5432/app-dirty2] ___________________________________

other = 'postgres://user:pass@localhost:5432/app'
dirty = IsUrl(typing.Annotated[pydantic_core._pydantic_core.MultiHostUrl, UrlConstraints(max_length=None, allowed_schemes=['po...+py-postgresql', 'postgresql+pygresql'], host_required=True, default_host=None, default_port=None, default_path=None)])

    @pytest.mark.parametrize(
        'other,dirty',
        [
            ('https://example.com', IsUrl),
            ('https://example.com', IsUrl(scheme='https')),
            ('postgres://user:pass@localhost:5432/app', IsUrl(postgres_dsn=True)),
        ],
    )
    def test_is_url_true(other, dirty):
>       assert other == dirty
E       AssertionError: assert 'postgres://user:pass@localhost:5432/app' == IsUrl(typing.Annotated[pydantic_core._pydantic_core.MultiHostUrl, UrlConstraints(max_length=None, allowed_schemes=['po...+py-postgresql', 'postgresql+pygresql'], host_required=True, default_host=None, default_port=None, default_path=None)])

tests/test_other.py:275: AssertionError
======================================================= short test summary info =======================================================
FAILED tests/test_docs.py::test_docstrings[dirty_equals/_other.py:197-206] - AssertionError: assert 'https://example.com' == IsUrl
FAILED tests/test_other.py::test_is_url_true[https://example.com-IsUrl] - AssertionError: assert 'https://example.com' == IsUrl
FAILED tests/test_other.py::test_is_url_true[https://example.com-dirty1] - AssertionError: assert 'https://example.com' == IsUrl(<class 'pydantic_core._pydantic_core.Url'>)
FAILED tests/test_other.py::test_is_url_true[postgres://user:pass@localhost:5432/app-dirty2] - AssertionError: assert 'postgres://user:pass@localhost:5432/app' == IsUrl(typing.Annotated[pydantic_core._pydantic_core.MultiHostU...
==================================================== 4 failed, 595 passed in 1.54s ====================================================

They pass after downgrading pydantic to <2.

Weird import conflict with xarray and typing-extensions on Python 3.8

One of our CI builds suddenly started failing with this stack trace:

tests/util/test_logging.py:1: in <module>
    import dirty_equals
venv38/lib64/python3.8/site-packages/dirty_equals/__init__.py:38: in <module>
    from ._sequence import Contains, HasLen, IsList, IsListOrTuple, IsTuple
venv38/lib64/python3.8/site-packages/dirty_equals/_sequence.py:215: in <module>
    class IsList(IsListOrTuple[List[Any]]):
/usr/lib64/python3.8/typing.py:258: in inner
    return cached(*args, **kwds)
/usr/lib64/python3.8/typing.py:895: in __class_getitem__
    return _GenericAlias(cls, params)
/usr/lib64/python3.8/typing.py:669: in __init__
    self.__parameters__ = _collect_type_vars(params)
venv38/lib64/python3.8/site-packages/typing_extensions.py:2994: in _collect_type_vars
    enforce_default_ordering = _has_generic_or_protocol_as_origin()
venv38/lib64/python3.8/site-packages/typing_extensions.py:2961: in _has_generic_or_protocol_as_origin
    return frame.f_locals.get("origin") in (
venv38/lib64/python3.8/site-packages/dirty_equals/_base.py:20: in __eq__
    return self() == other
venv38/lib64/python3.8/site-packages/dirty_equals/_sequence.py:181: in __init__
    length=_length_repr(self.length),
E   NameError: name '_length_repr' is not defined

After some rabbit hole crawling I could zoom in on a minimal reproduction procedure.
The problem started with the release of typing-extensions 4.12.1 (June 1st, 2024) and happens when xarray is imported before dirty_equals.

Example in a fresh docker run --rm -it python:3.8 /bin/bash container:

root@0a0f1606b929:/# pip install xarray dirty-equals typing-extensions
...
Successfully installed dirty-equals-0.7.1.post0 numpy-1.24.4 packaging-24.0 pandas-2.0.3 python-dateutil-2.9.0.post0 pytz-2024.1 six-1.16.0 typing-extensions-4.12.1 tzdata-2024.1 xarray-2023.1.0
...

root@0a0f1606b929:/# python
>>> import xarray
>>> import dirty_equals
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/local/lib/python3.8/site-packages/dirty_equals/__init__.py", line 38, in <module>
    from ._sequence import Contains, HasLen, IsList, IsListOrTuple, IsTuple
  File "/usr/local/lib/python3.8/site-packages/dirty_equals/_sequence.py", line 215, in <module>
    class IsList(IsListOrTuple[List[Any]]):
  File "/usr/local/lib/python3.8/typing.py", line 258, in inner
    return cached(*args, **kwds)
  File "/usr/local/lib/python3.8/typing.py", line 898, in __class_getitem__
    return _GenericAlias(cls, params)
  File "/usr/local/lib/python3.8/typing.py", line 672, in __init__
    self.__parameters__ = _collect_type_vars(params)
  File "/usr/local/lib/python3.8/site-packages/typing_extensions.py", line 2994, in _collect_type_vars
    enforce_default_ordering = _has_generic_or_protocol_as_origin()
  File "/usr/local/lib/python3.8/site-packages/typing_extensions.py", line 2961, in _has_generic_or_protocol_as_origin
    return frame.f_locals.get("origin") in (
  File "/usr/local/lib/python3.8/site-packages/dirty_equals/_base.py", line 20, in __eq__
    return self() == other
  File "/usr/local/lib/python3.8/site-packages/dirty_equals/_sequence.py", line 181, in __init__
    length=_length_repr(self.length),
NameError: name '_length_repr' is not defined

Possible workarounds:

  • pin down typing-extensions<4.12.1
  • use python 3.9 or higher
  • import dirty_equals before xarray

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.

nan type

Since float('nan') != float('nan') we need a custom FloatNan type.

Enforce clearer usage of `IsList()` etc. with no arguments

IsList() with no arguments is equivalent to an empty list, but the user probably just wants to check for a list with any number of items. It should throw an error suggesting that the user write IsList(length=0) or IsList(length=...) to prevent ambiguity. Similar for IsTuple(). Maybe even IsListOrTuple().

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,

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 ====================================================

`IsInstance` does not work for Pydantic BaseModels

It seems like IsInstance will never be equal to a pydantic model:

from dirty_equals import IsInstance
from pydantic import BaseModel

class A(BaseModel): pass

print(A() == IsInstance(A)) # False
print(A() == IsInstance[A]) # False

class B: pass

print(B() == IsInstance(B)) # True
print(B() == IsInstance[B]) # True

Interestingly enough it works perfectly with IsPartialDict:

from dirty_equals import IsPartialDict, IsNegativeInt
from pydantic import BaseModel

class C(BaseModel):
    a: int
    b: str

print(C(a=-1,b="a") ==  IsPartialDict(a=IsNegativeInt)) # True
print(C(a=-1,b="a") ==  IsPartialDict(a=IsNegativeInt, b=1)) # False

Maybe it's also nice to document that IsPartialDict works with pydantic models without having to call model_instance.dict()?

Tested with dirty-equals version 0.50 and pydantic versions 1.9.0 and 1.10.7.

unix datetime tests fail if TZ != UTC

The following tests fail if the system timezone is not UTC:

FAILED tests/test_datetime.py::test_is_datetime[unix-int] - assert 946684800 == IsDatetime(approx=datetime.datetime(2000, 1, 1, 0, 0...
FAILED tests/test_datetime.py::test_is_datetime[unix-float] - assert 946684800.123 == IsDatetime(approx=datetime.datetime(2000, 1, 1...
FAILED tests/test_docs.py::test_docs_examples[dirty_equals/_datetime.py:45-58] - assert 946684800.123 == IsDatetime(approx=datetime....

Full output:

============================================================== FAILURES ===============================================================
_____________________________________________________ test_is_datetime[unix-int] ______________________________________________________

value = 946684800, dirty = IsDatetime(approx=datetime.datetime(2000, 1, 1, 0, 0), unix_number=True), expect_match = True

    @pytest.mark.parametrize(
        'value,dirty,expect_match',
        [
            pytest.param(datetime(2000, 1, 1), IsDatetime(approx=datetime(2000, 1, 1)), True, id='same'),
            # Note: this requires the system timezone to be UTC
            pytest.param(946684800, IsDatetime(approx=datetime(2000, 1, 1), unix_number=True), True, id='unix-int'),
            # Note: this requires the system timezone to be UTC
            pytest.param(946684800.123, IsDatetime(approx=datetime(2000, 1, 1), unix_number=True), True, id='unix-float'),
            pytest.param(946684800, IsDatetime(approx=datetime(2000, 1, 1)), False, id='unix-different'),
            pytest.param(
                '2000-01-01T00:00', IsDatetime(approx=datetime(2000, 1, 1), iso_string=True), True, id='iso-string-true'
            ),
            pytest.param('2000-01-01T00:00', IsDatetime(approx=datetime(2000, 1, 1)), False, id='iso-string-different'),
            pytest.param('broken', IsDatetime(approx=datetime(2000, 1, 1)), False, id='iso-string-wrong'),
            pytest.param(
                '28/01/87', IsDatetime(approx=datetime(1987, 1, 28), format_string='%d/%m/%y'), True, id='string-format'
            ),
            pytest.param('28/01/87', IsDatetime(approx=datetime(2000, 1, 1)), False, id='string-format-different'),
            pytest.param('foobar', IsDatetime(approx=datetime(2000, 1, 1)), False, id='string-format-wrong'),
            pytest.param(datetime.now().isoformat(), IsNow(iso_string=True), True, id='isnow-str-true'),
            pytest.param(datetime(2000, 1, 1).isoformat(), IsNow(iso_string=True), False, id='isnow-str-different'),
            pytest.param([1, 2, 3], IsDatetime(approx=datetime(2000, 1, 1)), False, id='wrong-type'),
            pytest.param(
                datetime(2020, 1, 1, 12, 13, 14), IsDatetime(approx=datetime(2020, 1, 1, 12, 13, 14)), True, id='tz-same'
            ),
            pytest.param(
                datetime(2020, 1, 1, 12, 13, 14, tzinfo=timezone.utc),
                IsDatetime(approx=datetime(2020, 1, 1, 12, 13, 14), enforce_tz=False),
                True,
                id='tz-utc',
            ),
            pytest.param(
                datetime(2020, 1, 1, 12, 13, 14, tzinfo=timezone.utc),
                IsDatetime(approx=datetime(2020, 1, 1, 12, 13, 14)),
                False,
                id='tz-utc-different',
            ),
            pytest.param(
                datetime(2020, 1, 1, 12, 13, 14),
                IsDatetime(approx=datetime(2020, 1, 1, 12, 13, 14, tzinfo=timezone.utc), enforce_tz=False),
                False,
                id='tz-approx-tz',
            ),
            pytest.param(
                datetime(2020, 1, 1, 12, 13, 14, tzinfo=timezone(offset=timedelta(hours=1))),
                IsDatetime(approx=datetime(2020, 1, 1, 12, 13, 14), enforce_tz=False),
                True,
                id='tz-1-hour',
            ),
            pytest.param(
                pytz.timezone('Europe/London').localize(datetime(2022, 2, 15, 15, 15)),
                IsDatetime(
                    approx=pytz.timezone('America/New_York').localize(datetime(2022, 2, 15, 10, 15)), enforce_tz=False
                ),
                True,
                id='tz-both-tz',
            ),
            pytest.param(
                pytz.timezone('Europe/London').localize(datetime(2022, 2, 15, 15, 15)),
                IsDatetime(approx=pytz.timezone('America/New_York').localize(datetime(2022, 2, 15, 10, 15))),
                False,
                id='tz-both-tz-different',
            ),
            pytest.param(datetime(2000, 1, 1), IsDatetime(ge=datetime(2000, 1, 1)), True, id='ge'),
            pytest.param(datetime(1999, 1, 1), IsDatetime(ge=datetime(2000, 1, 1)), False, id='ge-not'),
            pytest.param(datetime(2000, 1, 2), IsDatetime(gt=datetime(2000, 1, 1)), True, id='gt'),
            pytest.param(datetime(2000, 1, 1), IsDatetime(gt=datetime(2000, 1, 1)), False, id='gt-not'),
        ],
    )
    def test_is_datetime(value, dirty, expect_match):
        if expect_match:
>           assert value == dirty
E           assert 946684800 == IsDatetime(approx=datetime.datetime(2000, 1, 1, 0, 0), unix_number=True)

tests/test_datetime.py:80: AssertionError
____________________________________________________ test_is_datetime[unix-float] _____________________________________________________

value = 946684800.123, dirty = IsDatetime(approx=datetime.datetime(2000, 1, 1, 0, 0), unix_number=True), expect_match = True

    @pytest.mark.parametrize(
        'value,dirty,expect_match',
        [
            pytest.param(datetime(2000, 1, 1), IsDatetime(approx=datetime(2000, 1, 1)), True, id='same'),
            # Note: this requires the system timezone to be UTC
            pytest.param(946684800, IsDatetime(approx=datetime(2000, 1, 1), unix_number=True), True, id='unix-int'),
            # Note: this requires the system timezone to be UTC
            pytest.param(946684800.123, IsDatetime(approx=datetime(2000, 1, 1), unix_number=True), True, id='unix-float'),
            pytest.param(946684800, IsDatetime(approx=datetime(2000, 1, 1)), False, id='unix-different'),
            pytest.param(
                '2000-01-01T00:00', IsDatetime(approx=datetime(2000, 1, 1), iso_string=True), True, id='iso-string-true'
            ),
            pytest.param('2000-01-01T00:00', IsDatetime(approx=datetime(2000, 1, 1)), False, id='iso-string-different'),
            pytest.param('broken', IsDatetime(approx=datetime(2000, 1, 1)), False, id='iso-string-wrong'),
            pytest.param(
                '28/01/87', IsDatetime(approx=datetime(1987, 1, 28), format_string='%d/%m/%y'), True, id='string-format'
            ),
            pytest.param('28/01/87', IsDatetime(approx=datetime(2000, 1, 1)), False, id='string-format-different'),
            pytest.param('foobar', IsDatetime(approx=datetime(2000, 1, 1)), False, id='string-format-wrong'),
            pytest.param(datetime.now().isoformat(), IsNow(iso_string=True), True, id='isnow-str-true'),
            pytest.param(datetime(2000, 1, 1).isoformat(), IsNow(iso_string=True), False, id='isnow-str-different'),
            pytest.param([1, 2, 3], IsDatetime(approx=datetime(2000, 1, 1)), False, id='wrong-type'),
            pytest.param(
                datetime(2020, 1, 1, 12, 13, 14), IsDatetime(approx=datetime(2020, 1, 1, 12, 13, 14)), True, id='tz-same'
            ),
            pytest.param(
                datetime(2020, 1, 1, 12, 13, 14, tzinfo=timezone.utc),
                IsDatetime(approx=datetime(2020, 1, 1, 12, 13, 14), enforce_tz=False),
                True,
                id='tz-utc',
            ),
            pytest.param(
                datetime(2020, 1, 1, 12, 13, 14, tzinfo=timezone.utc),
                IsDatetime(approx=datetime(2020, 1, 1, 12, 13, 14)),
                False,
                id='tz-utc-different',
            ),
            pytest.param(
                datetime(2020, 1, 1, 12, 13, 14),
                IsDatetime(approx=datetime(2020, 1, 1, 12, 13, 14, tzinfo=timezone.utc), enforce_tz=False),
                False,
                id='tz-approx-tz',
            ),
            pytest.param(
                datetime(2020, 1, 1, 12, 13, 14, tzinfo=timezone(offset=timedelta(hours=1))),
                IsDatetime(approx=datetime(2020, 1, 1, 12, 13, 14), enforce_tz=False),
                True,
                id='tz-1-hour',
            ),
            pytest.param(
                pytz.timezone('Europe/London').localize(datetime(2022, 2, 15, 15, 15)),
                IsDatetime(
                    approx=pytz.timezone('America/New_York').localize(datetime(2022, 2, 15, 10, 15)), enforce_tz=False
                ),
                True,
                id='tz-both-tz',
            ),
            pytest.param(
                pytz.timezone('Europe/London').localize(datetime(2022, 2, 15, 15, 15)),
                IsDatetime(approx=pytz.timezone('America/New_York').localize(datetime(2022, 2, 15, 10, 15))),
                False,
                id='tz-both-tz-different',
            ),
            pytest.param(datetime(2000, 1, 1), IsDatetime(ge=datetime(2000, 1, 1)), True, id='ge'),
            pytest.param(datetime(1999, 1, 1), IsDatetime(ge=datetime(2000, 1, 1)), False, id='ge-not'),
            pytest.param(datetime(2000, 1, 2), IsDatetime(gt=datetime(2000, 1, 1)), True, id='gt'),
            pytest.param(datetime(2000, 1, 1), IsDatetime(gt=datetime(2000, 1, 1)), False, id='gt-not'),
        ],
    )
    def test_is_datetime(value, dirty, expect_match):
        if expect_match:
>           assert value == dirty
E           assert 946684800.123 == IsDatetime(approx=datetime.datetime(2000, 1, 1, 0, 0), unix_number=True)

tests/test_datetime.py:80: AssertionError
_________________________________________ test_docs_examples[dirty_equals/_datetime.py:45-58] _________________________________________

module_name = '_datetime_45_58'
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\nfrom dirty_equals import Is...string=True)\n\nassert datetime(2000, 1, 2) == IsDatetime(gt=y2k)\nassert datetime(1999, 1, 2) != IsDatetime(gt=y2k)\n'
import_execute = <function import_execute.<locals>._import_execute at 0x7f3d4b0a4dc0>

    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/python3.10/site-packages/_pytest/assertion/rewrite.py:168: in exec_module
    exec(co, module.__dict__)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

    from dirty_equals import IsDatetime
    from datetime import datetime
    
    y2k = datetime(2000, 1, 1)
    assert datetime(2000, 1, 1) == IsDatetime(approx=y2k)
    # Note: this requires the system timezone to be UTC
    assert 946684800.123 == IsDatetime(approx=y2k, unix_number=True)
E   assert 946684800.123 == IsDatetime(approx=datetime.datetime(2000, 1, 1, 0, 0), unix_number=True)
E    +  where IsDatetime(approx=datetime.datetime(2000, 1, 1, 0, 0), unix_number=True) = IsDatetime(approx=datetime.datetime(2000, 1, 1, 0, 0), unix_number=True)

../pytest-of-mgorny/pytest-9/test_docs_examples_dirty_equal32/_datetime_45_58.py:52: AssertionError
======================================================= short test summary info =======================================================
FAILED tests/test_datetime.py::test_is_datetime[unix-int] - assert 946684800 == IsDatetime(approx=datetime.datetime(2000, 1, 1, 0, 0...
FAILED tests/test_datetime.py::test_is_datetime[unix-float] - assert 946684800.123 == IsDatetime(approx=datetime.datetime(2000, 1, 1...
FAILED tests/test_docs.py::test_docs_examples[dirty_equals/_datetime.py:45-58] - assert 946684800.123 == IsDatetime(approx=datetime....
==================================================== 3 failed, 481 passed in 1.09s ====================================================

`IsJson(*)` and `IsUUID(*)` reprs can't be evaluated

Why is repr(IsJson()) == 'IsJson(*)'? I think 'IsJson()' is a perfectly clear repr, and it follows the convention that eval(repr(x)) is equivalent to x. I also think seeing IsJson(*) in a diff would be confusing and distracting.

0.7.0: pytest test suite not ready for `pydantic` 2.x

Looks like current test suite is not redy for latest pydantic 2.4.2.

+ PYTHONPATH=/home/tkloczko/rpmbuild/BUILDROOT/python-dirty-equals-0.7.0-2.fc35.x86_64/usr/lib64/python3.8/site-packages:/home/tkloczko/rpmbuild/BUILDROOT/python-dirty-equals-0.7.0-2.fc35.x86_64/usr/lib/python3.8/site-packages
+ /usr/bin/pytest -ra -m 'not network' --ignore tests/test_docs.py
============================= test session starts ==============================
platform linux -- Python 3.8.18, pytest-7.4.2, pluggy-1.3.0
rootdir: /home/tkloczko/rpmbuild/BUILD/dirty-equals-0.7.0
configfile: pyproject.toml
testpaths: tests
collected 547 items

tests/test_base.py .............................                         [  5%]
tests/test_boolean.py ............................................       [ 13%]
tests/test_datetime.py ................................................. [ 22%]
                                                                         [ 22%]
tests/test_dict.py ..................................................... [ 31%]
.....                                                                    [ 32%]
tests/test_inspection.py ............................                    [ 38%]
tests/test_list_tuple.py ............................................... [ 46%]
...............................                                          [ 52%]
tests/test_numeric.py .................................................. [ 61%]
.................................                                        [ 67%]
tests/test_other.py .................................................... [ 76%]
...........................FFFFFFFF................................      [ 89%]
tests/test_strings.py .................................................. [ 98%]
.........                                                                [100%]

=================================== FAILURES ===================================
_________________ test_is_url_true[https://example.com-IsUrl] __________________

other = 'https://example.com', dirty = IsUrl

    @pytest.mark.parametrize(
        'other,dirty',
        [
            ('https://example.com', IsUrl),
            ('https://example.com', IsUrl(scheme='https')),
            ('postgres://user:pass@localhost:5432/app', IsUrl(postgres_dsn=True)),
        ],
    )
    def test_is_url_true(other, dirty):
>       assert other == dirty

tests/test_other.py:283:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
dirty_equals/_base.py:26: in __eq__
    return self() == other
dirty_equals/_base.py:104: in __eq__
    self._was_equal = self.equals(other)
dirty_equals/_other.py:264: in equals
    parsed = self.parse_obj_as(self.url_type, other)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

args = (<class 'pydantic_core._pydantic_core.Url'>, 'https://example.com')
kwargs = {}

    @functools.wraps(arg)
    def wrapper(*args, **kwargs):
>       warnings.warn(msg, category=category, stacklevel=stacklevel + 1)
E       pydantic.warnings.PydanticDeprecatedSince20: parse_obj_as is deprecated. Use pydantic.TypeAdapter.validate_python instead. Deprecated in Pydantic V2.0 to be removed in V3.0. See Pydantic V2 Migration Guide at https://errors.pydantic.dev/2.4/migration/

/usr/lib/python3.8/site-packages/typing_extensions.py:2359: PydanticDeprecatedSince20
_________________ test_is_url_true[https://example.com-dirty1] _________________

other = 'https://example.com'
dirty = IsUrl(<class 'pydantic_core._pydantic_core.Url'>)

    @pytest.mark.parametrize(
        'other,dirty',
        [
            ('https://example.com', IsUrl),
            ('https://example.com', IsUrl(scheme='https')),
            ('postgres://user:pass@localhost:5432/app', IsUrl(postgres_dsn=True)),
        ],
    )
    def test_is_url_true(other, dirty):
>       assert other == dirty

tests/test_other.py:283:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
dirty_equals/_base.py:104: in __eq__
    self._was_equal = self.equals(other)
dirty_equals/_other.py:264: in equals
    parsed = self.parse_obj_as(self.url_type, other)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

args = (<class 'pydantic_core._pydantic_core.Url'>, 'https://example.com')
kwargs = {}

    @functools.wraps(arg)
    def wrapper(*args, **kwargs):
>       warnings.warn(msg, category=category, stacklevel=stacklevel + 1)
E       pydantic.warnings.PydanticDeprecatedSince20: parse_obj_as is deprecated. Use pydantic.TypeAdapter.validate_python instead. Deprecated in Pydantic V2.0 to be removed in V3.0. See Pydantic V2 Migration Guide at https://errors.pydantic.dev/2.4/migration/

/usr/lib/python3.8/site-packages/typing_extensions.py:2359: PydanticDeprecatedSince20
_______ test_is_url_true[postgres://user:pass@localhost:5432/app-dirty2] _______

other = 'postgres://user:pass@localhost:5432/app'
dirty = IsUrl(typing_extensions.Annotated[pydantic_core._pydantic_core.MultiHostUrl, UrlConstraints(max_length=None, allowed_s...+py-postgresql', 'postgresql+pygresql'], host_required=True, default_host=None, default_port=None, default_path=None)])

    @pytest.mark.parametrize(
        'other,dirty',
        [
            ('https://example.com', IsUrl),
            ('https://example.com', IsUrl(scheme='https')),
            ('postgres://user:pass@localhost:5432/app', IsUrl(postgres_dsn=True)),
        ],
    )
    def test_is_url_true(other, dirty):
>       assert other == dirty

tests/test_other.py:283:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
dirty_equals/_base.py:104: in __eq__
    self._was_equal = self.equals(other)
dirty_equals/_other.py:264: in equals
    parsed = self.parse_obj_as(self.url_type, other)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

args = (typing_extensions.Annotated[pydantic_core._pydantic_core.MultiHostUrl, UrlConstraints(max_length=None, allowed_scheme...st_required=True, default_host=None, default_port=None, default_path=None)], 'postgres://user:pass@localhost:5432/app')
kwargs = {}

    @functools.wraps(arg)
    def wrapper(*args, **kwargs):
>       warnings.warn(msg, category=category, stacklevel=stacklevel + 1)
E       pydantic.warnings.PydanticDeprecatedSince20: parse_obj_as is deprecated. Use pydantic.TypeAdapter.validate_python instead. Deprecated in Pydantic V2.0 to be removed in V3.0. See Pydantic V2 Migration Guide at https://errors.pydantic.dev/2.4/migration/

/usr/lib/python3.8/site-packages/typing_extensions.py:2359: PydanticDeprecatedSince20
________________ test_is_url_false[https://example.com-dirty0] _________________

other = 'https://example.com'
dirty = IsUrl(typing_extensions.Annotated[pydantic_core._pydantic_core.MultiHostUrl, UrlConstraints(max_length=None, allowed_s...+py-postgresql', 'postgresql+pygresql'], host_required=True, default_host=None, default_port=None, default_path=None)])

    @pytest.mark.parametrize(
        'other,dirty',
        [
            ('https://example.com', IsUrl(postgres_dsn=True)),
            ('https://example.com', IsUrl(scheme='http')),
            ('definitely not a url', IsUrl),
            (42, IsUrl),
            ('https://anotherexample.com', IsUrl(postgres_dsn=True)),
        ],
    )
    def test_is_url_false(other, dirty):
>       assert other != dirty

tests/test_other.py:297:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
dirty_equals/_base.py:114: in __ne__
    return not self.equals(other)
dirty_equals/_other.py:264: in equals
    parsed = self.parse_obj_as(self.url_type, other)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

args = (typing_extensions.Annotated[pydantic_core._pydantic_core.MultiHostUrl, UrlConstraints(max_length=None, allowed_scheme...resql+pygresql'], host_required=True, default_host=None, default_port=None, default_path=None)], 'https://example.com')
kwargs = {}

    @functools.wraps(arg)
    def wrapper(*args, **kwargs):
>       warnings.warn(msg, category=category, stacklevel=stacklevel + 1)
E       pydantic.warnings.PydanticDeprecatedSince20: parse_obj_as is deprecated. Use pydantic.TypeAdapter.validate_python instead. Deprecated in Pydantic V2.0 to be removed in V3.0. See Pydantic V2 Migration Guide at https://errors.pydantic.dev/2.4/migration/

/usr/lib/python3.8/site-packages/typing_extensions.py:2359: PydanticDeprecatedSince20
________________ test_is_url_false[https://example.com-dirty1] _________________

other = 'https://example.com'
dirty = IsUrl(<class 'pydantic_core._pydantic_core.Url'>)

    @pytest.mark.parametrize(
        'other,dirty',
        [
            ('https://example.com', IsUrl(postgres_dsn=True)),
            ('https://example.com', IsUrl(scheme='http')),
            ('definitely not a url', IsUrl),
            (42, IsUrl),
            ('https://anotherexample.com', IsUrl(postgres_dsn=True)),
        ],
    )
    def test_is_url_false(other, dirty):
>       assert other != dirty

tests/test_other.py:297:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
dirty_equals/_base.py:114: in __ne__
    return not self.equals(other)
dirty_equals/_other.py:264: in equals
    parsed = self.parse_obj_as(self.url_type, other)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

args = (<class 'pydantic_core._pydantic_core.Url'>, 'https://example.com')
kwargs = {}

    @functools.wraps(arg)
    def wrapper(*args, **kwargs):
>       warnings.warn(msg, category=category, stacklevel=stacklevel + 1)
E       pydantic.warnings.PydanticDeprecatedSince20: parse_obj_as is deprecated. Use pydantic.TypeAdapter.validate_python instead. Deprecated in Pydantic V2.0 to be removed in V3.0. See Pydantic V2 Migration Guide at https://errors.pydantic.dev/2.4/migration/

/usr/lib/python3.8/site-packages/typing_extensions.py:2359: PydanticDeprecatedSince20
________________ test_is_url_false[definitely not a url-IsUrl] _________________

other = 'definitely not a url', dirty = IsUrl

    @pytest.mark.parametrize(
        'other,dirty',
        [
            ('https://example.com', IsUrl(postgres_dsn=True)),
            ('https://example.com', IsUrl(scheme='http')),
            ('definitely not a url', IsUrl),
            (42, IsUrl),
            ('https://anotherexample.com', IsUrl(postgres_dsn=True)),
        ],
    )
    def test_is_url_false(other, dirty):
>       assert other != dirty

tests/test_other.py:297:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
dirty_equals/_base.py:26: in __eq__
    return self() == other
dirty_equals/_base.py:104: in __eq__
    self._was_equal = self.equals(other)
dirty_equals/_other.py:264: in equals
    parsed = self.parse_obj_as(self.url_type, other)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

args = (<class 'pydantic_core._pydantic_core.Url'>, 'definitely not a url')
kwargs = {}

    @functools.wraps(arg)
    def wrapper(*args, **kwargs):
>       warnings.warn(msg, category=category, stacklevel=stacklevel + 1)
E       pydantic.warnings.PydanticDeprecatedSince20: parse_obj_as is deprecated. Use pydantic.TypeAdapter.validate_python instead. Deprecated in Pydantic V2.0 to be removed in V3.0. See Pydantic V2 Migration Guide at https://errors.pydantic.dev/2.4/migration/

/usr/lib/python3.8/site-packages/typing_extensions.py:2359: PydanticDeprecatedSince20
_________________________ test_is_url_false[42-IsUrl] __________________________

other = 42, dirty = IsUrl

    @pytest.mark.parametrize(
        'other,dirty',
        [
            ('https://example.com', IsUrl(postgres_dsn=True)),
            ('https://example.com', IsUrl(scheme='http')),
            ('definitely not a url', IsUrl),
            (42, IsUrl),
            ('https://anotherexample.com', IsUrl(postgres_dsn=True)),
        ],
    )
    def test_is_url_false(other, dirty):
>       assert other != dirty

tests/test_other.py:297:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
dirty_equals/_base.py:26: in __eq__
    return self() == other
dirty_equals/_base.py:104: in __eq__
    self._was_equal = self.equals(other)
dirty_equals/_other.py:264: in equals
    parsed = self.parse_obj_as(self.url_type, other)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

args = (<class 'pydantic_core._pydantic_core.Url'>, 42), kwargs = {}

    @functools.wraps(arg)
    def wrapper(*args, **kwargs):
>       warnings.warn(msg, category=category, stacklevel=stacklevel + 1)
E       pydantic.warnings.PydanticDeprecatedSince20: parse_obj_as is deprecated. Use pydantic.TypeAdapter.validate_python instead. Deprecated in Pydantic V2.0 to be removed in V3.0. See Pydantic V2 Migration Guide at https://errors.pydantic.dev/2.4/migration/

/usr/lib/python3.8/site-packages/typing_extensions.py:2359: PydanticDeprecatedSince20
_____________ test_is_url_false[https://anotherexample.com-dirty4] _____________

other = 'https://anotherexample.com'
dirty = IsUrl(typing_extensions.Annotated[pydantic_core._pydantic_core.MultiHostUrl, UrlConstraints(max_length=None, allowed_s...+py-postgresql', 'postgresql+pygresql'], host_required=True, default_host=None, default_port=None, default_path=None)])

    @pytest.mark.parametrize(
        'other,dirty',
        [
            ('https://example.com', IsUrl(postgres_dsn=True)),
            ('https://example.com', IsUrl(scheme='http')),
            ('definitely not a url', IsUrl),
            (42, IsUrl),
            ('https://anotherexample.com', IsUrl(postgres_dsn=True)),
        ],
    )
    def test_is_url_false(other, dirty):
>       assert other != dirty

tests/test_other.py:297:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
dirty_equals/_base.py:114: in __ne__
    return not self.equals(other)
dirty_equals/_other.py:264: in equals
    parsed = self.parse_obj_as(self.url_type, other)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

args = (typing_extensions.Annotated[pydantic_core._pydantic_core.MultiHostUrl, UrlConstraints(max_length=None, allowed_scheme...ygresql'], host_required=True, default_host=None, default_port=None, default_path=None)], 'https://anotherexample.com')
kwargs = {}

    @functools.wraps(arg)
    def wrapper(*args, **kwargs):
>       warnings.warn(msg, category=category, stacklevel=stacklevel + 1)
E       pydantic.warnings.PydanticDeprecatedSince20: parse_obj_as is deprecated. Use pydantic.TypeAdapter.validate_python instead. Deprecated in Pydantic V2.0 to be removed in V3.0. See Pydantic V2 Migration Guide at https://errors.pydantic.dev/2.4/migration/

/usr/lib/python3.8/site-packages/typing_extensions.py:2359: PydanticDeprecatedSince20
=========================== short test summary info ============================
FAILED tests/test_other.py::test_is_url_true[https://example.com-IsUrl] - pyd...
FAILED tests/test_other.py::test_is_url_true[https://example.com-dirty1] - py...
FAILED tests/test_other.py::test_is_url_true[postgres://user:pass@localhost:5432/app-dirty2]
FAILED tests/test_other.py::test_is_url_false[https://example.com-dirty0] - p...
FAILED tests/test_other.py::test_is_url_false[https://example.com-dirty1] - p...
FAILED tests/test_other.py::test_is_url_false[definitely not a url-IsUrl] - p...
FAILED tests/test_other.py::test_is_url_false[42-IsUrl] - pydantic.warnings.P...
FAILED tests/test_other.py::test_is_url_false[https://anotherexample.com-dirty4]
======================== 8 failed, 539 passed in 1.32s =========================

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 ๐Ÿ™‚

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)),
})

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

Makefile command for testing in local development

I think there should be Makefile command which runs tests with --update-examples and without coverage for speed, and this would be the recommended workflow when iterating and testing in local development.

In fact, I think that's what make test should be, and the current test command (i.e. with coverage and without updating examples) could be renamed to test:slow or ci or something to indicate that it's the slower version that you run less often to double check things.

Maintenance status of dirty_equals?

Hi,

We started depending on dirty_equals in a couple of test suites instead of further complicating a bunch of ad-hoc, homegrown hacks.
However I noticed there hasn't been activity on this repo since November last year, so I want to kindly check on the maintenance status of this project.
Just to be sure we're not betting on the wrong horse (including contributing back).

objects with multiline repr cause pytest to print a diff

from dirty_equals import IsList


def test():
    actions = [
        "ddsdsdads ",
        "sfafsdsd",
        "dfds sdfsef",
        "sfdssdf ",
        "sdsadfs",
        "dsfsdfsd",
        "cfdfdfd",
        "dgffgfdgfd",
        "fdsfsdgfsdgsf",
    ]
    assert (actions, 1, 2, 3, 4, 5, 6, 7, 8, 9) == (
        IsList(length=...),
        1,
        2,
        3,
        4,
        5,
        6,
        7,
        8,
        0,
    )

This prints:

nError: assert (['ddsdsdads ',\n  'sfafsdsd',\n  'dfds sdfsef',\n  'sfdssdf ',\n  'sdsadfs',\n  'dsfsdfsd',\n  'cfdfdfd',\n  'dgffgfdgfd',\n  'fdsfsdgfsdgsf'],\n 1,\n 2,\n 3,\n 4,\n 5,\n 6,\n 7,\n 8,\n 9) == (['ddsdsdads ', 'sfafsdsd', 'dfds sdfsef', 'sfdssdf ', 'sdsadfs', 'dsfsdfsd', 'cfdfdfd', 'dgffgfdgfd', 'fdsfsdgfsdgsf'],\n 1,\n 2,\n 3,\n 4,\n 5,\n 6,\n 7,\n 8,\n 0)
E         At index 9 diff: 9 != 0
E         Full diff:
E           (
E         -  ['ddsdsdads ', 'sfafsdsd', 'dfds sdfsef', 'sfdssdf ', 'sdsadfs', 'dsfsdfsd', 'cfdfdfd', 'dgffgfdgfd', 'fdsfsdgfsdgsf'],
E         +  ['ddsdsdads ',
E         +   'sfafsdsd',
E         +   'dfds sdfsef',
E         +   'sfdssdf ',
E         +   'sdsadfs',
E         +   'dsfsdfsd',
E         +   'cfdfdfd',
E         +   'dgffgfdgfd',
E         +   'fdsfsdgfsdgsf'],
E            1,
E            2,
E            3,
E            4,
E            5,
E            6,
E            7,
E            8,
E         -  0,
E         +  9,
E           )

The 0 vs 9 at the end is the only real diff. But I also get a lot of noise due to the long list before. The differences are just formatting, they are not real differences. They seem to stem from the fact that dirty_equals formats long lists differently from pytest (7.1.3).

If the list is short all is well, the problem only occurs when the list is long and pytest decides to format it with one element in each line.

I've been trying for a little bit to come up with a possible fix for this, to no avail. Does not seem to be a simple fix. I am not sure if it is possible to fix this. But it bothers me a lot and I am willing to try. Any advice is welcome.

`IsDataclass` type

I would be happy to work on IsDataclass (related to #3).

Before starting I have a few questions:

  • Should it be in the _other module?
  • Any drawback in using dataclasses.is_dataclass?
  • I think it would be good to have the option to distinguish between class definition and instance, but glad to get a feedback on this.

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

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.

Wrong version number in releases

The pyproject.toml file states:

version = "0"

for all current releases. This is going to break version requirements in other packages.

test_is_datetime fails on alpine linux edge x86_64

This test used to pass but fails now for some reason.

======================================= test session starts ========================================
platform linux -- Python 3.11.8, pytest-8.0.2, pluggy-1.4.0
rootdir: /home/ncopa/aports/community/py3-dirty-equals/src/dirty-equals-0.7.1
configfile: pyproject.toml
testpaths: tests
plugins: cov-4.1.0, xdist-3.5.0, requests-mock-1.11.0
collected 428 items                                                                                

tests/test_base.py .............................                                             [  6%]
tests/test_boolean.py ............................................                           [ 17%]
tests/test_datetime.py .FF..............................................                     [ 28%]
tests/test_dict.py ..........................................................                [ 42%]
tests/test_inspection.py ............................                                        [ 48%]
tests/test_list_tuple.py ................................................................... [ 64%]
...........                                                                                  [ 66%]
tests/test_numeric.py ...................................................................... [ 83%]
.............                                                                                [ 86%]
tests/test_strings.py ...........................................................            [100%]

============================================= FAILURES =============================================
____________________________________ test_is_datetime[unix-int] ____________________________________

value = 946684800, dirty = IsDatetime(approx=datetime.datetime(2000, 1, 1, 0, 0), unix_number=True)
expect_match = True

    @pytest.mark.parametrize(
        'value,dirty,expect_match',
        [
            pytest.param(datetime(2000, 1, 1), IsDatetime(approx=datetime(2000, 1, 1)), True, id='same'),
            # Note: this requires the system timezone to be UTC
            pytest.param(946684800, IsDatetime(approx=datetime(2000, 1, 1), unix_number=True), True, id='unix-int'),
            # Note: this requires the system timezone to be UTC
            pytest.param(946684800.123, IsDatetime(approx=datetime(2000, 1, 1), unix_number=True), True, id='unix-float'),
            pytest.param(946684800, IsDatetime(approx=datetime(2000, 1, 1)), False, id='unix-different'),
            pytest.param(
                '2000-01-01T00:00', IsDatetime(approx=datetime(2000, 1, 1), iso_string=True), True, id='iso-string-true'
            ),
            pytest.param('2000-01-01T00:00', IsDatetime(approx=datetime(2000, 1, 1)), False, id='iso-string-different'),
            pytest.param('broken', IsDatetime(approx=datetime(2000, 1, 1)), False, id='iso-string-wrong'),
            pytest.param(
                '28/01/87', IsDatetime(approx=datetime(1987, 1, 28), format_string='%d/%m/%y'), True, id='string-format'
            ),
            pytest.param('28/01/87', IsDatetime(approx=datetime(2000, 1, 1)), False, id='string-format-different'),
            pytest.param('foobar', IsDatetime(approx=datetime(2000, 1, 1)), False, id='string-format-wrong'),
            pytest.param(datetime(2000, 1, 1).isoformat(), IsNow(iso_string=True), False, id='isnow-str-different'),
            pytest.param([1, 2, 3], IsDatetime(approx=datetime(2000, 1, 1)), False, id='wrong-type'),
            pytest.param(
                datetime(2020, 1, 1, 12, 13, 14), IsDatetime(approx=datetime(2020, 1, 1, 12, 13, 14)), True, id='tz-same'
            ),
            pytest.param(
                datetime(2020, 1, 1, 12, 13, 14, tzinfo=timezone.utc),
                IsDatetime(approx=datetime(2020, 1, 1, 12, 13, 14), enforce_tz=False),
                True,
                id='tz-utc',
            ),
            pytest.param(
                datetime(2020, 1, 1, 12, 13, 14, tzinfo=timezone.utc),
                IsDatetime(approx=datetime(2020, 1, 1, 12, 13, 14)),
                False,
                id='tz-utc-different',
            ),
            pytest.param(
                datetime(2020, 1, 1, 12, 13, 14),
                IsDatetime(approx=datetime(2020, 1, 1, 12, 13, 14, tzinfo=timezone.utc), enforce_tz=False),
                False,
                id='tz-approx-tz',
            ),
            pytest.param(
                datetime(2020, 1, 1, 12, 13, 14, tzinfo=timezone(offset=timedelta(hours=1))),
                IsDatetime(approx=datetime(2020, 1, 1, 12, 13, 14), enforce_tz=False),
                True,
                id='tz-1-hour',
            ),
            pytest.param(
                pytz.timezone('Europe/London').localize(datetime(2022, 2, 15, 15, 15)),
                IsDatetime(
                    approx=pytz.timezone('America/New_York').localize(datetime(2022, 2, 15, 10, 15)), enforce_tz=False
                ),
                True,
                id='tz-both-tz',
            ),
            pytest.param(
                pytz.timezone('Europe/London').localize(datetime(2022, 2, 15, 15, 15)),
                IsDatetime(approx=pytz.timezone('America/New_York').localize(datetime(2022, 2, 15, 10, 15))),
                False,
                id='tz-both-tz-different',
            ),
            pytest.param(datetime(2000, 1, 1), IsDatetime(ge=datetime(2000, 1, 1)), True, id='ge'),
            pytest.param(datetime(1999, 1, 1), IsDatetime(ge=datetime(2000, 1, 1)), False, id='ge-not'),
            pytest.param(datetime(2000, 1, 2), IsDatetime(gt=datetime(2000, 1, 1)), True, id='gt'),
            pytest.param(datetime(2000, 1, 1), IsDatetime(gt=datetime(2000, 1, 1)), False, id='gt-not'),
        ],
    )
    def test_is_datetime(value, dirty, expect_match):
        if expect_match:
>           assert value == dirty
E           assert 946684800 == IsDatetime(approx=datetime.datetime(2000, 1, 1, 0, 0), unix_number=True)

tests/test_datetime.py:80: AssertionError
___________________________________ test_is_datetime[unix-float] ___________________________________

value = 946684800.123
dirty = IsDatetime(approx=datetime.datetime(2000, 1, 1, 0, 0), unix_number=True)
expect_match = True

    @pytest.mark.parametrize(
        'value,dirty,expect_match',
        [
            pytest.param(datetime(2000, 1, 1), IsDatetime(approx=datetime(2000, 1, 1)), True, id='same'),
            # Note: this requires the system timezone to be UTC
            pytest.param(946684800, IsDatetime(approx=datetime(2000, 1, 1), unix_number=True), True, id='unix-int'),
            # Note: this requires the system timezone to be UTC
            pytest.param(946684800.123, IsDatetime(approx=datetime(2000, 1, 1), unix_number=True), True, id='unix-float'),
            pytest.param(946684800, IsDatetime(approx=datetime(2000, 1, 1)), False, id='unix-different'),
            pytest.param(
                '2000-01-01T00:00', IsDatetime(approx=datetime(2000, 1, 1), iso_string=True), True, id='iso-string-true'
            ),
            pytest.param('2000-01-01T00:00', IsDatetime(approx=datetime(2000, 1, 1)), False, id='iso-string-different'),
            pytest.param('broken', IsDatetime(approx=datetime(2000, 1, 1)), False, id='iso-string-wrong'),
            pytest.param(
                '28/01/87', IsDatetime(approx=datetime(1987, 1, 28), format_string='%d/%m/%y'), True, id='string-format'
            ),
            pytest.param('28/01/87', IsDatetime(approx=datetime(2000, 1, 1)), False, id='string-format-different'),
            pytest.param('foobar', IsDatetime(approx=datetime(2000, 1, 1)), False, id='string-format-wrong'),
            pytest.param(datetime(2000, 1, 1).isoformat(), IsNow(iso_string=True), False, id='isnow-str-different'),
            pytest.param([1, 2, 3], IsDatetime(approx=datetime(2000, 1, 1)), False, id='wrong-type'),
            pytest.param(
                datetime(2020, 1, 1, 12, 13, 14), IsDatetime(approx=datetime(2020, 1, 1, 12, 13, 14)), True, id='tz-same'
            ),
            pytest.param(
                datetime(2020, 1, 1, 12, 13, 14, tzinfo=timezone.utc),
                IsDatetime(approx=datetime(2020, 1, 1, 12, 13, 14), enforce_tz=False),
                True,
                id='tz-utc',
            ),
            pytest.param(
                datetime(2020, 1, 1, 12, 13, 14, tzinfo=timezone.utc),
                IsDatetime(approx=datetime(2020, 1, 1, 12, 13, 14)),
                False,
                id='tz-utc-different',
            ),
            pytest.param(
                datetime(2020, 1, 1, 12, 13, 14),
                IsDatetime(approx=datetime(2020, 1, 1, 12, 13, 14, tzinfo=timezone.utc), enforce_tz=False),
                False,
                id='tz-approx-tz',
            ),
            pytest.param(
                datetime(2020, 1, 1, 12, 13, 14, tzinfo=timezone(offset=timedelta(hours=1))),
                IsDatetime(approx=datetime(2020, 1, 1, 12, 13, 14), enforce_tz=False),
                True,
                id='tz-1-hour',
            ),
            pytest.param(
                pytz.timezone('Europe/London').localize(datetime(2022, 2, 15, 15, 15)),
                IsDatetime(
                    approx=pytz.timezone('America/New_York').localize(datetime(2022, 2, 15, 10, 15)), enforce_tz=False
                ),
                True,
                id='tz-both-tz',
            ),
            pytest.param(
                pytz.timezone('Europe/London').localize(datetime(2022, 2, 15, 15, 15)),
                IsDatetime(approx=pytz.timezone('America/New_York').localize(datetime(2022, 2, 15, 10, 15))),
                False,
                id='tz-both-tz-different',
            ),
            pytest.param(datetime(2000, 1, 1), IsDatetime(ge=datetime(2000, 1, 1)), True, id='ge'),
            pytest.param(datetime(1999, 1, 1), IsDatetime(ge=datetime(2000, 1, 1)), False, id='ge-not'),
            pytest.param(datetime(2000, 1, 2), IsDatetime(gt=datetime(2000, 1, 1)), True, id='gt'),
            pytest.param(datetime(2000, 1, 1), IsDatetime(gt=datetime(2000, 1, 1)), False, id='gt-not'),
        ],
    )
    def test_is_datetime(value, dirty, expect_match):
        if expect_match:
>           assert value == dirty
E           assert 946684800.123 == IsDatetime(approx=datetime.datetime(2000, 1, 1, 0, 0), unix_number=True)

tests/test_datetime.py:80: AssertionError
===================================== short test summary info ======================================
FAILED tests/test_datetime.py::test_is_datetime[unix-int] - assert 946684800 == IsDatetime(approx=datetime.datetime(2000, 1, 1, 0, 0), unix_number=True)
FAILED tests/test_datetime.py::test_is_datetime[unix-float] - assert 946684800.123 == IsDatetime(approx=datetime.datetime(2000, 1, 1, 0, 0), unix_number=True)
================================== 2 failed, 426 passed in 0.21s ===================================

pytz version requirement

Is there a reason that dirty-equals requires a semver version of pytz or would you be happy to accept a PR to change the requirement to *?

I assume it was just added with poetry add or something.

More types

  • IsFalseLike #23
  • IsTrueLike #25
  • IsPartialDict #5
  • IsFullDict(order_matters=False) #5
  • IsIterable(*items, start=None, finish=None, order_matters=True, len=None) #7
  • IsList(*items, skip=DontSkip, order_matters=True, len=None) #7
  • IsTuple(*items, skip=DontSkip, order_matters=True, len=None) #7
  • IsPartialIterable(items: Dict[int, Any]) #7
  • IsStr(regex, min_length, max_length, upper, lower, digits)
  • IsBytes(regex, min_length, max_length, upper, lower, digits)
  • IsToday #20
  • HasProperties #26
  • HasName #26
  • HasRepr #26
  • HasLen #7
  • IsOneOf
  • EqualsFunc #4
  • IsJson #4
  • IsDataclass
  • IsEnumMember(v, strict=True)

RFE: use `zoneinfo` instead of `pytz`

Switch to standard zoneinfo module.
Below may help pydantic/pydantic-core@fd262933

[tkloczko@pers-jacek dirty-equals-0.7.1]$ grep -r pytz
dirty_equals/_datetime.py:    Instantiate a `ZoneInfo` object from a string, falling back to `pytz.timezone` when `ZoneInfo` is not available
dirty_equals/_datetime.py:            import pytz
dirty_equals/_datetime.py:            raise ImportError('`pytz` or `zoneinfo` required for tz handling') from e
dirty_equals/_datetime.py:            return pytz.timezone(tz)  # type: ignore[return-value]
dirty_equals/_datetime.py:                (or `pytz.timezone` on 3.8) to get a timezone,
requirements/linting.in:types-pytz
requirements/linting.txt:types-pytz==2023.3.1.1
requirements/tests.in:pytz
requirements/tests.txt:pytz==2023.3.post1
tests/test_datetime.py:import pytz
tests/test_datetime.py:            pytz.timezone('Europe/London').localize(datetime(2022, 2, 15, 15, 15)),
tests/test_datetime.py:                approx=pytz.timezone('America/New_York').localize(datetime(2022, 2, 15, 10, 15)), enforce_tz=False
tests/test_datetime.py:            pytz.timezone('Europe/London').localize(datetime(2022, 2, 15, 15, 15)),
tests/test_datetime.py:            IsDatetime(approx=pytz.timezone('America/New_York').localize(datetime(2022, 2, 15, 10, 15))),

Lacking a dirty way to count elements

I wonder if there's a dirty equals way to assert on element count?

I suppose it's possible to use IsListOrTuple with check_order=False. But then it's the precondition of having a list/tuple and explicitly multiply each element.

e.g.

from dirty_equals import IsListOrTuple
[3, 2, 1, 3, 2, 1] == IsListOrTuple(1, 1, 2, 2, 3, 3, check_order=False)

Another one is Contains, but as far as I can see it can't be upper bounded on "how many of something"?


Could this be some extension of Contains? e.g. under a parameter counts={<elem>: <count>}.

from dirty_equals import Contains
[3, 2, 1, 3, 2, 1] == Contains(counts={1: 2, 2: 2, 3: 2})

I suppose this might also imply some sort of partial={True|False} argument for Contains, for convenience?

Additionally, <element> of counts in a case like above could probably be useful as dirty equals types? e.g.

[("a", 1), ("b", 2), ("a", 1)] == Contains(counts={IsTuple(IsStr(), 1): 2, IsTuple(IsStr(), 2): 1})

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)

How to assert list of dict?

Hi ๐Ÿ‘‹๐Ÿป

I'm trying to validate the Pydantic error I get from fastAPI response. Below is my pytest fixture param:

[{'input': None,
  'loc': ['body', 'username'],
  'msg': 'Input should be a valid string'},
 {'input': None,
  'loc': ['body', 'email'],
  'msg': 'Input should be a valid string'},
 {'input': None,
  'loc': ['body', 'pwd'],
  'msg': 'Input should be a valid string'}]

I get error from API in this form:

[{'input': None,
  'loc': ['body', 'username'],
  'msg': 'Input should be a valid string',
  'type': 'string_type',
  'url': 'https://errors.pydantic.dev/2.3/v/string_type'},
 {'input': None,
  'loc': ['body', 'email'],
  'msg': 'Input should be a valid string',
  'type': 'string_type',
  'url': 'https://errors.pydantic.dev/2.3/v/string_type'},
 {'input': None,
  'loc': ['body', 'pwd'],
  'msg': 'Input should be a valid string',
  'type': 'string_type',
  'url': 'https://errors.pydantic.dev/2.3/v/string_type'}]

How can I assert these two?

# Doesn't work
assert res_err == IsList(
    [IsPartialDict(errDict) for errDict in fix_err],
    check_order=False,
)
Playground Script
from dirty_equals import IsList, IsPartialDict

fix_err = [
    {"input": None, "loc": ["body", "username"], "msg": "Input should be a valid string"},
    {"input": None, "loc": ["body", "email"], "msg": "Input should be a valid string"},
    {"input": None, "loc": ["body", "pwd"], "msg": "Input should be a valid string"},
]

res_err = [
    {
        "input": None,
        "loc": ["body", "username"],
        "msg": "Input should be a valid string",
        "type": "string_type",
        "url": "https://errors.pydantic.dev/2.3/v/string_type",
    },
    {
        "input": None,
        "loc": ["body", "email"],
        "msg": "Input should be a valid string",
        "type": "string_type",
        "url": "https://errors.pydantic.dev/2.3/v/string_type",
    },
    {
        "input": None,
        "loc": ["body", "pwd"],
        "msg": "Input should be a valid string",
        "type": "string_type",
        "url": "https://errors.pydantic.dev/2.3/v/string_type",
    },
]

assert res_err == IsList(
    [IsPartialDict(errDict) for errDict in fix_err],
    check_order=False,
)

A way to ensure that all elements in a sequence match constraint

I came across a situation when I need to assert that all of dicts in a list have a certain key/value pair, eg

my_obj = {
    "items": [{"x": 42, "type": 1}, {"x": 123, "type": 1}],
}
assert all(item["type"] == 1 for item in my_obj["items"])

Is there currently any way to achieve this? I could only come up with this

assert my_obj == IsPartialDict({"items": ~Contains(~IsPartialDict({"type": 1}))})

but it seems too dirty ๐Ÿฅฒ

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.

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.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.