pax_global_header00006660000000000000000000000064150175657160014527gustar00rootroot0000000000000052 comment=9552c8ad921190821cf45aff8bcfdb31343b1a01 pytest-unordered-0.7.0/000077500000000000000000000000001501756571600150505ustar00rootroot00000000000000pytest-unordered-0.7.0/.coveragerc000066400000000000000000000001051501756571600171650ustar00rootroot00000000000000[report] exclude_lines = pragma: no cover if TYPE_CHECKING: pytest-unordered-0.7.0/.github/000077500000000000000000000000001501756571600164105ustar00rootroot00000000000000pytest-unordered-0.7.0/.github/workflows/000077500000000000000000000000001501756571600204455ustar00rootroot00000000000000pytest-unordered-0.7.0/.github/workflows/publish.yml000066400000000000000000000012711501756571600226370ustar00rootroot00000000000000name: Publish on PyPI on: push: tags: - 'v*' jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.x' - name: Install dependencies run: | python -m pip install --upgrade pip pip install --upgrade setuptools wheel python setup.py sdist bdist_wheel - name: pypi-publish uses: pypa/gh-action-pypi-publish@v1.3.1 with: user: __token__ password: ${{ secrets.PYPI_TOKEN }} - name: Upload packages uses: actions/upload-artifact@v4 with: name: dist path: dist pytest-unordered-0.7.0/.github/workflows/test.yml000066400000000000000000000033721501756571600221540ustar00rootroot00000000000000name: Test on: - push - pull_request jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: '3.x' - name: Install pre-commit run: python -m pip install pre-commit - name: Run pre-commit run: pre-commit run --all-files test: strategy: fail-fast: false matrix: python-version: - "3.13" - "3.12" - "3.11" - "3.10" - "3.9" - "3.8" - "pypy3.8" - "pypy3.9" - "pypy3.10" - "pypy3.11" pytest-version: - "pytest<8" - "pytest<9" - "pytest" - "git+https://github.com/pytest-dev/pytest.git@main" exclude: - python-version: "3.8" pytest-version: "git+https://github.com/pytest-dev/pytest.git@main" - python-version: "pypy3.8" pytest-version: "git+https://github.com/pytest-dev/pytest.git@main" runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 2 - name: Set up Python uses: actions/setup-python@v5 with: python-version: "${{ matrix.python-version }}" - name: Install dependencies run: python -m pip install tox "${{ matrix.pytest-version }}" pytest-cov . - name: Test run: | coverage run --branch --source=pytest_unordered -m pytest tests/ coverage xml -o ./coverage.xml - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 with: fail_ci_if_error: false files: ./coverage.xml token: ${{ secrets.CODECOV_TOKEN }} verbose: true pytest-unordered-0.7.0/.gitignore000066400000000000000000000022741501756571600170450ustar00rootroot00000000000000.idea/* # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ pytest-unordered-0.7.0/.pre-commit-config.yaml000066400000000000000000000005361501756571600213350ustar00rootroot00000000000000repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.11.6 hooks: - id: ruff-format args: [., --check] - id: ruff-format args: [.] - id: ruff args: [., --fix, --exit-non-zero-on-fix, --show-fixes] - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.15.0 hooks: - id: mypy pytest-unordered-0.7.0/CHANGELOG.md000066400000000000000000000020571501756571600166650ustar00rootroot00000000000000# Changelog ## [0.7.0] - 2025-06-03 - Add Python 3.13 support - Add deep unordered for nested data structures (#17) - Switch to ruff for linting and formatting ## [0.6.1] - 2024-07-05 - Fix matching with `mock.ANY` (#16) ## [0.6.0] - 2024-03-13 - Add Pytest 8 support - Add Python 3.12 support - Drop Pytest 6 support - Drop Python 3.7 support ## [0.5.2] - 2022-11-28 - Reorder items on __eq__ for better diff ## [0.5.1] - 2022-07-08 - Convert to a package - Add `py.typed` so type checkers can use the type hints ## [0.5.0] - 2022-07-07 - Drop Python 3.6 support - Improve type hints ## [0.4.1] - 2021-03-28 - Add `check_type` argument to make it possible to disable type checking for single non-generators positional argument ## [0.4.0] - 2020-11-02 - Add sequence type check when using `unordered` with single argument ## [0.3.0] - 2020-10-20 - Allow passing a generator to unordered() ## [0.2.0] - 2020-08-10 - Better diffs both in command line and in IDE ## [0.1.1] - 2019-11-27 - Better type checking ## [0.1.0] - 2019-11-26 - First version pytest-unordered-0.7.0/LICENSE000066400000000000000000000020541501756571600160560ustar00rootroot00000000000000MIT License Copyright (c) 2019 Ivan Zaikin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. pytest-unordered-0.7.0/README.md000066400000000000000000000117471501756571600163410ustar00rootroot00000000000000# pytest-unordered: Test collection content, ignoring order [![Build Status](https://github.com/utapyngo/pytest-unordered/actions/workflows/test.yml/badge.svg?branch=master)](https://github.com/utapyngo/pytest-unordered/actions/workflows/test.yml?query=branch%3Amaster) [![Coverage Status](https://codecov.io/gh/utapyngo/pytest-unordered/branch/master/graph/badge.svg)](https://codecov.io/gh/utapyngo/pytest-unordered) ![Language](https://img.shields.io/github/languages/top/utapyngo/pytest-unordered) [![Python Compatibility](https://img.shields.io/pypi/pyversions/pytest-unordered)](https://pypi.python.org/pypi/pytest-unordered) [![PyPI](https://img.shields.io/pypi/v/pytest-unordered?color=rgb%2852%2C%20208%2C%2088%29)](https://pypi.org/project/pytest-unordered/) `pytest_unordered` allows you to write simple (pytest) assertions to test whether collections have the same content, regardless of order. For example: assert [1, 20, 300] == unordered([20, 300, 1]) It is especially useful when testing APIs that return some complex data structures in an arbitrary order, e.g.: assert response.json() == { "people": unordered( # Here we test that the collection type is list [ { "name": "Alice", "age": 20, "children": unordered( # Here the collection type is not important {"name": "Bob", "age": 2}, {"name": "Carol", "age": 3}, ), }, { "name": "Dave", "age": 30, "children": unordered( {"name": "Eve", "age": 5}, {"name": "Frank", "age": 6}, ), }, ] ), } ## Installation pip install pytest-unordered ## Usage ### Basics In most cases you just need the `unordered()` helper function: from pytest_unordered import unordered Compare list or tuples by wrapping your expected value with `unordered()`: assert [1, 20, 300] == unordered([20, 300, 1]) # Pass assert (1, 20, 300) == unordered((20, 300, 1)) # Pass Excessive/missing items will be reported by pytest: assert [1, 20, 300] == unordered([20, 300, 1, 300]) E Extra items in the right sequence: E 300 By default, the container type has to match too: assert (1, 20, 300) == unordered([20, 300, 1]) E Type mismatch: E != ### Nesting A seasoned developer will notice that the simple use cases above can also be addressed with appropriate usage of builtins like `set()`, `sorted()`, `isinstance()`, `repr()`, etc, but these solutions scale badly (in terms of boilerplate code) with the complexity of your data structures. For example: naively implementing order ignoring comparison with `set()` or `sorted()` does not work with lists of dictionaries because dictionaries are not hashable or sortable. `unordered()` supports this out of the box however: assert [{"bb": 20}, {"a": 1}] == unordered([{"a": 1}, {"bb": 20}]) # Pass The true value of `unordered()` lies in the fact that you can apply it inside large nested data structures to skip order checking only in desired places with surgical precision and without a lot of boilerplate code. For example: expected = unordered([ {"customer": "Alice", "orders": unordered([123, 456])}, {"customer": "Bob", "orders": [789, 1000]}, ]) actual = [ {"customer": "Bob", "orders": [789, 1000]}, {"customer": "Alice", "orders": [456, 123]}, ] assert actual == expected In this example we wrapped the outer customer list and the order list of Alice with `unordered()`, but didn't wrap Bob's order list. With the `actual` value of above (where customer order is different and Alice's orders are reversed), the assertion will pass. But if the orders of Bob would be swapped in `actual`, the assertion will fail and pytest will report: E Differing items: E {'orders': [1000, 789]} != {'orders': [789, 1000]} ### Container type checking As noted, the container types should be (by default) equal to pass the assertion. If you don't want this type check, call `unordered()` in a variable argument fashion (instead of passing a container as single argument): assert [1, 20, 300] == unordered(20, 300, 1) # Pass assert (1, 20, 300) == unordered(20, 300, 1) # Pass This pattern also allows comparing with iterators, generators and alike: assert iter([1, 20, 300]) == unordered(20, 300, 1) # Pass assert unordered(i for i in range(3)) == [2, 1, 0] # Pass If you want to enforce type checking when passing a single generator expression, pass `check_type=True`: assert unordered((i for i in range(3)), check_type=True) == [2, 1, 0] # Fail assert unordered((i for i in range(3)), check_type=True) == (i for i in range(2, -1, -1)) # Pass pytest-unordered-0.7.0/pyproject.toml000066400000000000000000000016401501756571600177650ustar00rootroot00000000000000[tool.ruff] line-length = 100 target-version = "py38" exclude =[ "__pycache__", ".git", ".venv*/*", "venv*/*", "*/site-packages/*", ] [tool.ruff.format] indent-style = "space" [tool.ruff.lint] select = ["ALL"] ignore = [ "ANN401", # Dynamically typed expressions (typing.Any) are disallowed "COM812", # missing-trailing-comma: May conflict with the formatter "D", # pydocstyle "Q", # flake8-quotes (may conflict with the formatter) "PLR2004", # Magic value used in comparison, consider replacing with a constant variable "S101", # Use of `assert` detected "SIM201", # negate-equal-op "SIM202", # negate-not-equal-op "UP038", # non-pep604-isinstance: This rule is deprecated ] [tool.ruff.lint.isort] force-single-line = true [tool.mypy] disallow_untyped_defs = true exclude = [ '\.eggs', '\.git', '\.pytest_cache', '\.tox', 'build', 'dist', 'venv', ] pytest-unordered-0.7.0/pytest_unordered/000077500000000000000000000000001501756571600204475ustar00rootroot00000000000000pytest-unordered-0.7.0/pytest_unordered/__init__.py000066400000000000000000000116351501756571600225660ustar00rootroot00000000000000from __future__ import annotations from collections.abc import Generator from collections.abc import Iterable from collections.abc import Mapping from typing import TYPE_CHECKING from typing import Any import pytest from _pytest._io.saferepr import saferepr from _pytest.assertion.util import _compare_eq_any if TYPE_CHECKING: from _pytest.config import Config class UnorderedList(list): def __init__(self, expected: Iterable, *, check_type: bool = True) -> None: if not isinstance(expected, Iterable): msg = f"cannot make unordered comparisons to non-iterable: {expected!r}" raise TypeError( msg, ) if isinstance(expected, Mapping): msg = f"cannot make unordered comparisons to mapping: {expected!r}" raise TypeError(msg) super().__init__(expected) self.expected_type = type(expected) if check_type else None def __eq__(self, actual: object) -> bool: if self.expected_type is not None and self.expected_type is not type(actual): return False if not isinstance(actual, Iterable): return self.copy() == actual actual_list = list(actual) if len(actual_list) != len(self): return False extra_left, extra_right = self.compare_to(actual_list) return not extra_left and not extra_right def __ne__(self, actual: object) -> bool: return not (self == actual) def compare_to(self, other: list) -> tuple[list, list]: extra_left = list(self) extra_right: list[Any] = [] reordered: list[Any] = [] placeholder = object() for elem in other: if elem in extra_left: i = extra_left.index(elem) reordered.append(extra_left.pop(i)) else: extra_right.append(elem) reordered.append(placeholder) placeholder_fillers = extra_left.copy() for i, elem in reversed(list(enumerate(reordered))): if not placeholder_fillers: break if elem == placeholder: reordered[i] = placeholder_fillers.pop() self[:] = [e for e in reordered if e is not placeholder] return extra_left, extra_right def unordered(*args: Any, check_type: bool | None = None) -> UnorderedList: if len(args) == 1: if check_type is None: check_type = not isinstance(args[0], Generator) return UnorderedList(args[0], check_type=check_type) return UnorderedList(args, check_type=False) def unordered_deep(obj: Any) -> Any: if isinstance(obj, dict): return {k: unordered_deep(v) for k, v in obj.items()} if isinstance(obj, (list, tuple)): return unordered(unordered_deep(x) for x in obj) return obj def _compare_eq_unordered(left: Iterable, right: Iterable) -> tuple[list, list]: extra_left: list[Any] = [] extra_right = list(right) for elem in left: if elem in extra_right: extra_right.remove(elem) else: extra_left.append(elem) return extra_left, extra_right def pytest_assertrepr_compare( config: Config, op: str, left: Any, right: Any, ) -> list[str] | None: if (isinstance(left, UnorderedList) or isinstance(right, UnorderedList)) and op == "==": verbose = config.getoption("verbose") left_repr = saferepr(left) right_repr = saferepr(right) result = [f"{left_repr} {op} {right_repr}"] left_type = left.expected_type if isinstance(left, UnorderedList) else type(left) right_type = right.expected_type if isinstance(right, UnorderedList) else type(right) if left_type and right_type and left_type != right_type: result.append("Type mismatch:") result.append(f"{left_type} != {right_type}") extra_left, extra_right = _compare_eq_unordered(left, right) if len(extra_left) == 1 and len(extra_right) == 1: result.append("One item replaced:") if pytest.version_tuple < (8, 0, 0): # pragma: no cover result.extend( _compare_eq_any(extra_left[0], extra_right[0], verbose=verbose), # type: ignore[call-arg] ) else: result.extend( _compare_eq_any( extra_left[0], extra_right[0], highlighter=config.get_terminal_writer()._highlight, # noqa: SLF001 verbose=verbose, ), ) else: if extra_left: result.append("Extra items in the left sequence:") result.extend(saferepr(item) for item in extra_left) if extra_right: result.append("Extra items in the right sequence:") result.extend(saferepr(item) for item in extra_right) return result return None pytest-unordered-0.7.0/pytest_unordered/py.typed000066400000000000000000000000001501756571600221340ustar00rootroot00000000000000pytest-unordered-0.7.0/setup.py000077500000000000000000000033541501756571600165720ustar00rootroot00000000000000#!/usr/bin/env python from pathlib import Path from setuptools import setup def read(fname: str) -> str: return (Path(__file__).parent / fname).read_text(encoding="utf-8") setup( name="pytest-unordered", version="0.7.0", author="Ivan Zaikin", author_email="ut@pyngo.tom.ru", maintainer="Ivan Zaikin", maintainer_email="ut@pyngo.tom.ru", license="MIT", url="https://github.com/utapyngo/pytest-unordered", description="Test equality of unordered collections in pytest", long_description=read("README.md"), long_description_content_type="text/markdown", packages=["pytest_unordered"], package_data={"pytest_unordered": ["py.typed"]}, install_requires=["pytest>=7.0.0"], classifiers=[ "Development Status :: 4 - Beta", "Framework :: Pytest", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Testing", "Topic :: Software Development :: Quality Assurance", "Topic :: Utilities", "Typing :: Typed", ], entry_points={"pytest11": ["unordered = pytest_unordered"]}, ) pytest-unordered-0.7.0/tests/000077500000000000000000000000001501756571600162125ustar00rootroot00000000000000pytest-unordered-0.7.0/tests/__init__.py000066400000000000000000000000001501756571600203110ustar00rootroot00000000000000pytest-unordered-0.7.0/tests/conftest.py000066400000000000000000000000341501756571600204060ustar00rootroot00000000000000pytest_plugins = "pytester" pytest-unordered-0.7.0/tests/test_unordered.py000066400000000000000000000210661501756571600216170ustar00rootroot00000000000000from __future__ import annotations import collections from typing import TYPE_CHECKING from typing import Any from unittest.mock import ANY import pytest from pytest_unordered import UnorderedList from pytest_unordered import _compare_eq_unordered from pytest_unordered import unordered from pytest_unordered import unordered_deep if TYPE_CHECKING: from collections.abc import Iterable from collections.abc import Mapping from _pytest.pytester import Pytester @pytest.mark.parametrize( ("expected", "actual"), [ (unordered(1, 2, 3), [3, 2, 1]), (unordered(1, 2, 3), (3, 2, 1)), (unordered(1, 2, 3), {3, 2, 1}), (unordered([1, 2, 3]), [3, 2, 1]), (unordered((1, 2, 3)), (3, 2, 1)), (unordered({1, 2, 3}), {3, 2, 1}), (unordered(1, 2, {"a": unordered(4, 5, 6)}), [{"a": [6, 5, 4]}, 2, 1]), (unordered([{1: unordered(["a", "b"])}, 2, 3]), [3, 2, {1: ["b", "a"]}]), (unordered(x for x in range(3)), [2, 1, 0]), (unordered(x for x in range(3)), (2, 1, 0)), (unordered(x for x in range(3)), {2, 1, 0}), (unordered(x for x in range(3)), range(3)), (unordered("abc"), "bac"), (unordered("a", "b", "c"), ["b", "a", "c"]), (unordered("a", "b", "c"), "bac"), ], ) def test_unordered(expected: UnorderedList, actual: Iterable) -> None: assert expected == actual assert actual == expected assert not (expected != actual) assert not (actual != expected) @pytest.mark.parametrize( ("left", "right"), [ (unordered(2, 1, 0), (x for x in range(3))), ((x for x in range(3)), unordered(2, 1, 0)), (unordered(x for x in range(3)), (2, 1, 0)), ((2, 1, 0), unordered(x for x in range(3))), (unordered("a", "b", "c"), (x for x in "bac")), ((x for x in "bac"), unordered("a", "b", "c")), ], ) def test_unordered_generators(left: Iterable, right: Iterable) -> None: # Because general generators can only be consumed once, # we can only do one assert assert left == right @pytest.mark.parametrize( ("expected", "actual"), [ (unordered([1, 2, 3]), [1, 2, 3, 4]), (unordered([1, 2, 3]), [1, 2, 3, 1]), (unordered([1, 2, 3]), (1, 2, 3)), (unordered([1, 2, 3]), {1, 2, 3}), (unordered([1, 2, 3]), (x + 1 for x in range(3))), (unordered((1, 2, 3)), (1, 2, 3, 4)), (unordered((1, 2, 3)), (1, 2, 3, 1)), (unordered((1, 2, 3)), [1, 2, 3]), (unordered((1, 2, 3)), {1, 2, 3}), (unordered((1, 2, 3)), (x + 1 for x in range(3))), (unordered({1, 2, 3}), {1, 2, 3, 4}), (unordered({1, 2, 3}), [1, 2, 3]), (unordered({1, 2, 3}), (1, 2, 3)), (unordered({1, 2, 3}), (x + 1 for x in range(3))), (unordered("abc"), ["b", "a", "c"]), (unordered("abc"), ("b", "a", "c")), (unordered("abc"), {"b", "a", "c"}), ], ) def test_unordered_reject(expected: UnorderedList, actual: Iterable) -> None: assert expected != actual assert actual != expected assert not (expected == actual) assert not (actual == expected) @pytest.mark.parametrize("value", [None, True, 42, object(), type, TypeError]) def test_non_sized_expected(value: Any) -> None: with pytest.raises(TypeError, match="cannot make unordered comparisons to non-iterable"): UnorderedList(value) @pytest.mark.parametrize("value", [None, True, 42, object(), type, TypeError]) def test_non_iterable_actual(value: Any) -> None: assert not (unordered(1, 2, 3) == value) assert not (value == unordered(1, 2, 3)) @pytest.mark.parametrize( "value", [ {1: 2, 3: 4}, collections.defaultdict(int, a=5), collections.OrderedDict({1: 2, 3: 4}), collections.Counter("count this"), ], ) def test_mapping_expected(value: Mapping) -> None: with pytest.raises(TypeError, match="cannot make unordered comparisons to mapping"): unordered(value) @pytest.mark.parametrize("value", [None, type, TypeError]) def test_compare_to_non_sequence(value: Any) -> None: assert not unordered("x") == value assert unordered("x") != value def test_check_type() -> None: assert not unordered([1]) == {1} assert not unordered([1], check_type=True) == {1} assert unordered([1], check_type=False) == {1} @pytest.mark.parametrize( ("left", "right", "extra_left", "extra_right"), [ ([1, 2, 3], [1, 2, 3, 4, 5], [], [4, 5]), ([3, 2, 1], [1, 2, 3, 4, 5], [], [4, 5]), ([3, 2, {1: ["a", "b"]}], [{1: ["a", "b"]}, 2, 3, 4, 5], [], [4, 5]), ([3, 2, {1: ["a", "b"]}], [{1: unordered("b", "a")}, 2, 3, 4, 5], [], [4, 5]), ], ) def test_compare_eq_unordered( left: Iterable, right: Iterable, extra_left: list, extra_right: list, ) -> None: assert _compare_eq_unordered(left, right) == (extra_left, extra_right) def test_len() -> None: assert len(unordered({1: ["a", "b"]}, 2, 3, 4, 5)) == 5 def test_fail_nonunique_left(pytester: Pytester) -> None: pytester.makepyfile( """ from pytest_unordered import unordered def test_unordered(): assert unordered(1, 2, 3, 3) == [1, 2, 3] """, ) result = pytester.runpytest() result.assert_outcomes(failed=1, passed=0) result.stdout.fnmatch_lines( [ "E Extra items in the left sequence:", "E 3", ], ) def test_fail_nonunique_right(pytester: Pytester) -> None: pytester.makepyfile( """ from pytest_unordered import unordered def test_unordered(): assert [1, 2, 3] == unordered(1, 2, 3, 3) """, ) result = pytester.runpytest() result.assert_outcomes(failed=1, passed=0) result.stdout.fnmatch_lines( [ "E Extra items in the right sequence:", "E 3", ], ) def test_replace(pytester: Pytester) -> None: pytester.makepyfile( """ from pytest_unordered import unordered def test_unordered(): assert [{"a": 1, "b": 2}, 2, 3] == unordered(2, 3, {"b": 2, "a": 3}) """, ) result = pytester.runpytest() result.assert_outcomes(failed=1, passed=0) result.stdout.fnmatch_lines( [ "E One item replaced:", "E Omitting 1 identical items, use -vv to show", "E Differing items:", "E {'a': 1} != {'a': 3}", ], ) def test_in(pytester: Pytester) -> None: pytester.makepyfile( """ from pytest_unordered import unordered def test_unordered(): assert 1 in unordered(2, 3) """, ) result = pytester.runpytest() result.assert_outcomes(failed=1, passed=0) result.stdout.fnmatch_lines( [ "E assert 1 in [2, 3]", "E + where [2, 3] = unordered(2, 3)", ], ) def test_type_check(pytester: Pytester) -> None: pytester.makepyfile( """ from pytest_unordered import unordered def test_unordered(): assert [3, 2, 1] == unordered((1, 2, 3)) """, ) result = pytester.runpytest() result.assert_outcomes(failed=1, passed=0) result.stdout.fnmatch_lines( [ "E Type mismatch:", "E != ", ], ) def test_reorder_on_eq() -> None: unordered_list = unordered([1, 2, 3]) assert unordered_list == [3, 1, 2] assert list(unordered_list) == [3, 1, 2] def test_mock_any() -> None: p_unordered = {"results": unordered({"foo1": ANY}, {"foo2": ANY})} test_1 = {"results": [{"foo1": "value10"}, {"foo2": "value20"}]} test_2 = {"results": [{"foo1": "value11"}, {"foo2": "value21"}]} test_3 = {"results": [{"foo1": "value10"}, {"foo2": "value20"}]} assert p_unordered == test_1 assert p_unordered == test_2 assert p_unordered == test_3 @pytest.mark.parametrize( ("expected", "actual"), [ (unordered_deep([1, 2, 3]), [3, 2, 1]), (unordered_deep((1, 2, 3)), (3, 2, 1)), (unordered_deep({1, 2, 3}), {3, 2, 1}), (unordered_deep([1, 2, {"a": (4, 5, 6)}]), [{"a": [6, 5, 4]}, 2, 1]), # fmt: skip (unordered_deep([{1: (["a", "b"])}, 2, 3]), [3, 2, {1: ["b", "a"]}]), # fmt: skip (unordered_deep(("a", "b", "c")), ["b", "a", "c"]), ], ) def test_unordered_deep(expected: UnorderedList, actual: Iterable) -> None: assert expected == actual assert actual == expected assert not (expected != actual) assert not (actual != expected) pytest-unordered-0.7.0/tox.ini000066400000000000000000000006641501756571600163710ustar00rootroot00000000000000[tox] envlist = py38-{pytest7,pytest8}, {py39,py310,py311,py312,py313,pypy3}-{pytest7,pytest8,pytestlatest}, pre-commit [testenv] commands = coverage run --branch --source=pytest_unordered -m pytest tests/ {posargs} deps = coverage codecov pytest7: pytest>=7.4.4,<8 pytest8: pytest>=8.1.1,<9 pytestlatest: pytest [testenv:pre-commit] skip_install = true deps = pre-commit commands = pre-commit run --all-files