assertpy2 2.8.0__tar.gz → 2.9.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. {assertpy2-2.8.0 → assertpy2-2.9.0}/.github/workflows/ci.yml +9 -3
  2. assertpy2-2.9.0/.github/workflows/mutation.yml +63 -0
  3. {assertpy2-2.8.0 → assertpy2-2.9.0}/PKG-INFO +18 -17
  4. {assertpy2-2.8.0 → assertpy2-2.9.0}/README.md +17 -16
  5. assertpy2-2.9.0/assertpy2/_introspection.py +66 -0
  6. {assertpy2-2.8.0 → assertpy2-2.9.0}/assertpy2/_mixin_base.py +12 -4
  7. {assertpy2-2.8.0 → assertpy2-2.9.0}/assertpy2/_typing.py +7 -2
  8. {assertpy2-2.8.0 → assertpy2-2.9.0}/assertpy2/assertpy.py +15 -31
  9. {assertpy2-2.8.0 → assertpy2-2.9.0}/assertpy2/async_assertions.py +4 -5
  10. {assertpy2-2.8.0 → assertpy2-2.9.0}/assertpy2/base.py +42 -49
  11. {assertpy2-2.8.0 → assertpy2-2.9.0}/assertpy2/behave_matchers.py +14 -9
  12. {assertpy2-2.8.0 → assertpy2-2.9.0}/assertpy2/collection.py +19 -21
  13. {assertpy2-2.8.0 → assertpy2-2.9.0}/assertpy2/contains.py +29 -29
  14. {assertpy2-2.8.0 → assertpy2-2.9.0}/assertpy2/date.py +24 -32
  15. {assertpy2-2.8.0 → assertpy2-2.9.0}/assertpy2/dict.py +14 -16
  16. {assertpy2-2.8.0 → assertpy2-2.9.0}/assertpy2/dynamic.py +7 -6
  17. {assertpy2-2.8.0 → assertpy2-2.9.0}/assertpy2/errors.py +3 -4
  18. {assertpy2-2.8.0 → assertpy2-2.9.0}/assertpy2/exception.py +4 -4
  19. {assertpy2-2.8.0 → assertpy2-2.9.0}/assertpy2/extracting.py +12 -10
  20. {assertpy2-2.8.0 → assertpy2-2.9.0}/assertpy2/file.py +10 -6
  21. {assertpy2-2.8.0 → assertpy2-2.9.0}/assertpy2/helpers.py +66 -52
  22. {assertpy2-2.8.0 → assertpy2-2.9.0}/assertpy2/matchers.py +50 -27
  23. {assertpy2-2.8.0 → assertpy2-2.9.0}/assertpy2/numeric.py +49 -61
  24. {assertpy2-2.8.0 → assertpy2-2.9.0}/assertpy2/pytest_plugin.py +27 -20
  25. assertpy2-2.9.0/assertpy2/snapshot.py +232 -0
  26. {assertpy2-2.8.0 → assertpy2-2.9.0}/assertpy2/string.py +35 -51
  27. {assertpy2-2.8.0 → assertpy2-2.9.0}/docs/assertions.md +19 -3
  28. assertpy2-2.9.0/docs/assets/diff-equal.png +0 -0
  29. assertpy2-2.9.0/docs/assets/diff-equal.svg +64 -0
  30. assertpy2-2.9.0/docs/assets/diff-match.svg +51 -0
  31. assertpy2-2.9.0/docs/assets/diff-sequence.svg +52 -0
  32. assertpy2-2.9.0/docs/assets/diff-set.svg +48 -0
  33. {assertpy2-2.8.0 → assertpy2-2.9.0}/docs/comparison.md +27 -21
  34. {assertpy2-2.8.0 → assertpy2-2.9.0}/docs/errors.md +21 -18
  35. {assertpy2-2.8.0 → assertpy2-2.9.0}/docs/extending.md +9 -0
  36. {assertpy2-2.8.0 → assertpy2-2.9.0}/docs/getting-started.md +5 -7
  37. {assertpy2-2.8.0 → assertpy2-2.9.0}/docs/index.md +13 -8
  38. {assertpy2-2.8.0 → assertpy2-2.9.0}/docs/matchers.md +15 -6
  39. {assertpy2-2.8.0 → assertpy2-2.9.0}/docs/migration.md +1 -1
  40. assertpy2-2.9.0/docs/type-safety.md +62 -0
  41. {assertpy2-2.8.0 → assertpy2-2.9.0}/mkdocs.yml +1 -0
  42. {assertpy2-2.8.0 → assertpy2-2.9.0}/pyproject.toml +14 -7
  43. {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_async.py +7 -0
  44. {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_collection.py +7 -0
  45. {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_core.py +6 -6
  46. {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_custom_dict.py +25 -25
  47. {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_datetime.py +78 -10
  48. {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_dict.py +1 -1
  49. {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_dict_compare.py +39 -5
  50. {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_expected_exception.py +4 -2
  51. {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_extracting.py +2 -1
  52. {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_file.py +9 -0
  53. {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_in.py +11 -0
  54. {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_list.py +7 -0
  55. {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_matchers.py +10 -0
  56. {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_matchers_phase3.py +3 -0
  57. assertpy2-2.9.0/tests/test_mutation_hardening.py +106 -0
  58. {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_namedtuple.py +1 -1
  59. {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_numbers.py +30 -0
  60. {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_pytest_plugin.py +28 -1
  61. {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_recursive_compare.py +5 -0
  62. {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_rich_diff.py +7 -2
  63. {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_snapshots.py +34 -0
  64. {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_structural.py +14 -0
  65. assertpy2-2.9.0/tests/test_typing.py +50 -0
  66. assertpy2-2.9.0/uv.lock +2523 -0
  67. assertpy2-2.8.0/assertpy2/snapshot.py +0 -194
  68. assertpy2-2.8.0/uv.lock +0 -1359
  69. {assertpy2-2.8.0 → assertpy2-2.9.0}/.codecov.yml +0 -0
  70. {assertpy2-2.8.0 → assertpy2-2.9.0}/.gitattributes +0 -0
  71. {assertpy2-2.8.0 → assertpy2-2.9.0}/.github/dependabot.yml +0 -0
  72. {assertpy2-2.8.0 → assertpy2-2.9.0}/.github/workflows/codeql.yml +0 -0
  73. {assertpy2-2.8.0 → assertpy2-2.9.0}/.github/workflows/docs.yml +0 -0
  74. {assertpy2-2.8.0 → assertpy2-2.9.0}/.github/workflows/publish.yml +0 -0
  75. {assertpy2-2.8.0 → assertpy2-2.9.0}/.github/workflows/scorecard.yml +0 -0
  76. {assertpy2-2.8.0 → assertpy2-2.9.0}/.github/workflows/zizmor.yml +0 -0
  77. {assertpy2-2.8.0 → assertpy2-2.9.0}/.gitignore +0 -0
  78. {assertpy2-2.8.0 → assertpy2-2.9.0}/CONTRIBUTING.md +0 -0
  79. {assertpy2-2.8.0 → assertpy2-2.9.0}/LICENSE +0 -0
  80. {assertpy2-2.8.0 → assertpy2-2.9.0}/SECURITY.md +0 -0
  81. {assertpy2-2.8.0 → assertpy2-2.9.0}/assertpy2/__init__.py +0 -0
  82. {assertpy2-2.8.0 → assertpy2-2.9.0}/assertpy2/_compat.py +0 -0
  83. {assertpy2-2.8.0 → assertpy2-2.9.0}/assertpy2/bytes_mixin.py +0 -0
  84. {assertpy2-2.8.0 → assertpy2-2.9.0}/assertpy2/json_mixin.py +0 -0
  85. {assertpy2-2.8.0 → assertpy2-2.9.0}/assertpy2/py.typed +0 -0
  86. {assertpy2-2.8.0 → assertpy2-2.9.0}/assertpy2/warning.py +0 -0
  87. {assertpy2-2.8.0 → assertpy2-2.9.0}/docs/data.md +0 -0
  88. {assertpy2-2.8.0 → assertpy2-2.9.0}/docs/fluent.md +0 -0
  89. {assertpy2-2.8.0 → assertpy2-2.9.0}/docs/integrations.md +0 -0
  90. {assertpy2-2.8.0 → assertpy2-2.9.0}/docs/logo-dark.svg +0 -0
  91. {assertpy2-2.8.0 → assertpy2-2.9.0}/docs/logo.svg +0 -0
  92. {assertpy2-2.8.0 → assertpy2-2.9.0}/docs/testing.md +0 -0
  93. {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_behave_matchers.py +0 -0
  94. {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_bool.py +0 -0
  95. {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_bytes.py +0 -0
  96. {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_class.py +0 -0
  97. {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_custom_list.py +0 -0
  98. {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_description.py +0 -0
  99. {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_dyn.py +0 -0
  100. {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_equals.py +0 -0
  101. {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_errors.py +0 -0
  102. {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_expected_warning.py +0 -0
  103. {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_extensions.py +0 -0
  104. {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_fail.py +0 -0
  105. {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_grouped_soft.py +0 -0
  106. {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_json.py +0 -0
  107. {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_matcher_registry.py +0 -0
  108. {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_none.py +0 -0
  109. {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_not.py +0 -0
  110. {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_overloads.py +0 -0
  111. {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_phase2.py +0 -0
  112. {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_pipeline.py +0 -0
  113. {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_property_based.py +0 -0
  114. {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_readme.py +0 -0
  115. {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_recursive_compare_attrs.py +0 -0
  116. {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_recursive_compare_pydantic.py +0 -0
  117. {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_regex_groups.py +0 -0
  118. {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_same_as.py +0 -0
  119. {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_soft.py +0 -0
  120. {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_soft_fail.py +0 -0
  121. {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_string.py +0 -0
  122. {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_traceback.py +0 -0
  123. {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_type.py +0 -0
  124. {assertpy2-2.8.0 → assertpy2-2.9.0}/tests/test_warn.py +0 -0
@@ -68,7 +68,7 @@ jobs:
68
68
  python-version: "3.14"
69
69
 
70
70
  - name: Install dependencies
71
- run: uv sync --extra json
71
+ run: uv sync --extra json --group typecheck
72
72
 
73
73
  - name: Ruff check
74
74
  run: uv run ruff check .
@@ -76,5 +76,11 @@ jobs:
76
76
  - name: Ruff format check
77
77
  run: uv run ruff format --check .
78
78
 
79
- - name: Type check
80
- run: uv run ty check assertpy2/
79
+ - name: Type check (ty)
80
+ run: uv run ty check assertpy2/ tests/test_typing.py
81
+
82
+ - name: Type check (mypy --strict, public typing surface)
83
+ run: uv run mypy --strict --follow-imports=silent tests/test_typing.py
84
+
85
+ - name: Type check (pyright, public typing surface)
86
+ run: uv run pyright tests/test_typing.py
@@ -0,0 +1,63 @@
1
+ name: Mutation
2
+
3
+ on:
4
+ schedule:
5
+ - cron: "0 4 * * 1" # weekly, Monday 04:00 UTC
6
+ workflow_dispatch: {}
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ concurrency:
12
+ group: ${{ github.workflow }}
13
+ cancel-in-progress: true
14
+
15
+ jobs:
16
+ mutation:
17
+ runs-on: ubuntu-latest
18
+ timeout-minutes: 120
19
+ strategy:
20
+ fail-fast: false
21
+ matrix:
22
+ module: [base, numeric, string, contains, collection, dict, date, matchers, helpers]
23
+ steps:
24
+ - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
25
+ with:
26
+ persist-credentials: false
27
+
28
+ - name: Install uv
29
+ uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
30
+
31
+ - name: Set up Python
32
+ uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
33
+ with:
34
+ python-version: "3.14"
35
+
36
+ - name: Install dependencies
37
+ run: uv sync --extra json --group mutation
38
+
39
+ - name: Run mutation testing
40
+ env:
41
+ MODULE: ${{ matrix.module }}
42
+ run: |
43
+ cat > cosmic-ray.toml <<EOF
44
+ [cosmic-ray]
45
+ module-path = "assertpy2/${MODULE}.py"
46
+ timeout = 30.0
47
+ excluded-modules = []
48
+ test-command = ".venv/bin/python -m pytest tests/ -x -q -p no:cacheprovider"
49
+
50
+ [cosmic-ray.distributor]
51
+ name = "local"
52
+ EOF
53
+ uv run cosmic-ray baseline cosmic-ray.toml
54
+ uv run cosmic-ray init cosmic-ray.toml session.sqlite
55
+ uv run cosmic-ray exec cosmic-ray.toml session.sqlite
56
+ {
57
+ echo "### Mutation: ${MODULE}.py"
58
+ echo ""
59
+ echo "Surviving mutants: $(uv run cr-rate session.sqlite)%"
60
+ } >> "$GITHUB_STEP_SUMMARY"
61
+ echo "::group::Surviving mutants (${MODULE}.py)"
62
+ uv run cr-report session.sqlite
63
+ echo "::endgroup::"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: assertpy2
3
- Version: 2.8.0
3
+ Version: 2.9.0
4
4
  Summary: Fluent assertion library for Python with composable matchers, structural matching, and full type safety
5
5
  Project-URL: Homepage, https://github.com/Solganis/assertpy2
6
6
  Project-URL: Repository, https://github.com/Solganis/assertpy2
@@ -66,7 +66,7 @@ Description-Content-Type: text/markdown
66
66
 
67
67
  ---
68
68
 
69
- ## Quick start
69
+ ## [Quick start](https://solganis.github.io/assertpy2/getting-started/)
70
70
 
71
71
  ```bash
72
72
  pip install assertpy2 # drop-in replacement for assertpy, just change the import
@@ -84,10 +84,12 @@ def test_user():
84
84
  assert_that(user).has_name("Alice")
85
85
  ```
86
86
 
87
- ## Why fluent assertions?
87
+ Browse the [full documentation](https://solganis.github.io/assertpy2/) for every assertion, matcher, and integration.
88
+
89
+ ## [Why fluent assertions?](https://solganis.github.io/assertpy2/comparison/)
88
90
 
89
91
  A fluent chain reads as one intent and replaces several bare asserts - and your IDE
90
- offers only the methods that fit the value's type:
92
+ offers only the [methods that fit the value's type](https://solganis.github.io/assertpy2/type-safety/):
91
93
 
92
94
  ```py
93
95
  # bare - three statements, no autocomplete help
@@ -111,25 +113,19 @@ E {'user': {'name': 'Alice', 'role': 'superadmin'}} != {'user': {'name': 'Al
111
113
  E {'status': 'active'} != {'status': 'disabled'}
112
114
  ```
113
115
 
114
- assertpy2 reports the exact path to every difference:
116
+ assertpy2 reports the [exact path to every difference](https://solganis.github.io/assertpy2/errors/#rich-pytest-diffs), in color:
115
117
 
116
- ```text
118
+ ```python
117
119
  assert_that(response).is_equal_to(expected)
118
- --- Structured Diff ---
119
- diff (dict):
120
- status:
121
- - 'active'
122
- + 'disabled'
123
- user.role:
124
- - 'superadmin'
125
- + 'admin'
126
120
  ```
127
121
 
122
+ <img src="https://raw.githubusercontent.com/Solganis/assertpy2/main/docs/assets/diff-equal.png" width="300" alt="Structured diff in the terminal: status and user.role shown with their paths, removals in red and additions in green">
123
+
128
124
  Recursive diffs work for dicts, dataclasses, namedtuples, attrs, and Pydantic models.
129
125
  For responses with dynamic fields (IDs, timestamps), validate a subset with
130
- `matches_structure()` instead of exact equality.
126
+ [`matches_structure()`](https://solganis.github.io/assertpy2/matchers/#structural-matching) instead of exact equality.
131
127
 
132
- ## Type-aware autocomplete
128
+ ## [Type-aware autocomplete](https://solganis.github.io/assertpy2/type-safety/)
133
129
 
134
130
  `assert_that()` uses `@overload` to return type-specific Protocols.
135
131
  Your IDE shows only methods relevant to the value you're testing, not all 100+:
@@ -142,6 +138,8 @@ Your IDE shows only methods relevant to the value you're testing, not all 100+:
142
138
 
143
139
  9 type-specific Protocols instead of one `Any`. Works in PyCharm, VS Code, and any LSP-compatible editor.
144
140
 
141
+ See the [**Type Safety**](https://solganis.github.io/assertpy2/type-safety/) guide for the full walkthrough.
142
+
145
143
  ---
146
144
 
147
145
  ## Features
@@ -170,6 +168,7 @@ Your IDE shows only methods relevant to the value you're testing, not all 100+:
170
168
  - [**Structured errors**](https://solganis.github.io/assertpy2/errors/#structured-errors): `AssertionFailure` with `.actual`, `.expected`, `.diff` attributes.
171
169
  - [**Rich pytest diffs**](https://solganis.github.io/assertpy2/errors/#rich-pytest-diffs): recursive structural diffs for lists, sets, strings, dicts, dataclasses, namedtuples, Pydantic models, and matcher-based assertions (`matches_structure()`, `satisfies()`, `each()`). Circular reference protection.
172
170
  - [**Snapshot testing**](https://solganis.github.io/assertpy2/testing/#snapshot-testing): store and compare data structures in JSON format.
171
+ - **Property-based tested**: comparison, selective-diff, matcher algebra, and collection logic are checked with [Hypothesis](https://hypothesis.readthedocs.io) against reference semantics, on top of 100% branch coverage.
173
172
 
174
173
  **Type safety**
175
174
 
@@ -191,7 +190,7 @@ See the [full documentation](https://solganis.github.io/assertpy2/) for all asse
191
190
 
192
191
  ---
193
192
 
194
- ## Integrations
193
+ ## [Integrations](https://solganis.github.io/assertpy2/integrations/)
195
194
 
196
195
  ### Allure
197
196
 
@@ -239,6 +238,8 @@ def step_impl(context, age):
239
238
 
240
239
  Available types: `PositiveInt`, `NonNegativeInt`, `PositiveFloat`, `NonEmptyString`, `BoolLike`.
241
240
 
241
+ See the [Integrations guide](https://solganis.github.io/assertpy2/integrations/) for attachment modes, configuration, and full examples.
242
+
242
243
  ---
243
244
 
244
245
  <p align="center">
@@ -28,7 +28,7 @@
28
28
 
29
29
  ---
30
30
 
31
- ## Quick start
31
+ ## [Quick start](https://solganis.github.io/assertpy2/getting-started/)
32
32
 
33
33
  ```bash
34
34
  pip install assertpy2 # drop-in replacement for assertpy, just change the import
@@ -46,10 +46,12 @@ def test_user():
46
46
  assert_that(user).has_name("Alice")
47
47
  ```
48
48
 
49
- ## Why fluent assertions?
49
+ Browse the [full documentation](https://solganis.github.io/assertpy2/) for every assertion, matcher, and integration.
50
+
51
+ ## [Why fluent assertions?](https://solganis.github.io/assertpy2/comparison/)
50
52
 
51
53
  A fluent chain reads as one intent and replaces several bare asserts - and your IDE
52
- offers only the methods that fit the value's type:
54
+ offers only the [methods that fit the value's type](https://solganis.github.io/assertpy2/type-safety/):
53
55
 
54
56
  ```py
55
57
  # bare - three statements, no autocomplete help
@@ -73,25 +75,19 @@ E {'user': {'name': 'Alice', 'role': 'superadmin'}} != {'user': {'name': 'Al
73
75
  E {'status': 'active'} != {'status': 'disabled'}
74
76
  ```
75
77
 
76
- assertpy2 reports the exact path to every difference:
78
+ assertpy2 reports the [exact path to every difference](https://solganis.github.io/assertpy2/errors/#rich-pytest-diffs), in color:
77
79
 
78
- ```text
80
+ ```python
79
81
  assert_that(response).is_equal_to(expected)
80
- --- Structured Diff ---
81
- diff (dict):
82
- status:
83
- - 'active'
84
- + 'disabled'
85
- user.role:
86
- - 'superadmin'
87
- + 'admin'
88
82
  ```
89
83
 
84
+ <img src="https://raw.githubusercontent.com/Solganis/assertpy2/main/docs/assets/diff-equal.png" width="300" alt="Structured diff in the terminal: status and user.role shown with their paths, removals in red and additions in green">
85
+
90
86
  Recursive diffs work for dicts, dataclasses, namedtuples, attrs, and Pydantic models.
91
87
  For responses with dynamic fields (IDs, timestamps), validate a subset with
92
- `matches_structure()` instead of exact equality.
88
+ [`matches_structure()`](https://solganis.github.io/assertpy2/matchers/#structural-matching) instead of exact equality.
93
89
 
94
- ## Type-aware autocomplete
90
+ ## [Type-aware autocomplete](https://solganis.github.io/assertpy2/type-safety/)
95
91
 
96
92
  `assert_that()` uses `@overload` to return type-specific Protocols.
97
93
  Your IDE shows only methods relevant to the value you're testing, not all 100+:
@@ -104,6 +100,8 @@ Your IDE shows only methods relevant to the value you're testing, not all 100+:
104
100
 
105
101
  9 type-specific Protocols instead of one `Any`. Works in PyCharm, VS Code, and any LSP-compatible editor.
106
102
 
103
+ See the [**Type Safety**](https://solganis.github.io/assertpy2/type-safety/) guide for the full walkthrough.
104
+
107
105
  ---
108
106
 
109
107
  ## Features
@@ -132,6 +130,7 @@ Your IDE shows only methods relevant to the value you're testing, not all 100+:
132
130
  - [**Structured errors**](https://solganis.github.io/assertpy2/errors/#structured-errors): `AssertionFailure` with `.actual`, `.expected`, `.diff` attributes.
133
131
  - [**Rich pytest diffs**](https://solganis.github.io/assertpy2/errors/#rich-pytest-diffs): recursive structural diffs for lists, sets, strings, dicts, dataclasses, namedtuples, Pydantic models, and matcher-based assertions (`matches_structure()`, `satisfies()`, `each()`). Circular reference protection.
134
132
  - [**Snapshot testing**](https://solganis.github.io/assertpy2/testing/#snapshot-testing): store and compare data structures in JSON format.
133
+ - **Property-based tested**: comparison, selective-diff, matcher algebra, and collection logic are checked with [Hypothesis](https://hypothesis.readthedocs.io) against reference semantics, on top of 100% branch coverage.
135
134
 
136
135
  **Type safety**
137
136
 
@@ -153,7 +152,7 @@ See the [full documentation](https://solganis.github.io/assertpy2/) for all asse
153
152
 
154
153
  ---
155
154
 
156
- ## Integrations
155
+ ## [Integrations](https://solganis.github.io/assertpy2/integrations/)
157
156
 
158
157
  ### Allure
159
158
 
@@ -201,6 +200,8 @@ def step_impl(context, age):
201
200
 
202
201
  Available types: `PositiveInt`, `NonNegativeInt`, `PositiveFloat`, `NonEmptyString`, `BoolLike`.
203
202
 
203
+ See the [Integrations guide](https://solganis.github.io/assertpy2/integrations/) for attachment modes, configuration, and full examples.
204
+
204
205
  ---
205
206
 
206
207
  <p align="center">
@@ -0,0 +1,66 @@
1
+ """Runtime-checkable protocols for duck-typed object introspection.
2
+
3
+ The comparison and diff code accepts arbitrary user objects, so it inspects them structurally: pydantic
4
+ models (``model_dump``), ``attrs`` classes (``__attrs_attrs__``) and namedtuples (``_fields`` /
5
+ ``_asdict``). Expressing those shapes as ``@runtime_checkable`` protocols lets the diff code use
6
+ ``isinstance`` checks the type checker can follow - narrowing the value instead of probing it with
7
+ ``hasattr`` and then suppressing the attribute access.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Any, Protocol, TypeGuard, runtime_checkable
13
+
14
+
15
+ @runtime_checkable
16
+ class SupportsModelDump(Protocol):
17
+ """A pydantic-style model exposing ``model_dump()``."""
18
+
19
+ def model_dump(self) -> dict[str, Any]: ...
20
+
21
+
22
+ @runtime_checkable
23
+ class NamedTupleLike(Protocol):
24
+ """A ``collections.namedtuple`` / ``typing.NamedTuple`` instance."""
25
+
26
+ _fields: tuple[str, ...]
27
+
28
+ def _asdict(self) -> dict[str, Any]: ...
29
+
30
+
31
+ @runtime_checkable
32
+ class AttrsInstance(Protocol):
33
+ """An ``attrs``-decorated instance exposing ``__attrs_attrs__``."""
34
+
35
+ __attrs_attrs__: tuple[Any, ...]
36
+
37
+
38
+ @runtime_checkable
39
+ class MappingLike(Protocol):
40
+ """A dict-like object that can be iterated over and subscripted by key."""
41
+
42
+ def keys(self) -> Any: ...
43
+
44
+ def __iter__(self) -> Any: ...
45
+
46
+ def __getitem__(self, key: Any) -> Any: ...
47
+
48
+
49
+ def is_model_dump_object(obj: object) -> TypeGuard[SupportsModelDump]:
50
+ """Return whether ``obj`` exposes a callable ``model_dump()`` (e.g. a pydantic model)."""
51
+ return isinstance(obj, SupportsModelDump) and callable(obj.model_dump)
52
+
53
+
54
+ def is_namedtuple(obj: object) -> TypeGuard[NamedTupleLike]:
55
+ """Return whether ``obj`` is a namedtuple instance (a ``tuple`` carrying ``_fields``/``_asdict``)."""
56
+ return isinstance(obj, tuple) and isinstance(obj, NamedTupleLike)
57
+
58
+
59
+ def is_attrs_instance(obj: object) -> TypeGuard[AttrsInstance]:
60
+ """Return whether ``obj`` is an ``attrs``-decorated instance."""
61
+ return isinstance(obj, AttrsInstance)
62
+
63
+
64
+ def is_mapping_like(obj: object) -> TypeGuard[MappingLike]:
65
+ """Return whether ``obj`` is dict-like: iterable with ``keys()`` and ``[]`` access."""
66
+ return isinstance(obj, MappingLike) and callable(obj.keys)
@@ -1,9 +1,10 @@
1
1
  from __future__ import annotations
2
2
 
3
- import logging
4
3
  from typing import TYPE_CHECKING, Any
5
4
 
6
5
  if TYPE_CHECKING:
6
+ import logging
7
+
7
8
  from ._compat import Self
8
9
  from .errors import DiffResult
9
10
 
@@ -57,15 +58,22 @@ class _MixinBase:
57
58
 
58
59
  def _validate_close_to_args(self, val: object, other: object, tolerance: object) -> None: ...
59
60
 
60
- def _check_dict_like(
61
+ def _is_dict_like(
62
+ self,
63
+ d: object,
64
+ check_keys: bool = ...,
65
+ check_values: bool = ...,
66
+ check_getitem: bool = ...,
67
+ ) -> bool: ...
68
+
69
+ def _require_dict_like(
61
70
  self,
62
71
  d: object,
63
72
  check_keys: bool = ...,
64
73
  check_values: bool = ...,
65
74
  check_getitem: bool = ...,
66
75
  name: str = ...,
67
- return_as_bool: bool = ...,
68
- ) -> bool | None: ...
76
+ ) -> None: ...
69
77
 
70
78
  def _check_iterable(self, val: object, check_getitem: bool = ..., name: str = ...) -> None: ...
71
79
 
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  from typing import TYPE_CHECKING
4
4
 
5
5
  if TYPE_CHECKING:
6
- from collections.abc import Callable
6
+ from collections.abc import Callable, Hashable
7
7
  from pathlib import Path
8
8
  from typing import Any, Protocol
9
9
 
@@ -12,12 +12,17 @@ if TYPE_CHECKING:
12
12
  from .async_assertions import AsyncAssertionBuilder
13
13
  from .matchers import Matcher
14
14
 
15
+ # ``ignore``/``include`` accept a single key, a nested-path tuple, or a list/set/frozenset of them.
16
+ _KeySpecs = Hashable | list[Hashable] | set[Hashable] | frozenset[Hashable]
17
+
15
18
  class _CoreAssertion(Protocol):
16
19
  """Base protocol with assertions available for all types."""
17
20
 
18
21
  # BaseMixin
19
22
  def described_as(self, description: str) -> Self: ...
20
- def is_equal_to(self, other: object, **kwargs: object) -> Self: ...
23
+ def is_equal_to(
24
+ self, other: object, *, ignore: _KeySpecs | None = ..., include: _KeySpecs | None = ...
25
+ ) -> Self: ...
21
26
  def is_not_equal_to(self, other: object) -> Self: ...
22
27
  def is_same_as(self, other: object) -> Self: ...
23
28
  def is_not_same_as(self, other: object) -> Self: ...
@@ -9,17 +9,18 @@ import logging
9
9
  import os
10
10
  import sys
11
11
  import types
12
- from collections.abc import Callable, Iterator
13
- from typing import TYPE_CHECKING, overload
12
+ from typing import TYPE_CHECKING, Final, overload
14
13
 
15
14
  if TYPE_CHECKING:
16
15
  import datetime
17
16
  import pathlib
17
+ from collections.abc import Callable, Iterator
18
18
 
19
19
  from ._compat import Self
20
20
  from ._typing import (
21
21
  _BytesAssertion,
22
22
  _CallableAssertion,
23
+ _CoreAssertion,
23
24
  _DateAssertion,
24
25
  _DictAssertion,
25
26
  _IterableAssertion,
@@ -47,37 +48,15 @@ from .snapshot import SnapshotMixin
47
48
  from .string import StringMixin
48
49
  from .warning import WarningMixin
49
50
 
50
- __version__ = "2.8.0"
51
+ __version__ = "2.9.0"
51
52
 
52
53
  __tracebackhide__ = True # clean tracebacks via py.test integration
53
54
  contextlib.__tracebackhide__ = True # ty: ignore[unresolved-attribute] # pytest monkey-patch
54
55
 
55
- # assertpy files
56
- ASSERTPY_FILES = [
57
- os.path.join("assertpy2", file)
58
- for file in [
59
- "assertpy.py",
60
- "async_assertions.py",
61
- "base.py",
62
- "bytes_mixin.py",
63
- "collection.py",
64
- "contains.py",
65
- "date.py",
66
- "dict.py",
67
- "dynamic.py",
68
- "errors.py",
69
- "exception.py",
70
- "extracting.py",
71
- "file.py",
72
- "helpers.py",
73
- "json_mixin.py",
74
- "matchers.py",
75
- "numeric.py",
76
- "pytest_plugin.py",
77
- "snapshot.py",
78
- "string.py",
79
- "warning.py",
80
- ]
56
+ # assertpy2 source files, used to strip internal frames when locating the caller for warn-mode messages.
57
+ # Derived from the package directory so new modules are covered automatically (no hand-maintained list).
58
+ ASSERTPY_FILES: Final = [
59
+ os.path.join("assertpy2", name) for name in os.listdir(os.path.dirname(__file__)) if name.endswith(".py")
81
60
  ]
82
61
 
83
62
  # soft assertions (contextvars for thread/async safety)
@@ -253,11 +232,16 @@ def assert_that(val: bytes | bytearray, description: str = "") -> _BytesAssertio
253
232
  def assert_that(val: Callable[..., object], description: str = "") -> _CallableAssertion: ...
254
233
 
255
234
 
235
+ # Fallback returns the concrete AssertionBuilder so object- and union-typed values keep the full API.
236
+ # The specific protocols are not assignable to AssertionBuilder, so mypy --strict reports overload-overlap
237
+ # for each specific overload and pyright one reportOverlappingOverload; ty (the gate) does not flag it. Kept
238
+ # intentionally - returning _CoreAssertion here would strip type-specific assertions from object/union values.
256
239
  @overload
257
240
  def assert_that(val: object, description: str = "") -> AssertionBuilder: ...
258
241
 
259
242
 
260
- def assert_that(val, description=""):
243
+ # Return the common base protocol so each overload stays consistent with the impl (no reportInconsistentOverload).
244
+ def assert_that(val, description="") -> _CoreAssertion:
261
245
  """Set the value to be tested, plus an optional description, and allow assertions to be called.
262
246
 
263
247
  This is a factory method for the :class:`AssertionBuilder`, and the single most important
@@ -613,7 +597,7 @@ class AssertionBuilder(
613
597
  def error(self, msg, *, actual=None, expected=None, diff=None) -> Self:
614
598
  """Helper to raise an ``AssertionError`` with the given message.
615
599
 
616
- If an error description is set by :meth:`~assertpy.base.BaseMixin.described_as`, then that
600
+ If an error description is set by :meth:`~assertpy2.base.BaseMixin.described_as`, then that
617
601
  description is prepended to the error message.
618
602
 
619
603
  When structured data (``actual``, ``expected``, or ``diff``) is provided, raises
@@ -61,15 +61,14 @@ class AsyncAssertionBuilder:
61
61
  last_error: AssertionError | None = None
62
62
  while True:
63
63
  try:
64
- if inspect.iscoroutinefunction(self._func):
65
- val = await self._func()
66
- else:
67
- val = self._func()
64
+ val = self._func()
65
+ if inspect.isawaitable(val):
66
+ val = await val
68
67
  builder = self._builder_func(val, self._description)
69
68
  method = getattr(builder, name)
70
69
  method(*args, **kwargs)
71
70
  return builder
72
- except AssertionError as exc:
71
+ except AssertionError as exc: # noqa: PERF203 # retry-on-failure needs the try/except per poll iteration
73
72
  last_error = exc
74
73
  if loop.time() >= deadline:
75
74
  raise AssertionError(