Skip to content

Releases: Solganis/assertpy2

2.11.0

27 Jun 12:42
810e445

Choose a tag to compare

Added

  • Pandas / polars / numpy data-frame and array assertions.
    Fluent equality for pandas/polars DataFrame/Series (is_frame_equal) and numpy arrays (is_array_equal, is_array_close_to), delegating comparison semantics to each library's own assert_frame_equal / assert_allclose and carrying its diff on failure. Optional extra: pip install assertpy2[pandas] (or [polars], [numpy], [data]).

    # Before 2.11.0:  AttributeError: assertpy has no assertion <is_frame_equal()>
    # Now:
    assert_that(df).is_frame_equal(expected, check_dtype=False)
    assert_that(arr).is_array_close_to(expected, rtol=1e-3)

Improved

  • Richer dict diffs.
    A failing is_equal_to() on a dict now decomposes nested dataclasses, models, namedtuples and nested lists to the exact differing path (matching the detail already shown for top-level values), and dicts with mixed-type keys no longer raise.

    @dataclass
    class Point:
        x: int
        y: int
    
    assert_that({"point": Point(1, 2)}).is_equal_to({"point": Point(1, 3)})
    
    # Before 2.11.0 - the nested object was one leaf:
    #   point:
    #     - Point(x=1, y=2)
    #     + Point(x=1, y=3)
    #
    # Now - decomposed to the exact path:
    #   point.y:
    #     - 2
    #     + 3

Fixed

  • Clear error when comparing array/frame-likes.
    is_equal_to() / is_not_equal_to() on a numpy array or pandas/polars frame now raise a clear, actionable TypeError instead of the library's cryptic "ambiguous truth value".

    # assert_that(df).is_equal_to(other)
    #
    # Before 2.11.0:
    #   ValueError: The truth value of a DataFrame is ambiguous. Use a.empty, a.bool(), ...
    #
    # Now:
    #   TypeError: is_equal_to() cannot directly compare <DataFrame>: its '==' is element-wise
    #   and has no single truth value. Compare the value's own equality (e.g.
    #   assert_that(actual.equals(expected)).is_true()), assert on extracted scalars
    #   (columns, shape, length), or use satisfies(...) with an explicit predicate.

Internal

  • Restructured the README integrations section (compact, linked) and added a data-frame row to the comparison table.
  • Bumped dev type-checker ty to 0.0.55; renamed a snapshot test off a dev-phase name.

2.10.0

26 Jun 13:40
d7211c9

Choose a tag to compare

Added

  • Pydantic v2 models in structural matching.
    matches_structure(), satisfies(match.structure(...)), each(...), and the == form now accept a Pydantic model directly (via model_dump()) and report a path-level diff. Previously a model raised TypeError: val must be a dict - you had to call .model_dump() yourself.

    class User(BaseModel):
        name: str
        role: str
    
    user = User(name="Alice", role="superadmin")
    
    # Before 2.10.0:  assert_that(user.model_dump()).matches_structure({...})   # TypeError otherwise
    # Now:
    assert_that(user).matches_structure({"role": match.is_in("admin", "user")})
    # diff (match):
    #   role: expected a value in <('admin', 'user')>, but was 'superadmin'
  • Pydantic v2 models in extracting().
    Pull attributes straight off model instances. Previously a list of models raised TypeError: item <User> does not have [] accessor (models are iterable but not subscriptable).

    users = [User(name="Alice", role="admin"), User(name="Bob", role="editor")]
    
    assert_that(users).extracting("name").contains("Alice", "Bob")
    assert_that(users).extracting("name", "role").is_equal_to(
        [("Alice", "admin"), ("Bob", "editor")]
    )

Improved

  • Richer nested diffs.
    Nested sequences and dataclass fields are now decomposed to the exact differing path, matching the detail already shown at the top level.

    @dataclass
    class Matrix:
        rows: list[list[int]]
    
    assert_that(Matrix([[1, 2], [3, 4]])).is_equal_to(Matrix([[1, 2], [3, 9]]))
    
    # Before 2.10.0 - the whole nested list was one leaf:
    #   .rows:
    #     - [[1, 2], [3, 4]]
    #     + [[1, 2], [3, 9]]
    #
    # Now - decomposed to the exact index:
    #   .rows[1][1]:
    #     - 4
    #     + 9

Internal

  • Mutation-testing gaps closed.
    Hardened the rich-diff ordering guards and file.is_named.
  • Tooling and docs.
    Refreshed diff screenshots and docs; bumped dev type-checker ty to 0.0.54.

v2.9.1

25 Jun 14:39
05b07d8

Choose a tag to compare

Fixed

match.structure() no longer reports a false circular reference when the same nested instance is reused under sibling keys.

When a spec or value shared one sub-object instance across two keys (a DAG, not a cycle), matches() - and with it satisfies(match.structure(...)), each(...), and the == form - failed incorrectly. matches_structure() was unaffected. The matcher now scopes its visited-set per path, so shared sub-objects match while genuine cycles are still detected.

Internal

The structure matcher's two parallel traversals were merged into one, with no behavior change beyond the fix.
Plus documentation and test-suite housekeeping. No public API changes.

v2.9.0

25 Jun 09:33
c832c81

Choose a tag to compare

Added

is_equal_to(..., ignore=) and include= now accept set and frozenset.
Selective field comparison previously required a list or tuple of keys; sets now work too:

assert_that(actual).is_equal_to(expected, ignore={"created_at", "id"})

Date assertions accept datetime subclasses.
is_before, is_after, is_equal_to_ignoring_*, and is_close_to now treat instances of datetime subclasses (e.g. third-party datetime libraries and test fakes) as valid datetimes instead of rejecting them on an exact-type check.

Fixed

is_subset_of() against a single-key superset dict raised KeyError instead of a clean assertion.
A value mismatch against a one-entry mapping crashed while formatting the failure message; it now reports the mismatch normally.

is_divisible_by() matcher rejects a zero divisor with a clear ValueError instead of failing with ZeroDivisionError at match time.

Parallel-safe snapshots. Snapshot writes are serialized with a file lock and the snapshot directory is created race-free, so parallel test runs no longer collide on snapshot files.

eventually() awaits awaitables returned by synchronous callables, so a plain function that returns a coroutine is handled correctly.

Plus smaller correctness fixes: is_child_of path-boundary check, is_between range-type error message, length matchers on non-Sized values, structural-match headline paths, the allure diff-entry cap, single-item contains diffs, and several failure-message wording fixes.

Internal

Test-suite hardening driven by mutation testing (cosmic-ray) closed real gaps across the date, collection, matchers, bytes, dict, numeric, and string assertions. A weekly mutation-testing workflow and a typed-overload cross-check (ty + mypy --strict + pyright over assert_that) were added to CI. Plus shared-helper refactors (dict-like checks, datetime formatting, collection guards) and dependency bumps. No public API changes beyond the above.

v2.8.1

22 Jun 03:54
89e763e

Choose a tag to compare

Fixed

starts_with() and ends_with() now accept generators. Calling either on a generator or any other non-Sized iterable previously raised TypeError from an internal len() check. They now consume the iterable correctly, matching the documented "string or iterable" contract:

assert_that(x for x in [1, 2, 3]).starts_with(1)  # previously raised TypeError

Internal

Type-checker alignment with no public API or behavior change: assert_that's overload implementation is annotated against the shared base protocol (clearing the overload-consistency diagnostics), structure-matcher dict parameters are now parameterized, and the value matchers return an explicit bool. The comparison docs were rebalanced - table emphasis and trimmed slogans.

v2.8.0

20 Jun 22:35
4f7e932

Choose a tag to compare

What's new

Path-level diffs for matcher assertions. When matches_structure(), satisfies(), or each() fail, the pytest plugin now renders a structured match diff pointing at the exact path of every failing field and the predicate that failed - not just the first mismatch:

diff (match):
  user.name: expected a non-empty string, but was ''
  user.role: expected a value in <('admin', 'user')>, but was 'superadmin'
  user.age: expected a value between <18> and <120>, but was 15

Previously these assertions raised a plain AssertionError with no structured diff. They now attach structured failure data (.actual / .expected / .diff with kind="match"), so the breakdown also flows into Allure attachments.

Docs

Failure output is now shown throughout the docs - landing page, README, comparison, matchers, errors, and getting started - including a side-by-side "when it fails" comparison against plain pytest and dirty-equals.

Compatibility

Backward compatible: failure messages are unchanged, AssertionFailure stays an AssertionError subclass, no API changes. Python 3.10+.

v2.7.0

20 Jun 19:52
9ba2a7e

Choose a tag to compare

New

  • returned() pivots a callable assertion onto the value the call returned. Use it after warns(), does_not_warn(), or does_not_raise() to assert on the return value in the same chain:
    assert_that(make_client).warns(DeprecationWarning).when_called_with().returned().is_instance_of(Client). It raises TypeError if the call raised (no return value to inspect).

Improved

  • when_called_with() is now typed to return a string assertion, so chaining .matches() / .starts_with() on a captured exception or warning message type-checks (it already worked at runtime).
  • Corrected the internal builder() type stub (expected is type[BaseException] | None).
  • Added Hypothesis property-based tests (dev-only) covering equality, ignore/include (incl. nested paths and dataclasses/namedtuples), collection multiset/ordering semantics, and matcher algebra.

v2.6.0

20 Jun 18:25
361b338

Choose a tag to compare

New

  • warns() / does_not_warn() for callables: assert that calling a function emits (or does not emit) a warning, mirroring raises() / does_not_raise(). On success the matched warning message becomes the new value, so you can chain assertions on it, e.g. assert_that(func).warns(DeprecationWarning).when_called_with(x).matches("since 2.6").
  • The expected category defaults to Warning (matches any warning) and matches subclasses. Unlike pytest.warns, DeprecationWarning / PendingDeprecationWarning are captured by default.

Notes

  • warns() / does_not_warn() are safe within a single thread (including asyncio tasks on one event loop), but not across OS threads - the same limitation as pytest.warns.

v2.5.1

19 Jun 17:04
8308a96

Choose a tag to compare

Packaging

  • typing_extensions is now installed only on Python 3.10. assertpy2 has no runtime dependencies on Python 3.11+.

Fixed

  • assertpy2.__version__ now reports the installed version (it was stale at 2.4.0).

Documentation

  • New documentation site: https://solganis.github.io/assertpy2/ - hand-written guides for assertions, matchers, the fluent API, testing, errors, extending and integrations, plus dedicated comparison and migration pages.

v2.5.0

18 Jun 17:54
99c1067

Choose a tag to compare

New

  • Recursive comparison for models: is_equal_to(ignore=..., include=...) now works with dataclasses, namedtuples, attrs classes, Pydantic models, and plain objects
  • Rich pytest diffs extended to model comparisons with field-level mismatch reporting

Improved

  • README restructured as a landing page; comparison table and migration guide moved to api.md
  • attrs tests extracted to separate file with pytest.importorskip (no longer a dev dependency)
  • Modernized idioms across codebase: is Falsenot, set([...]){...}, ABCMetaabc.ABC
  • Renamed single-letter test variables to descriptive names
  • Removed Python 2 legacy: unicode = str alias, commented-out 0L literals
  • Dogfooded remaining bare assert to assert_that() in recursive compare tests
  • Fixed typos in api.md (dyanmism, occured)
  • CI: OS matrix, concurrency groups, strict coverage threshold