├── .github ├── dependabot.yml └── workflows │ └── test-and-lint.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── asserts ├── __init__.py ├── __init__.pyi └── py.typed ├── mypy.ini ├── poetry.lock ├── pyproject.toml └── test_asserts.py /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | time: '04:00' 8 | groups: 9 | dependencies: 10 | exclude-patterns: 11 | - "mypy" 12 | - "types-*" 13 | update-types: 14 | - "minor" 15 | - "patch" 16 | mypy: 17 | patterns: 18 | - "mypy" 19 | types: 20 | patterns: 21 | - "types-*" 22 | 23 | - package-ecosystem: github-actions 24 | directory: "/" 25 | schedule: 26 | interval: weekly 27 | time: '04:00' 28 | -------------------------------------------------------------------------------- /.github/workflows/test-and-lint.yml: -------------------------------------------------------------------------------- 1 | name: Test and lint 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] 11 | fail-fast: false 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Set up Python ${{ matrix.python-version }} 16 | uses: actions/setup-python@v5 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | cache: "pip" 20 | cache-dependency-path: "**/poetry.lock" 21 | - name: Install poetry 22 | run: pip --disable-pip-version-check install -U poetry 23 | - name: Install Python packages 24 | run: poetry install 25 | - name: Lint with ruff 26 | run: | 27 | poetry run ruff --version 28 | poetry run poe lint --output-format=full 29 | - name: Type checking with mypy 30 | run: | 31 | poetry run mypy --version 32 | poetry run poe typecheck 33 | - name: Test with unittest 34 | run: poetry run poe test 35 | - name: Run doctests 36 | run: poetry run poe doctest 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info 2 | *.pyc 3 | 4 | /.venv* 5 | /build 6 | /dist 7 | 8 | .mypy_cache 9 | 10 | __pycache__ 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog for python-asserts 2 | 3 | python-asserts adheres to [semantic versioning](https://semver.org/). 4 | 5 | ## UNRELEASED – 6 | 7 | ## [0.13.1] – 2024-04-29 8 | 9 | ### Fixed 10 | 11 | Fixed Python 3.12 deprecation warnings. 12 | 13 | ## [0.13.0] – 2024-03-13 14 | 15 | ### Added 16 | 17 | - Add support for Python 3.12. 18 | - Add `Present` and `Absent` for absence checks in `assert_json_subset()`. 19 | 20 | ### Removed 21 | 22 | - Drop support for Python 3.7. 23 | 24 | ### Deprecated 25 | 26 | - Deprecate `Exists` in favor of `Present` and `Absent` in 27 | `assert_json_subset()`. 28 | 29 | ## [0.12.0] 30 | 31 | ### Added 32 | 33 | - Add `assert_not_regex()`. 34 | 35 | ### Changed 36 | 37 | - Modernize the type stubs. 38 | 39 | ### Removed 40 | 41 | - Drop support for Python 3.6. 42 | 43 | ## [0.11.1] 44 | 45 | ### Added 46 | 47 | - `assert_json_subset()` can now check for the existence or non-existence 48 | of object members using the new `Exists` helper. 49 | - Non-string (or `Exists`) object member names in the first argument to 50 | `assert_json_subset()` now raise a `TypeError`. 51 | 52 | ## [0.11.0] 53 | 54 | ### Removed 55 | 56 | - Drop support for Python 2.7 and 3.5. 57 | 58 | ## [0.10.0] 59 | 60 | ### Added 61 | 62 | - `AssertRaisesContext` and `AssertWarnsContext` now return themselves 63 | when `__enter__()` is called. By extension it now easier to call 64 | `add_test()` with `assert_raises()` et al: 65 | 66 | ```python 67 | with assert_raises(KeyError) as context: 68 | context.add_test(...) 69 | ... 70 | ``` 71 | 72 | - Add `AssertRaisesContext.exc_val` property to access the caught 73 | exception after leaving the context manager: 74 | 75 | ```python 76 | with assert_raises(KeyError) as context: 77 | ... 78 | assert_equal("expected message", str(context.exc_val)) 79 | ``` 80 | 81 | ### Removed 82 | 83 | - Drop support for Python 3.4. 84 | 85 | ## [0.9.1] 86 | 87 | ### Changed 88 | 89 | - `AssertRaisesContext` and sub-classes are now generic over the 90 | exception type. 91 | 92 | ## [0.9.0] 93 | 94 | ### Added 95 | 96 | - Add `assert_json_subset()`. 97 | 98 | ## [0.8.6] 99 | 100 | ### Added 101 | 102 | - Add support for Python 3.7 (contributed by Frank Niessink). 103 | 104 | ## [0.8.5] 105 | 106 | ### Added 107 | 108 | - Add `assert_dict_equal()`. 109 | - Add `assert_dict_superset()`. 110 | 111 | ### Changed 112 | 113 | - `assert_equal()`: Use `assert_dict_equal()` if applicable. 114 | 115 | ## [0.8.4] 116 | 117 | ### Changed 118 | 119 | - `fail()` is now marked with `NoReturn` in type stub. 120 | 121 | ### Fixed 122 | 123 | - Improve type annotations for Python 2. 124 | 125 | ## [0.8.3] 126 | 127 | ### Fixed 128 | 129 | - Fix type signature of `AssertRaisesContext.__exit__()`. 130 | 131 | ## [0.8.2] 132 | 133 | ### Added 134 | 135 | - Add a py.typed file to signal that this package supports type hints. 136 | 137 | ## [0.8.1] 138 | 139 | ### Fixed 140 | 141 | - `assert_raises_regex()`: Handle exceptions without any message correctly. 142 | 143 | ## [0.8.0] 144 | 145 | ### Added 146 | 147 | - assert_count_equal(): Add `msg_fmt` argument. 148 | - Add AssertRaisesErrnoContext, AssertRaisesRegexContext, and 149 | AssertWarnsRegexContext. 150 | 151 | ### Changed 152 | 153 | - Replace `msg` argument with `msg_fmt` in all assertions (except `fail()`). 154 | This allows you to customize error messages more easily than before, because 155 | `format()` with appropriate keyword arguments is now called on these 156 | strings. See the documentation of individual assertions for the supported 157 | arguments. 158 | - Replace AssertRaisesContext.msg and AssertWarnsContext.msg with msg_fmt. 159 | - assert_almost_equal(), assert_not_almost_equal(): Place msg_fmt as third 160 | argument. 161 | 162 | ## [0.7.3] 163 | 164 | ### Added 165 | 166 | - Add assert_not_almost_equal(). 167 | 168 | ### Changed 169 | 170 | - assert_almost_equal(): Raise ValueError if diff <= 0. 171 | 172 | ### Fixed 173 | 174 | - assert_almost_equal() would never fail if a delta was supplied and the 175 | second number was smaller than the first. 176 | - Use fail() instead of raise AssertionError in a few assertions. 177 | 178 | ## [0.7.2] 179 | 180 | ### Added 181 | 182 | - Add assert_warns() and assert_warns_regex(). 183 | 184 | ## [0.7.1] 185 | 186 | ### Changed 187 | 188 | - Distribute a wheel. 189 | - asserts is now a package, instead of a module. 190 | 191 | ## [0.7.0] 192 | 193 | ### Added 194 | 195 | - Add a stub file. 196 | - Add assert_count_equal(). 197 | 198 | ## [0.6] 199 | 200 | ### Added 201 | 202 | - Add assert_less(), assert_less_equal(), assert_greater(), and 203 | assert_greater_equal(). 204 | - Add assert_not_is_instance(). 205 | 206 | ### Changed 207 | 208 | - assert_datetime_about_now()/assert_datetime_about_now_utc(): Handle 209 | comparison with None more gracefully. 210 | 211 | ## [0.5.1] 212 | 213 | ### Added 214 | 215 | - Add the LICENSE file to the distribution. 216 | 217 | ## [0.5] 218 | 219 | Initial release. 220 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Sebastian Rittau 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python Asserts 2 | 3 | [![License](https://img.shields.io/pypi/l/asserts.svg)](https://pypi.python.org/pypi/asserts/) 4 | [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/asserts)](https://pypi.python.org/pypi/asserts/) 5 | [![GitHub](https://img.shields.io/github/release/srittau/python-asserts/all.svg)](https://github.com/srittau/python-asserts/releases/) 6 | [![pypi](https://img.shields.io/pypi/v/asserts.svg)](https://pypi.python.org/pypi/asserts/) 7 | [![GitHub Actions](https://img.shields.io/github/actions/workflow/status/srittau/python-asserts/test-and-lint.yml)](https://github.com/srittau/python-asserts/actions/workflows/test-and-lint.yml) 8 | 9 | Stand-alone Assertions for Python 10 | 11 | This package provides a few advantages over the assertions provided by 12 | unittest.TestCase: 13 | 14 | - Can be used stand-alone, for example: 15 | - In test cases, not derived from TestCase. 16 | - In fake and mock classes. 17 | - In implementations as rich alternative to the assert statement. 18 | - PEP 8 compliance. 19 | - Custom stand-alone assertions can be written easily. 20 | - Arguably a better separation of concerns, since TestCase is responsible 21 | for test running only, if assertion functions are used exclusively. 22 | 23 | There are a few regressions compared to assertions from TestCase: 24 | 25 | - The default assertion class (`AssertionError`) can not be overwritten. This 26 | is rarely a problem in practice. 27 | - asserts does not support the `addTypeEqualityFunc()` functionality. 28 | 29 | Usage: 30 | 31 | ```python 32 | >>> from asserts import assert_true, assert_equal, assert_raises 33 | >>> my_var = 13 34 | >>> assert_equal(13, my_var) 35 | >>> assert_true(True, msg="custom failure message") 36 | >>> with assert_raises(KeyError): 37 | ... raise KeyError() 38 | ``` 39 | 40 | Failure messages can be customized: 41 | 42 | ```python 43 | >>> assert_equal(13, 14, msg_fmt="{got} is wrong, expected {expected}") 44 | Traceback (most recent call last): 45 | ... 46 | AssertionError: 14 is wrong, expected 13 47 | ``` 48 | -------------------------------------------------------------------------------- /asserts/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Rich Assertions. 3 | 4 | This module contains several rich standard assertions that can be used in unit 5 | tests and in implementations. Users are encouraged to define their own 6 | assertions, possibly using assertions from this package as a basis. 7 | 8 | >>> assert_equal(13, 13) 9 | >>> assert_equal(13, 14) 10 | Traceback (most recent call last): 11 | ... 12 | AssertionError: 13 != 14 13 | >>> with assert_raises(KeyError): 14 | ... raise KeyError() 15 | >>> with assert_raises(KeyError): 16 | ... pass 17 | Traceback (most recent call last): 18 | ... 19 | AssertionError: KeyError not raised 20 | 21 | """ 22 | 23 | from __future__ import annotations 24 | 25 | import re 26 | import sys 27 | from datetime import datetime, timedelta, timezone 28 | from json import loads as json_loads 29 | from typing import Any, Callable, Set 30 | from warnings import WarningMessage, catch_warnings 31 | 32 | from typing_extensions import deprecated 33 | 34 | 35 | def fail(msg=None): 36 | """Raise an AssertionError with the given message. 37 | 38 | >>> fail("my message") 39 | Traceback (most recent call last): 40 | ... 41 | AssertionError: my message 42 | 43 | """ 44 | raise AssertionError(msg or "assertion failure") 45 | 46 | 47 | def assert_true(expr, msg_fmt="{msg}"): 48 | """Fail the test unless the expression is truthy. 49 | 50 | >>> assert_true("Hello World!") 51 | >>> assert_true("") 52 | Traceback (most recent call last): 53 | ... 54 | AssertionError: '' is not truthy 55 | 56 | The following msg_fmt arguments are supported: 57 | * msg - the default error message 58 | * expr - tested expression 59 | """ 60 | 61 | if not expr: 62 | msg = "{!r} is not truthy".format(expr) 63 | fail(msg_fmt.format(msg=msg, expr=expr)) 64 | 65 | 66 | def assert_false(expr, msg_fmt="{msg}"): 67 | """Fail the test unless the expression is falsy. 68 | 69 | >>> assert_false("") 70 | >>> assert_false("Hello World!") 71 | Traceback (most recent call last): 72 | ... 73 | AssertionError: 'Hello World!' is not falsy 74 | 75 | The following msg_fmt arguments are supported: 76 | * msg - the default error message 77 | * expr - tested expression 78 | """ 79 | 80 | if expr: 81 | msg = "{!r} is not falsy".format(expr) 82 | fail(msg_fmt.format(msg=msg, expr=expr)) 83 | 84 | 85 | def assert_boolean_true(expr, msg_fmt="{msg}"): 86 | """Fail the test unless the expression is the constant True. 87 | 88 | >>> assert_boolean_true(True) 89 | >>> assert_boolean_true("Hello World!") 90 | Traceback (most recent call last): 91 | ... 92 | AssertionError: 'Hello World!' is not True 93 | 94 | The following msg_fmt arguments are supported: 95 | * msg - the default error message 96 | * expr - tested expression 97 | """ 98 | 99 | if expr is not True: 100 | msg = "{!r} is not True".format(expr) 101 | fail(msg_fmt.format(msg=msg, expr=expr)) 102 | 103 | 104 | def assert_boolean_false(expr, msg_fmt="{msg}"): 105 | """Fail the test unless the expression is the constant False. 106 | 107 | >>> assert_boolean_false(False) 108 | >>> assert_boolean_false(0) 109 | Traceback (most recent call last): 110 | ... 111 | AssertionError: 0 is not False 112 | 113 | The following msg_fmt arguments are supported: 114 | * msg - the default error message 115 | * expr - tested expression 116 | """ 117 | 118 | if expr is not False: 119 | msg = "{!r} is not False".format(expr) 120 | fail(msg_fmt.format(msg=msg, expr=expr)) 121 | 122 | 123 | def assert_is_none(expr, msg_fmt="{msg}"): 124 | """Fail if the expression is not None. 125 | 126 | >>> assert_is_none(None) 127 | >>> assert_is_none(False) 128 | Traceback (most recent call last): 129 | ... 130 | AssertionError: False is not None 131 | 132 | The following msg_fmt arguments are supported: 133 | * msg - the default error message 134 | * expr - tested expression 135 | """ 136 | 137 | if expr is not None: 138 | msg = "{!r} is not None".format(expr) 139 | fail(msg_fmt.format(msg=msg, expr=expr)) 140 | 141 | 142 | def assert_is_not_none(expr, msg_fmt="{msg}"): 143 | """Fail if the expression is None. 144 | 145 | >>> assert_is_not_none(0) 146 | >>> assert_is_not_none(None) 147 | Traceback (most recent call last): 148 | ... 149 | AssertionError: expression is None 150 | 151 | The following msg_fmt arguments are supported: 152 | * msg - the default error message 153 | * expr - tested expression 154 | """ 155 | if expr is None: 156 | msg = "expression is None" 157 | fail(msg_fmt.format(msg=msg, expr=expr)) 158 | 159 | 160 | def assert_equal(first, second, msg_fmt="{msg}"): 161 | """Fail unless first equals second, as determined by the '==' operator. 162 | 163 | >>> assert_equal(5, 5.0) 164 | >>> assert_equal("Hello World!", "Goodbye!") 165 | Traceback (most recent call last): 166 | ... 167 | AssertionError: 'Hello World!' != 'Goodbye!' 168 | 169 | The following msg_fmt arguments are supported: 170 | * msg - the default error message 171 | * first - the first argument 172 | * second - the second argument 173 | """ 174 | 175 | if isinstance(first, dict) and isinstance(second, dict): 176 | assert_dict_equal(first, second, msg_fmt) 177 | elif not first == second: 178 | msg = "{!r} != {!r}".format(first, second) 179 | fail(msg_fmt.format(msg=msg, first=first, second=second)) 180 | 181 | 182 | def assert_not_equal(first, second, msg_fmt="{msg}"): 183 | """Fail if first equals second, as determined by the '==' operator. 184 | 185 | >>> assert_not_equal(5, 8) 186 | >>> assert_not_equal(-7, -7.0) 187 | Traceback (most recent call last): 188 | ... 189 | AssertionError: -7 == -7.0 190 | 191 | The following msg_fmt arguments are supported: 192 | * msg - the default error message 193 | * first - the first argument 194 | * second - the second argument 195 | """ 196 | 197 | if first == second: 198 | msg = "{!r} == {!r}".format(first, second) 199 | fail(msg_fmt.format(msg=msg, first=first, second=second)) 200 | 201 | 202 | def assert_almost_equal( 203 | first, second, msg_fmt="{msg}", places=None, delta=None 204 | ): 205 | """Fail if first and second are not equal after rounding. 206 | 207 | By default, the difference between first and second is rounded to 208 | 7 decimal places. This can be configured with the places argument. 209 | Alternatively, delta can be used to specify the maximum allowed 210 | difference between first and second. 211 | 212 | If first and second can not be rounded or both places and delta are 213 | supplied, a TypeError is raised. 214 | 215 | >>> assert_almost_equal(5, 5.00000001) 216 | >>> assert_almost_equal(5, 5.001) 217 | Traceback (most recent call last): 218 | ... 219 | AssertionError: 5 != 5.001 within 7 places 220 | >>> assert_almost_equal(5, 5.001, places=2) 221 | >>> assert_almost_equal(5, 5.001, delta=0.1) 222 | 223 | The following msg_fmt arguments are supported: 224 | * msg - the default error message 225 | * first - the first argument 226 | * second - the second argument 227 | * places - number of places to compare or None 228 | * delta - delta or None 229 | """ 230 | 231 | if delta is not None and places is not None: 232 | raise TypeError("'places' and 'delta' are mutually exclusive") 233 | if delta is not None: 234 | if delta <= 0: 235 | raise ValueError("delta must be larger than 0") 236 | diff = abs(second - first) 237 | success = diff < delta 238 | detail_msg = "with delta={}".format(delta) 239 | else: 240 | if places is None: 241 | places = 7 242 | success = not round(second - first, places) 243 | detail_msg = "within {} places".format(places) 244 | if not success: 245 | msg = "{!r} != {!r} {}".format(first, second, detail_msg) 246 | fail( 247 | msg_fmt.format( 248 | msg=msg, first=first, second=second, places=places, delta=delta 249 | ) 250 | ) 251 | 252 | 253 | def assert_not_almost_equal( 254 | first, second, msg_fmt="{msg}", places=None, delta=None 255 | ): 256 | """Fail if first and second are equal after rounding. 257 | 258 | By default, the difference between first and second is rounded to 259 | 7 decimal places. This can be configured with the places argument. 260 | Alternatively, delta can be used to specify the maximum allowed 261 | difference between first and second. 262 | 263 | If first and second can not be rounded or both places and delta are 264 | supplied, a TypeError is raised. 265 | 266 | >>> assert_not_almost_equal(5, 5.001) 267 | >>> assert_not_almost_equal(5, 5.00000001) 268 | Traceback (most recent call last): 269 | ... 270 | AssertionError: 5 == 5.00000001 within 7 places 271 | >>> assert_not_almost_equal(5, 5.001, places=2) 272 | Traceback (most recent call last): 273 | ... 274 | AssertionError: 5 == 5.001 within 2 places 275 | >>> assert_not_almost_equal(5, 5.001, delta=0.1) 276 | Traceback (most recent call last): 277 | ... 278 | AssertionError: 5 == 5.001 with delta=0.1 279 | 280 | The following msg_fmt arguments are supported: 281 | * msg - the default error message 282 | * first - the first argument 283 | * second - the second argument 284 | * places - number of places to compare or None 285 | * delta - delta or None 286 | """ 287 | 288 | if delta is not None and places is not None: 289 | raise TypeError("'places' and 'delta' are mutually exclusive") 290 | if delta is not None: 291 | if delta <= 0: 292 | raise ValueError("delta must be larger than 0") 293 | diff = abs(second - first) 294 | success = diff >= delta 295 | detail_msg = "with delta={}".format(delta) 296 | else: 297 | if places is None: 298 | places = 7 299 | success = bool(round(second - first, places)) 300 | detail_msg = "within {} places".format(places) 301 | if not success: 302 | msg = "{!r} == {!r} {}".format(first, second, detail_msg) 303 | fail( 304 | msg_fmt.format( 305 | msg=msg, first=first, second=second, places=places, delta=delta 306 | ) 307 | ) 308 | 309 | 310 | def assert_dict_equal( 311 | first, second, key_msg_fmt="{msg}", value_msg_fmt="{msg}" 312 | ): 313 | """Fail unless first dictionary equals second. 314 | 315 | The dictionaries are considered equal, if they both contain the same 316 | keys, and their respective values are also equal. 317 | 318 | >>> assert_dict_equal({"foo": 5}, {"foo": 5}) 319 | >>> assert_dict_equal({"foo": 5}, {}) 320 | Traceback (most recent call last): 321 | ... 322 | AssertionError: key 'foo' missing from right dict 323 | 324 | The following key_msg_fmt arguments are supported, if the keys do not 325 | match: 326 | * msg - the default error message 327 | * first - the first dict 328 | * second - the second dict 329 | * missing_keys - list of keys missing from right 330 | * extra_keys - list of keys missing from left 331 | 332 | The following value_msg_fmt arguments are supported, if a value does not 333 | match: 334 | * msg - the default error message 335 | * first - the first dict 336 | * second - the second dict 337 | * key - the key where the value does not match 338 | * first_value - the value in the first dict 339 | * second_value - the value in the second dict 340 | """ 341 | first_keys = set(first.keys()) 342 | second_keys = set(second.keys()) 343 | missing_keys = list(first_keys - second_keys) 344 | extra_keys = list(second_keys - first_keys) 345 | if missing_keys or extra_keys: 346 | if missing_keys: 347 | if len(missing_keys) == 1: 348 | msg = "key {!r} missing from right dict".format( 349 | missing_keys[0] 350 | ) 351 | else: 352 | keys = ", ".join(sorted(repr(k) for k in missing_keys)) 353 | msg = "keys {} missing from right dict".format(keys) 354 | else: 355 | if len(extra_keys) == 1: 356 | msg = "extra key {!r} in right dict".format(extra_keys[0]) 357 | else: 358 | keys = ", ".join(sorted(repr(k) for k in extra_keys)) 359 | msg = "extra keys {} in right dict".format(keys) 360 | if key_msg_fmt: 361 | msg = key_msg_fmt.format( 362 | msg=msg, 363 | first=first, 364 | second=second, 365 | missing_keys=missing_keys, 366 | extra_keys=extra_keys, 367 | ) 368 | raise AssertionError(msg) 369 | for key in first: 370 | first_value = first[key] 371 | second_value = second[key] 372 | msg = "key '{}' differs: {!r} != {!r}".format( 373 | key, first_value, second_value 374 | ) 375 | if value_msg_fmt: 376 | msg = value_msg_fmt.format( 377 | msg=msg, 378 | first=first, 379 | second=second, 380 | key=key, 381 | first_value=first_value, 382 | second_value=second_value, 383 | ) 384 | msg = msg.replace("{", "{{").replace("}", "}}") 385 | assert_equal(first_value, second_value, msg_fmt=msg) 386 | 387 | 388 | def assert_dict_superset( 389 | first, second, key_msg_fmt="{msg}", value_msg_fmt="{msg}" 390 | ): 391 | """Fail unless second dictionary is a superset of the first. 392 | 393 | The second dictionary must contain all keys of the first and their 394 | values are equal (or a superset in case of dicts). But the second 395 | dictionary can contain additional keys. 396 | 397 | >>> assert_dict_superset({"foo": 5}, {"foo": 5, "bar": 10}) 398 | >>> assert_dict_superset({"foo": 5}, {}) 399 | Traceback (most recent call last): 400 | ... 401 | AssertionError: key 'foo' missing from right dict 402 | 403 | The following key_msg_fmt arguments are supported, if the keys do not 404 | match: 405 | * msg - the default error message 406 | * first - the first dict 407 | * second - the second dict 408 | * missing_keys - list of keys missing from right 409 | 410 | The following value_msg_fmt arguments are supported, if a value does not 411 | match: 412 | * msg - the default error message 413 | * first - the first dict 414 | * second - the second dict 415 | * key - the key where the value does not match 416 | * first_value - the value in the first dict 417 | * second_value - the value in the second dict 418 | """ 419 | first_keys = set(first.keys()) 420 | second_keys = set(second.keys()) 421 | missing_keys = list(first_keys - second_keys) 422 | if missing_keys: 423 | if len(missing_keys) == 1: 424 | msg = "key {!r} missing from right dict".format(missing_keys[0]) 425 | else: 426 | keys = ", ".join(sorted(repr(k) for k in missing_keys)) 427 | msg = "keys {} missing from right dict".format(keys) 428 | if key_msg_fmt: 429 | msg = key_msg_fmt.format( 430 | msg=msg, first=first, second=second, missing_keys=missing_keys 431 | ) 432 | raise AssertionError(msg) 433 | for key in first: 434 | first_value = first[key] 435 | second_value = second[key] 436 | msg = "key '{}' differs: {!r} != {!r}".format( 437 | key, first_value, second_value 438 | ) 439 | if value_msg_fmt: 440 | msg = value_msg_fmt.format( 441 | msg=msg, 442 | first=first, 443 | second=second, 444 | key=key, 445 | first_value=first_value, 446 | second_value=second_value, 447 | ) 448 | msg = msg.replace("{", "{{").replace("}", "}}") 449 | assert_equal(first_value, second_value, msg_fmt=msg) 450 | 451 | 452 | def assert_less(first, second, msg_fmt="{msg}"): 453 | """Fail if first is not less than second. 454 | 455 | >>> assert_less('bar', 'foo') 456 | >>> assert_less(5, 5) 457 | Traceback (most recent call last): 458 | ... 459 | AssertionError: 5 is not less than 5 460 | 461 | The following msg_fmt arguments are supported: 462 | * msg - the default error message 463 | * first - the first argument 464 | * second - the second argument 465 | """ 466 | 467 | if not first < second: 468 | msg = "{!r} is not less than {!r}".format(first, second) 469 | fail(msg_fmt.format(msg=msg, first=first, second=second)) 470 | 471 | 472 | def assert_less_equal(first, second, msg_fmt="{msg}"): 473 | """Fail if first is not less than or equal to second. 474 | 475 | >>> assert_less_equal('bar', 'foo') 476 | >>> assert_less_equal(5, 5) 477 | >>> assert_less_equal(6, 5) 478 | Traceback (most recent call last): 479 | ... 480 | AssertionError: 6 is not less than or equal to 5 481 | 482 | The following msg_fmt arguments are supported: 483 | * msg - the default error message 484 | * first - the first argument 485 | * second - the second argument 486 | """ 487 | 488 | if not first <= second: 489 | msg = "{!r} is not less than or equal to {!r}".format(first, second) 490 | fail(msg_fmt.format(msg=msg, first=first, second=second)) 491 | 492 | 493 | def assert_greater(first, second, msg_fmt="{msg}"): 494 | """Fail if first is not greater than second. 495 | 496 | >>> assert_greater('foo', 'bar') 497 | >>> assert_greater(5, 5) 498 | Traceback (most recent call last): 499 | ... 500 | AssertionError: 5 is not greater than 5 501 | 502 | The following msg_fmt arguments are supported: 503 | * msg - the default error message 504 | * first - the first argument 505 | * second - the second argument 506 | """ 507 | 508 | if not first > second: 509 | msg = "{!r} is not greater than {!r}".format(first, second) 510 | fail(msg_fmt.format(msg=msg, first=first, second=second)) 511 | 512 | 513 | def assert_greater_equal(first, second, msg_fmt="{msg}"): 514 | """Fail if first is not greater than or equal to second. 515 | 516 | >>> assert_greater_equal('foo', 'bar') 517 | >>> assert_greater_equal(5, 5) 518 | >>> assert_greater_equal(5, 6) 519 | Traceback (most recent call last): 520 | ... 521 | AssertionError: 5 is not greater than or equal to 6 522 | 523 | The following msg_fmt arguments are supported: 524 | * msg - the default error message 525 | * first - the first argument 526 | * second - the second argument 527 | """ 528 | 529 | if not first >= second: 530 | msg = "{!r} is not greater than or equal to {!r}".format(first, second) 531 | fail(msg_fmt.format(msg=msg, first=first, second=second)) 532 | 533 | 534 | def assert_regex(text, regex, msg_fmt="{msg}"): 535 | """Fail if text does not match the regular expression. 536 | 537 | regex can be either a regular expression string or a compiled regular 538 | expression object. 539 | 540 | >>> assert_regex("Hello World!", r"llo.*rld!$") 541 | >>> assert_regex("Hello World!", r"\\d") 542 | Traceback (most recent call last): 543 | ... 544 | AssertionError: 'Hello World!' does not match '\\\\d' 545 | 546 | The following msg_fmt arguments are supported: 547 | * msg - the default error message 548 | * text - text that is matched 549 | * pattern - regular expression pattern as string 550 | """ 551 | 552 | compiled = re.compile(regex) 553 | if not compiled.search(text): 554 | msg = "{!r} does not match {!r}".format(text, compiled.pattern) 555 | fail(msg_fmt.format(msg=msg, text=text, pattern=compiled.pattern)) 556 | 557 | 558 | def assert_not_regex(text, regex, msg_fmt="{msg}"): 559 | """Fail if text does match the regular expression. 560 | 561 | regex can be either a regular expression string or a compiled regular 562 | expression object. 563 | 564 | >>> assert_regex("Hello World!", r"llo.*rld!$") 565 | >>> assert_regex("Hello World!", r"\\d") 566 | Traceback (most recent call last): 567 | ... 568 | AssertionError: 'Hello World!' does not match '\\\\d' 569 | 570 | The following msg_fmt arguments are supported: 571 | * msg - the default error message 572 | * text - text that is matched 573 | * pattern - regular expression pattern as string 574 | """ 575 | 576 | compiled = re.compile(regex) 577 | if compiled.search(text): 578 | msg = "{!r} matches {!r}".format(text, compiled.pattern) 579 | fail(msg_fmt.format(msg=msg, text=text, pattern=compiled.pattern)) 580 | 581 | 582 | def assert_is(first, second, msg_fmt="{msg}"): 583 | """Fail if first and second do not refer to the same object. 584 | 585 | >>> list1 = [5, "foo"] 586 | >>> list2 = [5, "foo"] 587 | >>> assert_is(list1, list1) 588 | >>> assert_is(list1, list2) 589 | Traceback (most recent call last): 590 | ... 591 | AssertionError: [5, 'foo'] is not [5, 'foo'] 592 | 593 | The following msg_fmt arguments are supported: 594 | * msg - the default error message 595 | * first - the first argument 596 | * second - the second argument 597 | """ 598 | 599 | if first is not second: 600 | msg = "{!r} is not {!r}".format(first, second) 601 | fail(msg_fmt.format(msg=msg, first=first, second=second)) 602 | 603 | 604 | def assert_is_not(first, second, msg_fmt="{msg}"): 605 | """Fail if first and second refer to the same object. 606 | 607 | >>> list1 = [5, "foo"] 608 | >>> list2 = [5, "foo"] 609 | >>> assert_is_not(list1, list2) 610 | >>> assert_is_not(list1, list1) 611 | Traceback (most recent call last): 612 | ... 613 | AssertionError: both arguments refer to [5, 'foo'] 614 | 615 | The following msg_fmt arguments are supported: 616 | * msg - the default error message 617 | * first - the first argument 618 | * second - the second argument 619 | """ 620 | 621 | if first is second: 622 | msg = "both arguments refer to {!r}".format(first) 623 | fail(msg_fmt.format(msg=msg, first=first, second=second)) 624 | 625 | 626 | def assert_in(first, second, msg_fmt="{msg}"): 627 | """Fail if first is not in collection second. 628 | 629 | >>> assert_in("foo", [4, "foo", {}]) 630 | >>> assert_in("bar", [4, "foo", {}]) 631 | Traceback (most recent call last): 632 | ... 633 | AssertionError: 'bar' not in [4, 'foo', {}] 634 | 635 | The following msg_fmt arguments are supported: 636 | * msg - the default error message 637 | * first - the element looked for 638 | * second - the container looked in 639 | """ 640 | 641 | if first not in second: 642 | msg = "{!r} not in {!r}".format(first, second) 643 | fail(msg_fmt.format(msg=msg, first=first, second=second)) 644 | 645 | 646 | def assert_not_in(first, second, msg_fmt="{msg}"): 647 | """Fail if first is in a collection second. 648 | 649 | >>> assert_not_in("bar", [4, "foo", {}]) 650 | >>> assert_not_in("foo", [4, "foo", {}]) 651 | Traceback (most recent call last): 652 | ... 653 | AssertionError: 'foo' is in [4, 'foo', {}] 654 | 655 | The following msg_fmt arguments are supported: 656 | * msg - the default error message 657 | * first - the element looked for 658 | * second - the container looked in 659 | """ 660 | if first in second: 661 | msg = "{!r} is in {!r}".format(first, second) 662 | fail(msg_fmt.format(msg=msg, first=first, second=second)) 663 | 664 | 665 | def assert_count_equal(sequence1, sequence2, msg_fmt="{msg}"): 666 | """Compare the items of two sequences, ignoring order. 667 | 668 | >>> assert_count_equal([1, 2], {2, 1}) 669 | 670 | Items missing in either sequence will be listed: 671 | 672 | >>> assert_count_equal(["a", "b", "c"], ["a", "d"]) 673 | Traceback (most recent call last): 674 | ... 675 | AssertionError: missing from sequence 1: 'd'; missing from sequence 2: 'b', 'c' 676 | 677 | Items are counted in each sequence. This makes it useful to detect 678 | duplicates: 679 | 680 | >>> assert_count_equal({"a", "b"}, ["a", "a", "b"]) 681 | Traceback (most recent call last): 682 | ... 683 | AssertionError: missing from sequence 1: 'a' 684 | 685 | The following msg_fmt arguments are supported: 686 | * msg - the default error message 687 | * first - first sequence 688 | * second - second sequence 689 | """ 690 | 691 | def compare(): 692 | missing1 = list(sequence2) 693 | missing2 = [] 694 | for item in sequence1: 695 | try: 696 | missing1.remove(item) 697 | except ValueError: 698 | missing2.append(item) 699 | return missing1, missing2 700 | 701 | def build_message(): 702 | msg = "" 703 | if missing_from_1: 704 | msg += "missing from sequence 1: " + ", ".join( 705 | repr(i) for i in missing_from_1 706 | ) 707 | if missing_from_1 and missing_from_2: 708 | msg += "; " 709 | if missing_from_2: 710 | msg += "missing from sequence 2: " + ", ".join( 711 | repr(i) for i in missing_from_2 712 | ) 713 | return msg 714 | 715 | missing_from_1, missing_from_2 = compare() 716 | if missing_from_1 or missing_from_2: 717 | fail( 718 | msg_fmt.format( 719 | msg=build_message(), first=sequence1, second=sequence2 720 | ) 721 | ) 722 | 723 | 724 | def assert_between(lower_bound, upper_bound, expr, msg_fmt="{msg}"): 725 | """Fail if an expression is not between certain bounds (inclusive). 726 | 727 | >>> assert_between(5, 15, 5) 728 | >>> assert_between(5, 15, 15) 729 | >>> assert_between(5, 15, 4.9) 730 | Traceback (most recent call last): 731 | ... 732 | AssertionError: 4.9 is not between 5 and 15 733 | 734 | The following msg_fmt arguments are supported: 735 | * msg - the default error message 736 | * lower - lower bound 737 | * upper - upper bound 738 | * expr - tested expression 739 | """ 740 | 741 | if not lower_bound <= expr <= upper_bound: 742 | msg = "{!r} is not between {} and {}".format( 743 | expr, lower_bound, upper_bound 744 | ) 745 | fail( 746 | msg_fmt.format( 747 | msg=msg, lower=lower_bound, upper=upper_bound, expr=expr 748 | ) 749 | ) 750 | 751 | 752 | def assert_is_instance(obj, cls, msg_fmt="{msg}"): 753 | """Fail if an object is not an instance of a class or tuple of classes. 754 | 755 | >>> assert_is_instance(5, int) 756 | >>> assert_is_instance('foo', (str, bytes)) 757 | >>> assert_is_instance(5, str) 758 | Traceback (most recent call last): 759 | ... 760 | AssertionError: 5 is an instance of , expected 761 | 762 | The following msg_fmt arguments are supported: 763 | * msg - the default error message 764 | * obj - object to test 765 | * types - tuple of types tested against 766 | """ 767 | if not isinstance(obj, cls): 768 | msg = "{!r} is an instance of {!r}, expected {!r}".format( 769 | obj, obj.__class__, cls 770 | ) 771 | types = cls if isinstance(cls, tuple) else (cls,) 772 | fail(msg_fmt.format(msg=msg, obj=obj, types=types)) 773 | 774 | 775 | def assert_not_is_instance(obj, cls, msg_fmt="{msg}"): 776 | """Fail if an object is an instance of a class or tuple of classes. 777 | 778 | >>> assert_not_is_instance(5, str) 779 | >>> assert_not_is_instance(5, (str, bytes)) 780 | >>> assert_not_is_instance('foo', str) 781 | Traceback (most recent call last): 782 | ... 783 | AssertionError: 'foo' is an instance of 784 | 785 | The following msg_fmt arguments are supported: 786 | * msg - the default error message 787 | * obj - object to test 788 | * types - tuple of types tested against 789 | """ 790 | if isinstance(obj, cls): 791 | msg = "{!r} is an instance of {!r}".format(obj, obj.__class__) 792 | types = cls if isinstance(cls, tuple) else (cls,) 793 | fail(msg_fmt.format(msg=msg, obj=obj, types=types)) 794 | 795 | 796 | def assert_has_attr(obj, attribute, msg_fmt="{msg}"): 797 | """Fail is an object does not have an attribute. 798 | 799 | >>> assert_has_attr([], "index") 800 | >>> assert_has_attr([], "i_do_not_have_this") 801 | Traceback (most recent call last): 802 | ... 803 | AssertionError: [] does not have attribute 'i_do_not_have_this' 804 | 805 | The following msg_fmt arguments are supported: 806 | * msg - the default error message 807 | * obj - object to test 808 | * attribute - name of the attribute to check 809 | """ 810 | 811 | if not hasattr(obj, attribute): 812 | msg = "{!r} does not have attribute '{}'".format(obj, attribute) 813 | fail(msg_fmt.format(msg=msg, obj=obj, attribute=attribute)) 814 | 815 | 816 | _EPSILON_SECONDS = 5 817 | 818 | 819 | def assert_datetime_about_now(actual, msg_fmt="{msg}"): 820 | """Fail if a datetime object is not within 5 seconds of the local time. 821 | 822 | >>> assert_datetime_about_now(datetime.now()) 823 | >>> assert_datetime_about_now(datetime(1900, 1, 1, 12, 0, 0)) 824 | Traceback (most recent call last): 825 | ... 826 | AssertionError: datetime.datetime(1900, 1, 1, 12, 0) is not close to current date/time 827 | 828 | The following msg_fmt arguments are supported: 829 | * msg - the default error message 830 | * actual - datetime object to check 831 | * now - current datetime that was tested against 832 | """ 833 | 834 | now = datetime.now() 835 | if actual is None: 836 | msg = "None is not a valid date/time" 837 | fail(msg_fmt.format(msg=msg, actual=actual, now=now)) 838 | lower_bound = now - timedelta(seconds=_EPSILON_SECONDS) 839 | upper_bound = now + timedelta(seconds=_EPSILON_SECONDS) 840 | if not lower_bound <= actual <= upper_bound: 841 | msg = "{!r} is not close to current date/time".format(actual) 842 | fail(msg_fmt.format(msg=msg, actual=actual, now=now)) 843 | 844 | 845 | def assert_datetime_about_now_utc(actual, msg_fmt="{msg}"): 846 | """Fail if a datetime object is not within 5 seconds of UTC. 847 | 848 | >>> assert_datetime_about_now_utc(datetime.now(timezone.utc).replace(tzinfo=None)) 849 | >>> assert_datetime_about_now_utc(datetime(1900, 1, 1, 12, 0, 0)) 850 | Traceback (most recent call last): 851 | ... 852 | AssertionError: datetime.datetime(1900, 1, 1, 12, 0) is not close to current UTC date/time 853 | 854 | The following msg_fmt arguments are supported: 855 | * msg - the default error message 856 | * actual - datetime object to check 857 | * now - current datetime that was tested against 858 | """ 859 | 860 | now = datetime.now(timezone.utc).replace(tzinfo=None) 861 | if actual is None: 862 | msg = "None is not a valid date/time" 863 | fail(msg_fmt.format(msg=msg, actual=actual, now=now)) 864 | lower_bound = now - timedelta(seconds=_EPSILON_SECONDS) 865 | upper_bound = now + timedelta(seconds=_EPSILON_SECONDS) 866 | if not lower_bound <= actual <= upper_bound: 867 | msg = "{!r} is not close to current UTC date/time".format(actual) 868 | fail(msg_fmt.format(msg=msg, actual=actual, now=now)) 869 | 870 | 871 | class AssertRaisesContext: 872 | """A context manager to test for exceptions with certain properties. 873 | 874 | When the context is left and no exception has been raised, an 875 | AssertionError will be raised: 876 | 877 | >>> context = AssertRaisesContext(TypeError) 878 | >>> with context: 879 | ... pass 880 | Traceback (most recent call last): 881 | ... 882 | AssertionError: TypeError not raised 883 | 884 | If an exception that is not a sub-class of the exception class provided 885 | to the constructor is raised, it will be passed on: 886 | 887 | >>> with context: 888 | ... raise ValueError("Wrong Class") 889 | Traceback (most recent call last): 890 | ... 891 | ValueError: Wrong Class 892 | 893 | If the exception has the right class, any additional tests that have been 894 | configured on the context, will be called: 895 | 896 | >>> def test(exc): 897 | ... assert_equal("Hello World!", str(exc)) 898 | >>> context.add_test(test) 899 | >>> with context: 900 | ... raise TypeError("Wrong Message") 901 | Traceback (most recent call last): 902 | ... 903 | AssertionError: 'Hello World!' != 'Wrong Message' 904 | 905 | """ 906 | 907 | def __init__(self, exception, msg_fmt="{msg}"): 908 | self.exception = exception 909 | self.msg_fmt = msg_fmt 910 | self._exc_type = exception 911 | self._exc_val = None 912 | self._exception_name = getattr(exception, "__name__", str(exception)) 913 | self._tests: list[Callable[[Any], object]] = [] 914 | 915 | def __enter__(self): 916 | return self 917 | 918 | def __exit__(self, exc_type, exc_val, exc_tb): 919 | if not exc_type or not exc_val: 920 | msg = "{} not raised".format(self._exception_name) 921 | fail(self.format_message(msg)) 922 | self._exc_val = exc_val 923 | if not issubclass(exc_type, self.exception): 924 | return False 925 | for test in self._tests: 926 | test(exc_val) 927 | return True 928 | 929 | def format_message(self, default_msg): 930 | return self.msg_fmt.format( 931 | msg=default_msg, 932 | exc_type=self._exc_type, 933 | exc_name=self._exception_name, 934 | ) 935 | 936 | def add_test(self, cb: Callable[[Any], object]) -> None: 937 | """Add a test callback. 938 | 939 | This callback is called after determining that the right exception 940 | class was raised. The callback will get the raised exception as only 941 | argument. 942 | 943 | """ 944 | self._tests.append(cb) 945 | 946 | @property 947 | def exc_val(self): 948 | if self._exc_val is None: 949 | raise RuntimeError("must be called after leaving the context") 950 | return self._exc_val 951 | 952 | 953 | class AssertRaisesRegexContext(AssertRaisesContext): 954 | """A context manager to test for exceptions and their messages.""" 955 | 956 | def __init__(self, exception, pattern, msg_fmt="{msg}"): 957 | super(AssertRaisesRegexContext, self).__init__(exception, msg_fmt) 958 | self.pattern = pattern 959 | 960 | def format_message(self, default_msg): 961 | return self.msg_fmt.format( 962 | msg=default_msg, 963 | exc_type=self._exc_type, 964 | exc_name=self._exception_name, 965 | pattern=self.pattern, 966 | text="", 967 | ) 968 | 969 | 970 | class AssertRaisesErrnoContext(AssertRaisesContext): 971 | """A context manager to test for exceptions with errnos.""" 972 | 973 | def __init__(self, exception, expected_errno, msg_fmt="{msg}"): 974 | super(AssertRaisesErrnoContext, self).__init__(exception, msg_fmt) 975 | self.expected_errno = expected_errno 976 | 977 | def format_message(self, default_msg): 978 | return self.msg_fmt.format( 979 | msg=default_msg, 980 | exc_type=self._exc_type, 981 | exc_name=self._exception_name, 982 | expected_errno=self.expected_errno, 983 | actual_errno=None, 984 | ) 985 | 986 | 987 | def assert_raises(exception, msg_fmt="{msg}"): 988 | """Fail unless a specific exception is raised inside the context. 989 | 990 | If a different type of exception is raised, it will not be caught. 991 | 992 | >>> with assert_raises(TypeError): 993 | ... raise TypeError() 994 | ... 995 | >>> with assert_raises(TypeError): 996 | ... pass 997 | ... 998 | Traceback (most recent call last): 999 | ... 1000 | AssertionError: TypeError not raised 1001 | >>> with assert_raises(TypeError): 1002 | ... raise ValueError("wrong error") 1003 | ... 1004 | Traceback (most recent call last): 1005 | ... 1006 | ValueError: wrong error 1007 | 1008 | The following msg_fmt arguments are supported: 1009 | * msg - the default error message 1010 | * exc_type - exception type that is expected 1011 | * exc_name - expected exception type name 1012 | """ 1013 | 1014 | return AssertRaisesContext(exception, msg_fmt) 1015 | 1016 | 1017 | def assert_raises_regex(exception, regex, msg_fmt="{msg}"): 1018 | """Fail unless an exception with a message that matches a regular 1019 | expression is raised within the context. 1020 | 1021 | The regular expression can be a regular expression string or object. 1022 | 1023 | >>> with assert_raises_regex(ValueError, r"\\d+"): 1024 | ... raise ValueError("Error #42") 1025 | ... 1026 | >>> with assert_raises_regex(ValueError, r"\\d+"): 1027 | ... raise ValueError("Generic Error") 1028 | ... 1029 | Traceback (most recent call last): 1030 | ... 1031 | AssertionError: 'Generic Error' does not match '\\\\d+' 1032 | 1033 | The following msg_fmt arguments are supported: 1034 | * msg - the default error message 1035 | * exc_type - exception type that is expected 1036 | * exc_name - expected exception type name 1037 | * text - actual error text 1038 | * pattern - expected error message as regular expression string 1039 | """ 1040 | 1041 | def test(exc): 1042 | compiled = re.compile(regex) 1043 | if not exc.args: 1044 | msg = "{} without message".format(exception.__name__) 1045 | fail( 1046 | msg_fmt.format( 1047 | msg=msg, 1048 | text=None, 1049 | pattern=compiled.pattern, 1050 | exc_type=exception, 1051 | exc_name=exception.__name__, 1052 | ) 1053 | ) 1054 | text = exc.args[0] 1055 | if not compiled.search(text): 1056 | msg = "{!r} does not match {!r}".format(text, compiled.pattern) 1057 | fail( 1058 | msg_fmt.format( 1059 | msg=msg, 1060 | text=text, 1061 | pattern=compiled.pattern, 1062 | exc_type=exception, 1063 | exc_name=exception.__name__, 1064 | ) 1065 | ) 1066 | 1067 | context = AssertRaisesRegexContext(exception, regex, msg_fmt) 1068 | context.add_test(test) 1069 | return context 1070 | 1071 | 1072 | def assert_raises_errno(exception, errno, msg_fmt="{msg}"): 1073 | """Fail unless an exception with a specific errno is raised with the 1074 | context. 1075 | 1076 | >>> with assert_raises_errno(OSError, 42): 1077 | ... raise OSError(42, "OS Error") 1078 | ... 1079 | >>> with assert_raises_errno(OSError, 44): 1080 | ... raise OSError(17, "OS Error") 1081 | ... 1082 | Traceback (most recent call last): 1083 | ... 1084 | AssertionError: wrong errno: 44 != 17 1085 | 1086 | The following msg_fmt arguments are supported: 1087 | * msg - the default error message 1088 | * exc_type - exception type that is expected 1089 | * exc_name - expected exception type name 1090 | * expected_errno - 1091 | * actual_errno - raised errno or None if no matching exception was raised 1092 | """ 1093 | 1094 | def check_errno(exc): 1095 | if errno != exc.errno: 1096 | msg = "wrong errno: {!r} != {!r}".format(errno, exc.errno) 1097 | fail( 1098 | msg_fmt.format( 1099 | msg=msg, 1100 | exc_type=exception, 1101 | exc_name=exception.__name__, 1102 | expected_errno=errno, 1103 | actual_errno=exc.errno, 1104 | ) 1105 | ) 1106 | 1107 | context = AssertRaisesErrnoContext(exception, errno, msg_fmt) 1108 | context.add_test(check_errno) 1109 | return context 1110 | 1111 | 1112 | def assert_succeeds(exception, msg_fmt="{msg}"): 1113 | """Fail if a specific exception is raised within the context. 1114 | 1115 | This assertion should be used for cases, where successfully running a 1116 | function signals a successful test, and raising the exception of a 1117 | certain type signals a test failure. All other raised exceptions are 1118 | passed on and will usually still result in a test error. This can be 1119 | used to signal the intent of a block. 1120 | 1121 | >>> l = ["foo", "bar"] 1122 | >>> with assert_succeeds(ValueError): 1123 | ... i = l.index("foo") 1124 | ... 1125 | >>> with assert_succeeds(ValueError): 1126 | ... raise ValueError() 1127 | ... 1128 | Traceback (most recent call last): 1129 | ... 1130 | AssertionError: ValueError was unexpectedly raised 1131 | >>> with assert_succeeds(ValueError): 1132 | ... raise TypeError("Wrong Error") 1133 | ... 1134 | Traceback (most recent call last): 1135 | ... 1136 | TypeError: Wrong Error 1137 | 1138 | The following msg_fmt arguments are supported: 1139 | * msg - the default error message 1140 | * exc_type - exception type 1141 | * exc_name - exception type name 1142 | * exception - exception that was raised 1143 | """ 1144 | 1145 | class _AssertSucceeds(object): 1146 | def __enter__(self): 1147 | pass 1148 | 1149 | def __exit__(self, exc_type, exc_val, exc_tb): 1150 | if exc_type and issubclass(exc_type, exception): 1151 | msg = exception.__name__ + " was unexpectedly raised" 1152 | fail( 1153 | msg_fmt.format( 1154 | msg=msg, 1155 | exc_type=exception, 1156 | exc_name=exception.__name__, 1157 | exception=exc_val, 1158 | ) 1159 | ) 1160 | 1161 | return _AssertSucceeds() 1162 | 1163 | 1164 | class AssertWarnsContext(object): 1165 | """A context manager to test for warnings with certain properties. 1166 | 1167 | When the context is left and the expected warning has not been raised, an 1168 | AssertionError will be raised: 1169 | 1170 | >>> context = AssertWarnsContext(DeprecationWarning) 1171 | >>> with context: 1172 | ... pass 1173 | Traceback (most recent call last): 1174 | ... 1175 | AssertionError: DeprecationWarning not issued 1176 | 1177 | If the warning has the right class, any additional tests that have been 1178 | configured on the context, will be called: 1179 | 1180 | >>> from warnings import warn 1181 | >>> def test(warning): 1182 | ... return False 1183 | >>> context.add_test(test) 1184 | >>> with context: 1185 | ... warn("Wrong Message", DeprecationWarning) 1186 | Traceback (most recent call last): 1187 | ... 1188 | AssertionError: DeprecationWarning not issued 1189 | 1190 | """ 1191 | 1192 | def __init__(self, warning_class, msg_fmt="{msg}"): 1193 | self._warning_class = warning_class 1194 | self._msg_fmt = msg_fmt 1195 | self._warning_context: catch_warnings[list[WarningMessage]] | None = ( 1196 | None 1197 | ) 1198 | self._warnings = [] 1199 | self._tests: list[Callable[[Warning], bool]] = [] 1200 | 1201 | def __enter__(self): 1202 | self._warning_context = catch_warnings(record=True) 1203 | self._warnings = self._warning_context.__enter__() 1204 | return self 1205 | 1206 | def __exit__(self, exc_type, exc_val, exc_tb): 1207 | assert self._warning_context is not None 1208 | self._warning_context.__exit__(exc_type, exc_val, exc_tb) 1209 | if not any(self._is_expected_warning(w) for w in self._warnings): 1210 | fail(self.format_message()) 1211 | 1212 | def format_message(self): 1213 | msg = "{} not issued".format(self._warning_class.__name__) 1214 | return self._msg_fmt.format( 1215 | msg=msg, 1216 | exc_type=self._warning_class, 1217 | exc_name=self._warning_class.__name__, 1218 | ) 1219 | 1220 | def _is_expected_warning(self, warning) -> bool: 1221 | if not issubclass(warning.category, self._warning_class): 1222 | return False 1223 | return all(test(warning) for test in self._tests) 1224 | 1225 | def add_test(self, cb: Callable[[Warning], bool]) -> None: 1226 | """Add a test callback. 1227 | 1228 | This callback is called after determining that the right warning 1229 | class was issued. The callback will get the issued warning as only 1230 | argument and must return a boolean value. 1231 | 1232 | """ 1233 | self._tests.append(cb) 1234 | 1235 | 1236 | class AssertWarnsRegexContext(AssertWarnsContext): 1237 | """A context manager to test for warnings and their messages.""" 1238 | 1239 | def __init__(self, warning_class, pattern, msg_fmt="{msg}"): 1240 | super(AssertWarnsRegexContext, self).__init__(warning_class, msg_fmt) 1241 | self.pattern = pattern 1242 | 1243 | def format_message(self): 1244 | msg = "no {} matching {} issued".format( 1245 | self._warning_class.__name__, repr(self.pattern) 1246 | ) 1247 | return self._msg_fmt.format( 1248 | msg=msg, 1249 | exc_type=self._warning_class, 1250 | exc_name=self._warning_class.__name__, 1251 | pattern=self.pattern, 1252 | ) 1253 | 1254 | 1255 | def assert_warns(warning_type, msg_fmt="{msg}"): 1256 | """Fail unless a specific warning is issued inside the context. 1257 | 1258 | If a different type of warning is issued, it will not be caught. 1259 | 1260 | >>> from warnings import warn 1261 | >>> with assert_warns(UserWarning): 1262 | ... warn("warning message", UserWarning) 1263 | ... 1264 | >>> with assert_warns(UserWarning): 1265 | ... pass 1266 | ... 1267 | Traceback (most recent call last): 1268 | ... 1269 | AssertionError: UserWarning not issued 1270 | >>> with assert_warns(UserWarning): 1271 | ... warn("warning message", UnicodeWarning) 1272 | ... 1273 | Traceback (most recent call last): 1274 | ... 1275 | AssertionError: UserWarning not issued 1276 | 1277 | The following msg_fmt arguments are supported: 1278 | * msg - the default error message 1279 | * exc_type - exception type 1280 | * exc_name - exception type name 1281 | """ 1282 | return AssertWarnsContext(warning_type, msg_fmt) 1283 | 1284 | 1285 | def assert_warns_regex(warning_type, regex, msg_fmt="{msg}"): 1286 | """Fail unless a warning with a message is issued inside the context. 1287 | 1288 | The message can be a regular expression string or object. 1289 | 1290 | >>> from warnings import warn 1291 | >>> with assert_warns_regex(UserWarning, r"#\\d+"): 1292 | ... warn("Error #42", UserWarning) 1293 | ... 1294 | >>> with assert_warns_regex(UserWarning, r"Expected Error"): 1295 | ... warn("Generic Error", UserWarning) 1296 | ... 1297 | Traceback (most recent call last): 1298 | ... 1299 | AssertionError: no UserWarning matching 'Expected Error' issued 1300 | 1301 | The following msg_fmt arguments are supported: 1302 | * msg - the default error message 1303 | * exc_type - warning type 1304 | * exc_name - warning type name 1305 | * pattern - expected warning message as regular expression string 1306 | """ 1307 | 1308 | def test(warning): 1309 | return re.search(regex, str(warning.message)) is not None 1310 | 1311 | context = AssertWarnsRegexContext(warning_type, regex, msg_fmt) 1312 | context.add_test(test) 1313 | return context 1314 | 1315 | 1316 | if sys.version_info >= (3,): 1317 | _Str = str 1318 | else: 1319 | _Str = unicode # noqa: F821 1320 | 1321 | 1322 | def assert_json_subset(first, second): 1323 | """Assert that a JSON object or array is a subset of another JSON object 1324 | or array. 1325 | 1326 | The first JSON object or array must be supplied as a JSON-compatible 1327 | dict or list, the JSON object or array to check must be a string, an 1328 | UTF-8 bytes object, or a JSON-compatible list or dict. 1329 | 1330 | A JSON non-object, non-array value is the subset of another JSON value, 1331 | if they are equal. 1332 | 1333 | A JSON object is the subset of another JSON object if for each name/value 1334 | pair in the former there is a name/value pair in the latter with the same 1335 | name. Additionally, the value of the former pair must be a subset of the 1336 | value of the latter pair. 1337 | 1338 | A JSON array is the subset of another JSON array, if they have the same 1339 | number of elements and each element in the former is a subset of the 1340 | corresponding element in the latter. 1341 | 1342 | >>> assert_json_subset({}, '{}') 1343 | >>> assert_json_subset({}, '{"foo": "bar"}') 1344 | >>> assert_json_subset({"foo": "bar"}, '{}') 1345 | Traceback (most recent call last): 1346 | ... 1347 | AssertionError: element 'foo' missing from element $ 1348 | >>> assert_json_subset([1, 2], '[1, 2]') 1349 | >>> assert_json_subset([2, 1], '[1, 2]') 1350 | Traceback (most recent call last): 1351 | ... 1352 | AssertionError: element $[0] differs: 2 != 1 1353 | >>> assert_json_subset([{}], '[{"foo": "bar"}]') 1354 | >>> assert_json_subset({}, "INVALID JSON") 1355 | Traceback (most recent call last): 1356 | ... 1357 | json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0) 1358 | 1359 | In objects, the special classes `Present` and `Absent` can be used to 1360 | check for the presence or absence of a specific key: 1361 | 1362 | >>> assert_json_subset({Exists("foo"): True}, '{"foo": "bar"}') 1363 | >>> assert_json_subset({Exists("foo"): True}, '{}') 1364 | Traceback (most recent call last): 1365 | ... 1366 | AssertionError: element 'foo' missing from element $ 1367 | >>> assert_json_subset({Exists("foo"): False}, '{}') 1368 | >>> assert_json_subset({Exists("foo"): False}, '{"foo": "bar"}') 1369 | Traceback (most recent call last): 1370 | ... 1371 | AssertionError: spurious member 'foo' in object $ 1372 | """ 1373 | 1374 | if not isinstance(second, (dict, list, str, bytes)): 1375 | raise TypeError("second must be dict, list, str, or bytes") 1376 | if isinstance(second, bytes): 1377 | second = second.decode("utf-8") 1378 | if isinstance(second, _Str): 1379 | parsed_second = json_loads(second) 1380 | else: 1381 | parsed_second = second 1382 | 1383 | if not isinstance(parsed_second, (dict, list)): 1384 | raise AssertionError( 1385 | "second must decode to dict or list, not {}".format( 1386 | type(parsed_second) 1387 | ) 1388 | ) 1389 | 1390 | comparer = _JSONComparer(_JSONPath("$"), first, parsed_second) 1391 | comparer.assert_() 1392 | 1393 | 1394 | class _JSONComparer: 1395 | def __init__(self, path, expected, actual): 1396 | self._path = path 1397 | self._expected = expected 1398 | self._actual = actual 1399 | 1400 | def assert_(self): 1401 | self._assert_types_are_equal() 1402 | if isinstance(self._expected, dict): 1403 | self._assert_dicts_equal() 1404 | elif isinstance(self._expected, list): 1405 | self._assert_arrays_equal() 1406 | elif _is_present(self._expected): 1407 | pass 1408 | else: 1409 | self._assert_fundamental_values_equal() 1410 | 1411 | def _assert_types_are_equal(self): 1412 | if self._types_differ(): 1413 | self._raise_different_values() 1414 | 1415 | def _types_differ(self): 1416 | if self._expected is None: 1417 | return self._actual is not None 1418 | elif isinstance(self._expected, (int, float)): 1419 | return not isinstance(self._actual, (int, float)) 1420 | elif _is_present(self._expected): 1421 | return False 1422 | for type_ in [bool, str, _Str, list, dict]: 1423 | if isinstance(self._expected, type_): 1424 | return not isinstance(self._actual, type_) 1425 | else: 1426 | raise TypeError("unsupported type {}".format(type(self._expected))) 1427 | 1428 | def _assert_dicts_equal(self) -> None: 1429 | for name in self._expected: 1430 | if not isinstance(name, (str, Exists)): 1431 | raise TypeError( 1432 | f"{repr(name)} is not a valid object member name", 1433 | ) 1434 | self._assert_all_expected_keys_in_actual_dict() 1435 | self._assert_no_wrong_keys() 1436 | self._assert_dict_values_equal() 1437 | 1438 | def _assert_all_expected_keys_in_actual_dict(self) -> None: 1439 | keys = self._expected_key_names.difference(self._actual.keys()) 1440 | if keys: 1441 | self._raise_missing_element(keys) 1442 | 1443 | def _assert_no_wrong_keys(self) -> None: 1444 | for name in self._expected: 1445 | if isinstance(name, str) and _is_absent(self._expected[name]): 1446 | if name in self._actual: 1447 | self._raise_assertion_error( 1448 | f"spurious member '{name}' in object {{path}}" 1449 | ) 1450 | if isinstance(name, Exists) and not self._expected[name]: 1451 | if name.member_name in self._actual: 1452 | self._raise_assertion_error( 1453 | f"spurious member '{name.member_name}' in object {{path}}" 1454 | ) 1455 | 1456 | def _assert_dict_values_equal(self) -> None: 1457 | for name in self._expected: 1458 | if isinstance(name, str) and not _is_absent(self._expected[name]): 1459 | self._assert_json_value_equals_with_item(name) 1460 | 1461 | @property 1462 | def _expected_key_names(self) -> Set[str]: 1463 | keys: Set[str] = set() 1464 | for k in self._expected.keys(): 1465 | if isinstance(k, str): 1466 | if not _is_absent(self._expected[k]): 1467 | keys.add(k) 1468 | elif isinstance(k, Exists) and self._expected[k]: 1469 | keys.add(k.member_name) 1470 | return keys 1471 | 1472 | def _assert_arrays_equal(self): 1473 | if len(self._expected) != len(self._actual): 1474 | self._raise_different_sizes() 1475 | for i in range(len(self._expected)): 1476 | self._assert_json_value_equals_with_item(i) 1477 | 1478 | def _assert_json_value_equals_with_item(self, item): 1479 | path = self._path.append(item) 1480 | expected = self._expected[item] 1481 | actual = self._actual[item] 1482 | _JSONComparer(path, expected, actual).assert_() 1483 | 1484 | def _assert_fundamental_values_equal(self): 1485 | if self._expected != self._actual: 1486 | self._raise_different_values() 1487 | 1488 | def _raise_different_values(self): 1489 | self._raise_assertion_error( 1490 | "element {path} differs: {expected} != {actual}" 1491 | ) 1492 | 1493 | def _raise_different_sizes(self): 1494 | self._raise_assertion_error( 1495 | "JSON array {path} differs in size: " 1496 | "{expected_len} != {actual_len}", 1497 | expected_len=len(self._expected), 1498 | actual_len=len(self._actual), 1499 | ) 1500 | 1501 | def _raise_missing_element(self, keys): 1502 | if len(keys) == 1: 1503 | format_string = "element {elements} missing from element {path}" 1504 | elements = repr(next(iter(keys))) 1505 | else: 1506 | format_string = "elements {elements} missing from element {path}" 1507 | sorted_keys = sorted(keys) 1508 | elements = ( 1509 | ", ".join(repr(k) for k in sorted_keys[:-1]) 1510 | + ", and " 1511 | + repr(sorted_keys[-1]) 1512 | ) 1513 | self._raise_assertion_error(format_string, elements=elements) 1514 | 1515 | def _raise_assertion_error(self, format_, **kwargs): 1516 | kwargs.update( 1517 | { 1518 | "path": self._path, 1519 | "expected": repr(self._expected), 1520 | "actual": repr(self._actual), 1521 | } 1522 | ) 1523 | raise AssertionError(format_.format(**kwargs)) 1524 | 1525 | 1526 | class _JSONPath: 1527 | def __init__(self, path): 1528 | self._path = path 1529 | 1530 | def __str__(self): 1531 | return self._path 1532 | 1533 | def append(self, item): 1534 | return _JSONPath("{0}[{1}]".format(self._path, repr(item))) 1535 | 1536 | 1537 | class Present: 1538 | """Helper class for presence checks in assert_json_subset().""" 1539 | 1540 | 1541 | def _is_present(o: object) -> bool: 1542 | return o is Present or isinstance(o, Present) 1543 | 1544 | 1545 | class Absent: 1546 | """Helper class for absence checks in assert_json_subset().""" 1547 | 1548 | 1549 | def _is_absent(o: object) -> bool: 1550 | return o is Absent or isinstance(o, Absent) 1551 | 1552 | 1553 | @deprecated("Use Present and Absent instead.") 1554 | class Exists: 1555 | """Helper class for existence checks in assert_json_subset().""" 1556 | 1557 | def __init__(self, member_name: str) -> None: 1558 | self.member_name = member_name 1559 | -------------------------------------------------------------------------------- /asserts/__init__.pyi: -------------------------------------------------------------------------------- 1 | import datetime 2 | from collections.abc import Callable, Container, Iterable 3 | from contextlib import AbstractContextManager as ContextManager 4 | from re import Pattern 5 | from types import TracebackType 6 | from typing import Any, Generic, NoReturn, TypeVar 7 | 8 | from typing_extensions import deprecated 9 | 10 | _E = TypeVar("_E", bound=BaseException) 11 | _S = TypeVar("_S") 12 | 13 | class AssertRaisesContext(Generic[_E]): 14 | exception: type[_E] 15 | msg_fmt: str 16 | def __init__(self, exception: type[_E], msg_fmt: str = ...) -> None: ... 17 | def __enter__(self: _S) -> _S: ... 18 | def __exit__( 19 | self, 20 | exc_type: type[BaseException] | None, 21 | exc_val: BaseException | None, 22 | exc_tb: TracebackType | None, 23 | ) -> bool: ... 24 | def format_message(self, default_msg: str) -> str: ... 25 | def add_test(self, cb: Callable[[_E], object]) -> None: ... 26 | @property 27 | def exc_val(self) -> _E: ... 28 | 29 | class AssertRaisesErrnoContext(AssertRaisesContext[_E]): 30 | expected_errno: int 31 | def __init__( 32 | self, exception: type[_E], expected_errno: int, msg_fmt: str = ... 33 | ) -> None: ... 34 | 35 | class AssertRaisesRegexContext(AssertRaisesContext[_E]): 36 | pattern: str 37 | def __init__( 38 | self, exception: type[_E], pattern: str, msg_fmt: str = ... 39 | ) -> None: ... 40 | 41 | class AssertWarnsContext: 42 | def __init__( 43 | self, warning_class: type[Warning], msg_fmt: str = ... 44 | ) -> None: ... 45 | def __enter__(self: _S) -> _S: ... 46 | def __exit__( 47 | self, 48 | exc_type: type[BaseException] | None, 49 | exc_val: BaseException | None, 50 | exc_tb: TracebackType | None, 51 | ) -> None: ... 52 | def format_message(self) -> str: ... 53 | def add_test(self, cb: Callable[[Warning], bool]) -> None: ... 54 | 55 | class AssertWarnsRegexContext(AssertWarnsContext): 56 | pattern: str 57 | def __init__( 58 | self, warning_class: type[Warning], msg_fmt: str = ... 59 | ) -> None: ... 60 | 61 | def fail(msg: str = ...) -> NoReturn: ... 62 | def assert_true(expr: object, msg_fmt: str = ...) -> None: ... 63 | def assert_false(expr: object, msg_fmt: str = ...) -> None: ... 64 | def assert_boolean_true(expr: object, msg_fmt: str = ...) -> None: ... 65 | def assert_boolean_false(expr: object, msg_fmt: str = ...) -> None: ... 66 | def assert_is_none(expr: object, msg_fmt: str = ...) -> None: ... 67 | def assert_is_not_none(expr: object, msg_fmt: str = ...) -> None: ... 68 | def assert_equal( 69 | first: object, second: object, msg_fmt: str = ... 70 | ) -> None: ... 71 | def assert_not_equal( 72 | first: object, second: object, msg_fmt: str = ... 73 | ) -> None: ... 74 | def assert_almost_equal( 75 | first: float, 76 | second: float, 77 | msg_fmt: str = ..., 78 | places: int = ..., 79 | delta: float = ..., 80 | ) -> None: ... 81 | def assert_not_almost_equal( 82 | first: float, 83 | second: float, 84 | msg_fmt: str = ..., 85 | places: int = ..., 86 | delta: float = ..., 87 | ) -> None: ... 88 | def assert_dict_equal( 89 | first: dict, 90 | second: dict, 91 | key_msg_fmt: str = ..., 92 | value_msg_fmt: str = ..., 93 | ) -> None: ... 94 | def assert_dict_superset( 95 | first: dict, 96 | second: dict, 97 | key_msg_fmt: str = ..., 98 | value_msg_fmt: str = ..., 99 | ) -> None: ... 100 | def assert_less(first: Any, second: Any, msg_fmt: str = ...) -> None: ... 101 | def assert_less_equal(first: Any, second: Any, msg_fmt: str = ...) -> None: ... 102 | def assert_greater(first: Any, second: Any, msg_fmt: str = ...) -> None: ... 103 | def assert_greater_equal( 104 | first: Any, second: Any, msg_fmt: str = ... 105 | ) -> None: ... 106 | def assert_regex( 107 | text: str, regex: str | Pattern[str], msg_fmt: str = ... 108 | ) -> None: ... 109 | def assert_not_regex( 110 | text: str, regex: str | Pattern[str], msg_fmt: str = ... 111 | ) -> None: ... 112 | def assert_is(first: object, second: object, msg_fmt: str = ...) -> None: ... 113 | def assert_is_not( 114 | first: object, second: object, msg_fmt: str = ... 115 | ) -> None: ... 116 | def assert_in( 117 | first: Any, second: Container[Any], msg_fmt: str = ... 118 | ) -> None: ... 119 | def assert_not_in( 120 | first: Any, second: Container[Any], msg_fmt: str = ... 121 | ) -> None: ... 122 | def assert_between( 123 | lower_bound: Any, upper_bound: Any, expr: Any, msg_fmt: str = ... 124 | ) -> None: ... 125 | def assert_is_instance( 126 | obj: object, cls: type | tuple[type, ...], msg_fmt: str = ... 127 | ) -> None: ... 128 | def assert_not_is_instance( 129 | obj: object, cls: type | tuple[type, ...], msg_fmt: str = ... 130 | ) -> None: ... 131 | def assert_count_equal( 132 | sequence1: Iterable[Any], sequence2: Iterable[Any], msg_fmt: str = ... 133 | ) -> None: ... 134 | def assert_has_attr( 135 | obj: object, attribute: str, msg_fmt: str = ... 136 | ) -> None: ... 137 | def assert_datetime_about_now( 138 | actual: datetime.datetime | None, msg_fmt: str = ... 139 | ) -> None: ... 140 | def assert_datetime_about_now_utc( 141 | actual: datetime.datetime | None, msg_fmt: str = ... 142 | ) -> None: ... 143 | def assert_raises( 144 | exception: type[BaseException], msg_fmt: str = ... 145 | ) -> AssertRaisesContext: ... 146 | def assert_raises_regex( 147 | exception: type[BaseException], 148 | regex: str | Pattern[str], 149 | msg_fmt: str = ..., 150 | ) -> AssertRaisesContext: ... 151 | def assert_raises_errno( 152 | exception: type[BaseException], errno: int, msg_fmt: str = ... 153 | ) -> AssertRaisesContext: ... 154 | def assert_succeeds( 155 | exception: type[BaseException], msg_fmt: str = ... 156 | ) -> ContextManager: ... 157 | def assert_warns( 158 | warning_type: type[Warning], msg_fmt: str = ... 159 | ) -> AssertWarnsContext: ... 160 | def assert_warns_regex( 161 | warning_type: type[Warning], regex: str, msg_fmt: str = ... 162 | ) -> AssertWarnsContext: ... 163 | def assert_json_subset( 164 | first: dict[Any, Any] | list[Any], # dict key can be 'str' or 'Exists' 165 | second: dict[str, Any] | list[Any] | str | bytes, 166 | ) -> None: ... 167 | 168 | class Present: ... 169 | class Absent: ... 170 | 171 | @deprecated("Use Present and Absent instead.") 172 | class Exists: 173 | member_name: str 174 | def __init__(self, member_name: str) -> None: ... 175 | -------------------------------------------------------------------------------- /asserts/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/srittau/python-asserts/e81ed77e887c6c14bb5dee98d91d06be0796c9fc/asserts/py.typed -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | check_untyped_defs = True 3 | disallow_subclassing_any = True 4 | disallow_untyped_decorators = True 5 | no_implicit_optional = True 6 | no_implicit_reexport = True 7 | warn_redundant_casts = True 8 | warn_return_any = True 9 | warn_unused_configs = True 10 | warn_unused_ignores = True 11 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "mypy" 5 | version = "1.14.1" 6 | description = "Optional static typing for Python" 7 | optional = false 8 | python-versions = ">=3.8" 9 | groups = ["dev"] 10 | files = [ 11 | {file = "mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb"}, 12 | {file = "mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0"}, 13 | {file = "mypy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d"}, 14 | {file = "mypy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b"}, 15 | {file = "mypy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427"}, 16 | {file = "mypy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f"}, 17 | {file = "mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c"}, 18 | {file = "mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1"}, 19 | {file = "mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8"}, 20 | {file = "mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f"}, 21 | {file = "mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1"}, 22 | {file = "mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae"}, 23 | {file = "mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14"}, 24 | {file = "mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9"}, 25 | {file = "mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11"}, 26 | {file = "mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e"}, 27 | {file = "mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89"}, 28 | {file = "mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b"}, 29 | {file = "mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255"}, 30 | {file = "mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34"}, 31 | {file = "mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a"}, 32 | {file = "mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9"}, 33 | {file = "mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd"}, 34 | {file = "mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107"}, 35 | {file = "mypy-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7084fb8f1128c76cd9cf68fe5971b37072598e7c31b2f9f95586b65c741a9d31"}, 36 | {file = "mypy-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8f845a00b4f420f693f870eaee5f3e2692fa84cc8514496114649cfa8fd5e2c6"}, 37 | {file = "mypy-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44bf464499f0e3a2d14d58b54674dee25c031703b2ffc35064bd0df2e0fac319"}, 38 | {file = "mypy-1.14.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c99f27732c0b7dc847adb21c9d47ce57eb48fa33a17bc6d7d5c5e9f9e7ae5bac"}, 39 | {file = "mypy-1.14.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:bce23c7377b43602baa0bd22ea3265c49b9ff0b76eb315d6c34721af4cdf1d9b"}, 40 | {file = "mypy-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:8edc07eeade7ebc771ff9cf6b211b9a7d93687ff892150cb5692e4f4272b0837"}, 41 | {file = "mypy-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3888a1816d69f7ab92092f785a462944b3ca16d7c470d564165fe703b0970c35"}, 42 | {file = "mypy-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46c756a444117c43ee984bd055db99e498bc613a70bbbc120272bd13ca579fbc"}, 43 | {file = "mypy-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27fc248022907e72abfd8e22ab1f10e903915ff69961174784a3900a8cba9ad9"}, 44 | {file = "mypy-1.14.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:499d6a72fb7e5de92218db961f1a66d5f11783f9ae549d214617edab5d4dbdbb"}, 45 | {file = "mypy-1.14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57961db9795eb566dc1d1b4e9139ebc4c6b0cb6e7254ecde69d1552bf7613f60"}, 46 | {file = "mypy-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:07ba89fdcc9451f2ebb02853deb6aaaa3d2239a236669a63ab3801bbf923ef5c"}, 47 | {file = "mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1"}, 48 | {file = "mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6"}, 49 | ] 50 | 51 | [package.dependencies] 52 | mypy_extensions = ">=1.0.0" 53 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 54 | typing_extensions = ">=4.6.0" 55 | 56 | [package.extras] 57 | dmypy = ["psutil (>=4.0)"] 58 | faster-cache = ["orjson"] 59 | install-types = ["pip"] 60 | mypyc = ["setuptools (>=50)"] 61 | reports = ["lxml"] 62 | 63 | [[package]] 64 | name = "mypy-extensions" 65 | version = "1.0.0" 66 | description = "Type system extensions for programs checked with the mypy type checker." 67 | optional = false 68 | python-versions = ">=3.5" 69 | groups = ["dev"] 70 | files = [ 71 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 72 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 73 | ] 74 | 75 | [[package]] 76 | name = "pastel" 77 | version = "0.2.1" 78 | description = "Bring colors to your terminal." 79 | optional = false 80 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 81 | groups = ["dev"] 82 | files = [ 83 | {file = "pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364"}, 84 | {file = "pastel-0.2.1.tar.gz", hash = "sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d"}, 85 | ] 86 | 87 | [[package]] 88 | name = "poethepoet" 89 | version = "0.30.0" 90 | description = "A task runner that works well with poetry." 91 | optional = false 92 | python-versions = ">=3.8" 93 | groups = ["dev"] 94 | files = [ 95 | {file = "poethepoet-0.30.0-py3-none-any.whl", hash = "sha256:bf875741407a98da9e96f2f2d0b2c4c34f56d89939a7f53a4b6b3a64b546ec4e"}, 96 | {file = "poethepoet-0.30.0.tar.gz", hash = "sha256:9f7ccda2d6525616ce989ca8ef973739fd668f50bef0b9d3631421d504d9ae4a"}, 97 | ] 98 | 99 | [package.dependencies] 100 | pastel = ">=0.2.1,<0.3.0" 101 | pyyaml = ">=6.0.2,<7.0.0" 102 | tomli = {version = ">=1.2.2", markers = "python_version < \"3.11\""} 103 | 104 | [package.extras] 105 | poetry-plugin = ["poetry (>=1.0,<2.0)"] 106 | 107 | [[package]] 108 | name = "pyyaml" 109 | version = "6.0.2" 110 | description = "YAML parser and emitter for Python" 111 | optional = false 112 | python-versions = ">=3.8" 113 | groups = ["dev"] 114 | files = [ 115 | {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, 116 | {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, 117 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, 118 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, 119 | {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, 120 | {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, 121 | {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, 122 | {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, 123 | {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, 124 | {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, 125 | {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, 126 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, 127 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, 128 | {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, 129 | {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, 130 | {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, 131 | {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, 132 | {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, 133 | {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, 134 | {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, 135 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, 136 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, 137 | {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, 138 | {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, 139 | {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, 140 | {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, 141 | {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, 142 | {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, 143 | {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, 144 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, 145 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, 146 | {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, 147 | {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, 148 | {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, 149 | {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, 150 | {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, 151 | {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, 152 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, 153 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, 154 | {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, 155 | {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, 156 | {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, 157 | {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, 158 | {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, 159 | {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, 160 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, 161 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, 162 | {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, 163 | {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, 164 | {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, 165 | {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, 166 | {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, 167 | {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, 168 | ] 169 | 170 | [[package]] 171 | name = "ruff" 172 | version = "0.11.12" 173 | description = "An extremely fast Python linter and code formatter, written in Rust." 174 | optional = false 175 | python-versions = ">=3.7" 176 | groups = ["dev"] 177 | files = [ 178 | {file = "ruff-0.11.12-py3-none-linux_armv6l.whl", hash = "sha256:c7680aa2f0d4c4f43353d1e72123955c7a2159b8646cd43402de6d4a3a25d7cc"}, 179 | {file = "ruff-0.11.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2cad64843da9f134565c20bcc430642de897b8ea02e2e79e6e02a76b8dcad7c3"}, 180 | {file = "ruff-0.11.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9b6886b524a1c659cee1758140138455d3c029783d1b9e643f3624a5ee0cb0aa"}, 181 | {file = "ruff-0.11.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cc3a3690aad6e86c1958d3ec3c38c4594b6ecec75c1f531e84160bd827b2012"}, 182 | {file = "ruff-0.11.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f97fdbc2549f456c65b3b0048560d44ddd540db1f27c778a938371424b49fe4a"}, 183 | {file = "ruff-0.11.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74adf84960236961090e2d1348c1a67d940fd12e811a33fb3d107df61eef8fc7"}, 184 | {file = "ruff-0.11.12-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b56697e5b8bcf1d61293ccfe63873aba08fdbcbbba839fc046ec5926bdb25a3a"}, 185 | {file = "ruff-0.11.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d47afa45e7b0eaf5e5969c6b39cbd108be83910b5c74626247e366fd7a36a13"}, 186 | {file = "ruff-0.11.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bf9603fe1bf949de8b09a2da896f05c01ed7a187f4a386cdba6760e7f61be"}, 187 | {file = "ruff-0.11.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08033320e979df3b20dba567c62f69c45e01df708b0f9c83912d7abd3e0801cd"}, 188 | {file = "ruff-0.11.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:929b7706584f5bfd61d67d5070f399057d07c70585fa8c4491d78ada452d3bef"}, 189 | {file = "ruff-0.11.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7de4a73205dc5756b8e09ee3ed67c38312dce1aa28972b93150f5751199981b5"}, 190 | {file = "ruff-0.11.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2635c2a90ac1b8ca9e93b70af59dfd1dd2026a40e2d6eebaa3efb0465dd9cf02"}, 191 | {file = "ruff-0.11.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d05d6a78a89166f03f03a198ecc9d18779076ad0eec476819467acb401028c0c"}, 192 | {file = "ruff-0.11.12-py3-none-win32.whl", hash = "sha256:f5a07f49767c4be4772d161bfc049c1f242db0cfe1bd976e0f0886732a4765d6"}, 193 | {file = "ruff-0.11.12-py3-none-win_amd64.whl", hash = "sha256:5a4d9f8030d8c3a45df201d7fb3ed38d0219bccd7955268e863ee4a115fa0832"}, 194 | {file = "ruff-0.11.12-py3-none-win_arm64.whl", hash = "sha256:65194e37853158d368e333ba282217941029a28ea90913c67e558c611d04daa5"}, 195 | {file = "ruff-0.11.12.tar.gz", hash = "sha256:43cf7f69c7d7c7d7513b9d59c5d8cafd704e05944f978614aa9faff6ac202603"}, 196 | ] 197 | 198 | [[package]] 199 | name = "tomli" 200 | version = "2.0.1" 201 | description = "A lil' TOML parser" 202 | optional = false 203 | python-versions = ">=3.7" 204 | groups = ["dev"] 205 | markers = "python_version < \"3.11\"" 206 | files = [ 207 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 208 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 209 | ] 210 | 211 | [[package]] 212 | name = "typing-extensions" 213 | version = "4.13.2" 214 | description = "Backported and Experimental Type Hints for Python 3.8+" 215 | optional = false 216 | python-versions = ">=3.8" 217 | groups = ["main", "dev"] 218 | files = [ 219 | {file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"}, 220 | {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"}, 221 | ] 222 | 223 | [metadata] 224 | lock-version = "2.1" 225 | python-versions = ">=3.8.1" 226 | content-hash = "c6e7a772cbc261360a7723dceaa2f33b5addcea8230062e48dc6f729294d4e9b" 227 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "asserts" 3 | version = "0.13.2.dev0" 4 | description = "Stand-alone Assertions" 5 | readme = "README.md" 6 | authors = ["Sebastian Rittau "] 7 | license = "MIT" 8 | homepage = "https://github.com/srittau/python-asserts" 9 | repository = "https://github.com/srittau/python-asserts" 10 | classifiers = [ 11 | "Development Status :: 3 - Alpha", 12 | "Intended Audience :: Developers", 13 | "Topic :: Software Development :: Quality Assurance", 14 | "Topic :: Software Development :: Testing", 15 | ] 16 | packages = [{ include = "asserts" }] 17 | include = ["*/py.typed", "*.pyi"] 18 | 19 | [tool.poetry.urls] 20 | "GitHub" = "https://github.com/srittau/python-asserts" 21 | "Bug Tracker" = "https://github.com/srittau/python-asserts/issues" 22 | "Changes" = "https://github.com/srittau/python-asserts/blob/main/CHANGELOG.md" 23 | 24 | [tool.poetry.dependencies] 25 | python = ">=3.8.1" 26 | typing-extensions = "^4.10.0" 27 | 28 | [tool.poetry.group.dev.dependencies] 29 | mypy = ">=1.9,<1.15" 30 | poethepoet = ">=0.27.0,<0.31" 31 | ruff = ">=0.5.1,<0.12.0" 32 | 33 | [tool.ruff] 34 | line-length = 79 35 | target-version = "py38" 36 | 37 | [tool.poe.tasks] 38 | test = "python3 -Wall -m unittest test_asserts" 39 | doctest = "python3 -m doctest asserts/__init__.py" 40 | lint = "ruff check asserts test_asserts.py" 41 | typecheck = "mypy asserts test_asserts.py" 42 | 43 | [build-system] 44 | requires = ["poetry-core>=1.0.0"] 45 | build-backend = "poetry.core.masonry.api" 46 | -------------------------------------------------------------------------------- /test_asserts.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import re 4 | import sys 5 | from collections import OrderedDict 6 | from datetime import datetime, timedelta, timezone 7 | from json import JSONDecodeError 8 | from unittest import TestCase 9 | from warnings import catch_warnings, simplefilter, warn 10 | 11 | from asserts import ( 12 | Absent, 13 | Exists, 14 | Present, 15 | assert_almost_equal, 16 | assert_between, 17 | assert_boolean_false, 18 | assert_boolean_true, 19 | assert_count_equal, 20 | assert_datetime_about_now, 21 | assert_datetime_about_now_utc, 22 | assert_dict_equal, 23 | assert_dict_superset, 24 | assert_equal, 25 | assert_false, 26 | assert_greater, 27 | assert_greater_equal, 28 | assert_has_attr, 29 | assert_in, 30 | assert_is, 31 | assert_is_instance, 32 | assert_is_none, 33 | assert_is_not, 34 | assert_is_not_none, 35 | assert_json_subset, 36 | assert_less, 37 | assert_less_equal, 38 | assert_not_almost_equal, 39 | assert_not_equal, 40 | assert_not_in, 41 | assert_not_is_instance, 42 | assert_not_regex, 43 | assert_raises, 44 | assert_raises_errno, 45 | assert_raises_regex, 46 | assert_regex, 47 | assert_succeeds, 48 | assert_true, 49 | assert_warns, 50 | assert_warns_regex, 51 | fail, 52 | ) 53 | 54 | 55 | class Box: 56 | def __init__(self, initial_value): 57 | self.value = initial_value 58 | 59 | 60 | class _DummyObject(object): 61 | def __init__(self, value="x"): 62 | self.value = value 63 | 64 | def __repr__(self): 65 | return "" 66 | 67 | 68 | def _assert_raises_assertion(expected_message): 69 | """Fail if the context does not raise an AssertionError or the exception 70 | message does not match. 71 | 72 | This is used to test assertions, without using those assertions. 73 | 74 | """ 75 | 76 | class Context(object): 77 | def __enter__(self): 78 | pass 79 | 80 | def __exit__(self, exc_type, exc_val, exc_tb): 81 | if exc_type is None: 82 | raise AssertionError("no AssertionError raised") 83 | if not issubclass(exc_type, AssertionError): 84 | return False 85 | if str(exc_val) != expected_message: 86 | raise AssertionError( 87 | "expected exception message {!r}, got {!r}".format( 88 | expected_message, str(exc_val) 89 | ) 90 | ) 91 | return True 92 | 93 | return Context() 94 | 95 | 96 | class AssertTest(TestCase): 97 | _type_string = "type" if sys.version_info[0] < 3 else "class" 98 | 99 | # fail() 100 | 101 | def test_fail__default_message(self): 102 | with _assert_raises_assertion("assertion failure"): 103 | fail() 104 | 105 | def test_fail__with_message(self): 106 | with _assert_raises_assertion("test message"): 107 | fail("test message") 108 | 109 | # assert_true() 110 | 111 | def test_assert_true__truthy_value(self): 112 | assert_true("Hello World!") 113 | 114 | def test_assert_true__falsy_value__default_message(self): 115 | with _assert_raises_assertion("'' is not truthy"): 116 | assert_true("") 117 | 118 | def test_assert_true__falsy_value__custom_message(self): 119 | with _assert_raises_assertion("0 is not truthy;0"): 120 | assert_true(0, "{msg};{expr}") 121 | 122 | # assert_false() 123 | 124 | def test_assert_false__falsy_value(self): 125 | assert_false("") 126 | 127 | def test_assert_false__truthy_value__default_message(self): 128 | with _assert_raises_assertion("25 is not falsy"): 129 | assert_false(25) 130 | 131 | def test_assert_false__truthy_value__custom_message(self): 132 | with _assert_raises_assertion("'foo' is not falsy;foo"): 133 | assert_false("foo", "{msg};{expr}") 134 | 135 | # assert_boolean_true() 136 | 137 | def test_assert_boolean_true__true(self): 138 | assert_boolean_true(True) 139 | 140 | def test_assert_boolean_true__false__custom_message(self): 141 | with _assert_raises_assertion("'Foo' is not True;Foo"): 142 | assert_boolean_true("Foo", "{msg};{expr}") 143 | 144 | def test_assert_boolean_true__truthy__default_message(self): 145 | with _assert_raises_assertion("1 is not True"): 146 | assert_boolean_true(1) 147 | 148 | # assert_boolean_false() 149 | 150 | def test_assert_boolean_false__false(self): 151 | assert_boolean_false(False) 152 | 153 | def test_assert_boolean_false__true__default_message(self): 154 | with _assert_raises_assertion("'foo' is not False"): 155 | assert_boolean_false("foo") 156 | 157 | def test_assert_boolean_false__falsy__custom_message(self): 158 | with _assert_raises_assertion("0 is not False;0"): 159 | assert_boolean_false(0, "{msg};{expr}") 160 | 161 | # assert_is_none() 162 | 163 | def test_assert_is_none__none(self): 164 | assert_is_none(None) 165 | 166 | def test_assert_is_none__string__default_message(self): 167 | with _assert_raises_assertion("'' is not None"): 168 | assert_is_none("") 169 | 170 | def test_assert_is_none__int__custom_message(self): 171 | with _assert_raises_assertion("55 is not None;55"): 172 | assert_is_none(55, "{msg};{expr}") 173 | 174 | # assert_is_not_none() 175 | 176 | def test_assert_is_not_none__string(self): 177 | assert_is_not_none("") 178 | 179 | def test_assert_is_not_none__none__default_message(self): 180 | with _assert_raises_assertion("expression is None"): 181 | assert_is_not_none(None) 182 | 183 | def test_assert_is_not_none__none__custom_message(self): 184 | with _assert_raises_assertion("expression is None;None"): 185 | assert_is_not_none(None, "{msg};{expr!r}") 186 | 187 | # assert_equal() 188 | 189 | def test_assert_equal__equal_strings(self): 190 | assert_equal("foo", "foo") 191 | 192 | def test_assert_equal__equal_objects(self): 193 | class MyClass(object): 194 | def __eq__(self, other): 195 | return True 196 | 197 | assert_equal(MyClass(), MyClass()) 198 | 199 | def test_assert_equal__not_equal__default_message(self): 200 | with _assert_raises_assertion("'string' != 55"): 201 | assert_equal("string", 55) 202 | 203 | def test_assert_equal__not_equal__custom_message(self): 204 | with _assert_raises_assertion("'string' != 55;'string';55"): 205 | assert_equal("string", 55, "{msg};{first!r};{second!r}") 206 | 207 | def test_assert_equal__dict(self): 208 | with _assert_raises_assertion("key 'foo' missing from right dict"): 209 | assert_equal({"foo": 5}, {}) 210 | 211 | # assert_not_equal() 212 | 213 | def test_assert_not_equal__not_equal(self): 214 | assert_not_equal("abc", "def") 215 | 216 | def test_assert_not_equal__equal__default_message(self): 217 | with _assert_raises_assertion("'abc' == 'abc'"): 218 | assert_not_equal("abc", "abc") 219 | 220 | def test_assert_not_equal__equal__custom_message(self): 221 | with _assert_raises_assertion("1.0 == 1;1.0;1"): 222 | assert_not_equal(1.0, 1, "{msg};{first};{second}") 223 | 224 | # assert_almost_equal() 225 | 226 | def test_assert_almost_equal__same(self): 227 | assert_almost_equal(5, 5) 228 | 229 | def test_assert_almost_equal__similar__defaults(self): 230 | assert_almost_equal(5, 5.00000001) 231 | 232 | def test_assert_almost_equal__similar__places(self): 233 | assert_almost_equal(5, 5.0001, places=3) 234 | 235 | def test_assert_almost_equal__similar__delta(self): 236 | assert_almost_equal(5, 5.001, delta=0.1) 237 | 238 | def test_assert_almost_equal__similar__delta_reverse(self): 239 | assert_almost_equal(5, 5.001, delta=0.1) 240 | 241 | def test_assert_almost_equal__not_similar__default_message(self): 242 | with _assert_raises_assertion("5 != 5.0001 within 7 places"): 243 | assert_almost_equal(5, 5.0001) 244 | 245 | def test_assert_almost_equal__not_similar__places__default_message(self): 246 | with _assert_raises_assertion("5 != 6 within 3 places"): 247 | assert_almost_equal(5, 6, places=3) 248 | 249 | def test_assert_almost_equal__not_similar__delta__default_message(self): 250 | with _assert_raises_assertion("5 != 6 with delta=0.1"): 251 | assert_almost_equal(5, 6, delta=0.1) 252 | 253 | def test_assert_almost_equal__not_similar__delta_reverse(self): 254 | with _assert_raises_assertion("6 != 5 with delta=0.3"): 255 | assert_almost_equal(6, 5, delta=0.3) 256 | 257 | def test_assert_almost_equal__not_similar__custom_message(self): 258 | with _assert_raises_assertion("5 != -5 within 7 places;5;-5;7;None"): 259 | assert_almost_equal( 260 | 5, -5, msg_fmt="{msg};{first};{second};{places};{delta!r}" 261 | ) 262 | 263 | def test_assert_almost_equal__not_similar__places__custom_message(self): 264 | with _assert_raises_assertion("5 != -5 within 3 places;5;-5;3;None"): 265 | assert_almost_equal( 266 | 5, 267 | -5, 268 | places=3, 269 | msg_fmt="{msg};{first};{second};{places};{delta!r}", 270 | ) 271 | 272 | def test_assert_almost_equal__not_similar__delta__custom_message(self): 273 | with _assert_raises_assertion("5 != 6 with delta=0.1;5;6;None;0.1"): 274 | assert_almost_equal( 275 | 5, 276 | 6, 277 | delta=0.1, 278 | msg_fmt="{msg};{first};{second};{places!r};{delta}", 279 | ) 280 | 281 | def test_assert_almost_equal__wrong_types(self): 282 | try: 283 | assert_almost_equal("5", "5") # type: ignore[arg-type] 284 | except TypeError: 285 | pass 286 | else: 287 | raise AssertionError("TypeError not raised") 288 | 289 | def test_assert_almost_equal__places_and_delta(self): 290 | try: 291 | assert_almost_equal(5, 5, places=3, delta=0.0003) 292 | except TypeError: 293 | pass 294 | else: 295 | raise AssertionError("TypeError not raised") 296 | 297 | def test_assert_almost_equal__delta_eq_0(self): 298 | try: 299 | assert_almost_equal(5, 5, delta=0) 300 | except ValueError: 301 | pass 302 | else: 303 | raise AssertionError("ValueError not raised") 304 | 305 | def test_assert_almost_equal__delta_lt_0(self): 306 | try: 307 | assert_almost_equal(5, 5, delta=-1) 308 | except ValueError: 309 | pass 310 | else: 311 | raise AssertionError("ValueError not raised") 312 | 313 | # assert_not_almost_equal() 314 | 315 | def test_assert_not_almost_equal__same(self): 316 | with _assert_raises_assertion("5 == 5 within 7 places"): 317 | assert_not_almost_equal(5, 5) 318 | 319 | def test_assert_not_almost_equal__similar__defaults(self): 320 | with _assert_raises_assertion("5 == 5.00000001 within 7 places"): 321 | assert_not_almost_equal(5, 5.00000001) 322 | 323 | def test_assert_not_almost_equal__similar__places(self): 324 | with _assert_raises_assertion("5 == 5.0001 within 3 places"): 325 | assert_not_almost_equal(5, 5.0001, places=3) 326 | 327 | def test_assert_not_almost_equal__similar__delta(self): 328 | with _assert_raises_assertion("5 == 5.1 with delta=0.1"): 329 | assert_not_almost_equal(5, 5.1, delta=0.1) 330 | 331 | def test_assert_not_almost_equal__similar__delta_reverse(self): 332 | with _assert_raises_assertion("5 != 6 with delta=0.3"): 333 | assert_almost_equal(5, 6, delta=0.3) 334 | 335 | def test_assert_not_almost_equal__not_similar(self): 336 | assert_not_almost_equal(5, 5.0001) 337 | 338 | def test_assert_not_almost_equal__not_similar__delta(self): 339 | assert_not_almost_equal(5, 5.1, delta=0.05) 340 | 341 | def test_assert_not_almost_equal__not_similar__delta_reverse(self): 342 | assert_not_almost_equal(5.1, 5, delta=0.05) 343 | 344 | def test_assert_not_almost_equal__similar__custom_message(self): 345 | with _assert_raises_assertion( 346 | "5 == 5.00000001 within 7 places;5;5.00000001;7;None" 347 | ): 348 | assert_not_almost_equal( 349 | 5, 350 | 5.00000001, 351 | msg_fmt="{msg};{first};{second};{places};{delta!r}", 352 | ) 353 | 354 | def test_assert_not_almost_equal__similar__places__custom_message(self): 355 | with _assert_raises_assertion( 356 | "5 == 5.0001 within 3 places;5;5.0001;3;None" 357 | ): 358 | assert_not_almost_equal( 359 | 5, 360 | 5.0001, 361 | places=3, 362 | msg_fmt="{msg};{first};{second};{places};{delta!r}", 363 | ) 364 | 365 | def test_assert_not_almost_equal__similar__delta__custom_message(self): 366 | with _assert_raises_assertion("5 == 6 with delta=1.1;5;6;None;1.1"): 367 | assert_not_almost_equal( 368 | 5, 369 | 6, 370 | delta=1.1, 371 | msg_fmt="{msg};{first};{second};{places!r};{delta}", 372 | ) 373 | 374 | def test_assert_not_almost_equal__wrong_types(self): 375 | try: 376 | assert_not_almost_equal("5", "5") # type: ignore[arg-type] 377 | except TypeError: 378 | pass 379 | else: 380 | raise AssertionError("TypeError not raised") 381 | 382 | def test_assert_not_almost_equal__places_and_delta(self): 383 | try: 384 | assert_not_almost_equal(5, 5, places=3, delta=0.0003) 385 | except TypeError: 386 | pass 387 | else: 388 | raise AssertionError("TypeError not raised") 389 | 390 | def test_not_assert_almost_equal__delta_eq_0(self): 391 | try: 392 | assert_not_almost_equal(5, 5, delta=0) 393 | except ValueError: 394 | pass 395 | else: 396 | raise AssertionError("ValueError not raised") 397 | 398 | def test_not_assert_almost_equal__delta_lt_0(self): 399 | try: 400 | assert_not_almost_equal(5, 5, delta=-1) 401 | except ValueError: 402 | pass 403 | else: 404 | raise AssertionError("ValueError not raised") 405 | 406 | # assert_dict_equal() 407 | 408 | def test_assert_dict_equal__empty_dicts(self): 409 | assert_dict_equal({}, {}) 410 | 411 | def test_assert_dict_equal__dicts_are_equal(self): 412 | assert_dict_equal({"foo": 5}, {"foo": 5}) 413 | 414 | def test_assert_dict_equal__one_key_missing_from_right(self): 415 | with _assert_raises_assertion("key 'foo' missing from right dict"): 416 | assert_dict_equal({"bar": 10, "foo": 5}, {"bar": 10}) 417 | 418 | def test_assert_dict_equal__multiple_keys_missing_from_right(self): 419 | with _assert_raises_assertion( 420 | "keys 'bar', 'foo' missing from right dict" 421 | ): 422 | assert_dict_equal({"foo": 5, "bar": 10, "baz": 15}, {"baz": 15}) 423 | 424 | def test_assert_dict_equal__one_key_missing_from_left(self): 425 | with _assert_raises_assertion("extra key 'foo' in right dict"): 426 | assert_dict_equal({"bar": 10}, {"bar": 10, "foo": 5}) 427 | 428 | def test_assert_dict_equal__multiple_keys_missing_from_left(self): 429 | with _assert_raises_assertion("extra keys 'bar', 'foo' in right dict"): 430 | assert_dict_equal({"baz": 15}, {"foo": 5, "bar": 10, "baz": 15}) 431 | 432 | def test_assert_dict_equal__values_do_not_match(self): 433 | with _assert_raises_assertion("key 'foo' differs: 15 != 10"): 434 | assert_dict_equal({"foo": 15}, {"foo": 10}) 435 | 436 | def test_assert_dict_equal__not_string_keys(self): 437 | with _assert_raises_assertion("key 10 missing from right dict"): 438 | assert_dict_equal({10: "foo"}, {}) 439 | with _assert_raises_assertion("keys 'foo', 5 missing from right dict"): 440 | assert_dict_equal({5: "", "foo": ""}, {}) 441 | with _assert_raises_assertion("extra key 10 in right dict"): 442 | assert_dict_equal({}, {10: "foo"}) 443 | with _assert_raises_assertion("extra keys 'foo', 5 in right dict"): 444 | assert_dict_equal({}, {5: "", "foo": ""}) 445 | 446 | def test_assert_dict_equal__message_precedence(self): 447 | with _assert_raises_assertion("key 'foo' missing from right dict"): 448 | assert_dict_equal( 449 | {"foo": "", "bar": "", "baz": 5}, 450 | {"bar": "", "baz": 10, "extra": ""}, 451 | ) 452 | with _assert_raises_assertion("extra key 'extra' in right dict"): 453 | assert_dict_equal( 454 | {"bar": "", "baz": 5}, {"bar": "", "baz": 10, "extra": ""} 455 | ) 456 | 457 | def test_assert_dict_equal__custom_key_message(self): 458 | with _assert_raises_assertion( 459 | "key 'foo' missing from right dict;" 460 | "{'foo': ''};{'bar': ''};['foo'];['bar']" 461 | ): 462 | assert_dict_equal( 463 | {"foo": ""}, 464 | {"bar": ""}, 465 | key_msg_fmt="{msg};{first!r};{second!r};" 466 | "{missing_keys!r};{extra_keys!r}", 467 | ) 468 | 469 | def test_assert_dict_equal__custom_value_message(self): 470 | with _assert_raises_assertion( 471 | "key 'foo' differs: 5 != 10;{'foo': 5};{'foo': 10};" "'foo';5;10" 472 | ): 473 | assert_dict_equal( 474 | {"foo": 5}, 475 | {"foo": 10}, 476 | value_msg_fmt="{msg};{first!r};{second!r};" 477 | "{key!r};{first_value};{second_value}", 478 | ) 479 | 480 | # assert_dict_superset() 481 | 482 | def test_assert_dict_superset__empty_dicts(self): 483 | assert_dict_superset({}, {}) 484 | 485 | def test_assert_dict_superset__dicts_are_equal(self): 486 | assert_dict_superset({"foo": 5}, {"foo": 5}) 487 | 488 | def test_assert_dict_superset__dicts_is_superset(self): 489 | assert_dict_superset({"foo": 5}, {"foo": 5, "bar": 10}) 490 | 491 | def test_assert_dict_superset__one_key_missing_from_right(self): 492 | with _assert_raises_assertion("key 'foo' missing from right dict"): 493 | assert_dict_superset({"bar": 10, "foo": 5}, {"bar": 10}) 494 | 495 | def test_assert_dict_superset__multiple_keys_missing_from_right(self): 496 | with _assert_raises_assertion( 497 | "keys 'bar', 'foo' missing from right dict" 498 | ): 499 | assert_dict_superset({"foo": 5, "bar": 10, "baz": 15}, {"baz": 15}) 500 | 501 | def test_assert_dict_superset__values_do_not_match(self): 502 | with _assert_raises_assertion("key 'foo' differs: 15 != 10"): 503 | assert_dict_superset({"foo": 15}, {"foo": 10, "bar": 15}) 504 | 505 | def test_assert_dict_superset__not_string_keys(self): 506 | with _assert_raises_assertion("key 10 missing from right dict"): 507 | assert_dict_superset({10: "foo"}, {}) 508 | with _assert_raises_assertion("keys 'foo', 5 missing from right dict"): 509 | assert_dict_superset({5: "", "foo": ""}, {}) 510 | 511 | def test_assert_dict_superset__message_precedence(self): 512 | with _assert_raises_assertion("key 'foo' missing from right dict"): 513 | assert_dict_superset({"foo": "", "bar": 5}, {"bar": 1}) 514 | 515 | def test_assert_dict_superset__custom_key_message(self): 516 | with _assert_raises_assertion( 517 | "key 'foo' missing from right dict;" 518 | "{'foo': ''};{'bar': ''};['foo']" 519 | ): 520 | assert_dict_superset( 521 | {"foo": ""}, 522 | {"bar": ""}, 523 | key_msg_fmt="{msg};{first!r};{second!r};" "{missing_keys!r}", 524 | ) 525 | 526 | def test_assert_dict_superset__custom_value_message(self): 527 | with _assert_raises_assertion( 528 | "key 'foo' differs: 5 != 10;{'foo': 5};{'foo': 10};" "'foo';5;10" 529 | ): 530 | assert_dict_superset( 531 | {"foo": 5}, 532 | {"foo": 10}, 533 | value_msg_fmt="{msg};{first!r};{second!r};" 534 | "{key!r};{first_value};{second_value}", 535 | ) 536 | 537 | # assert_less() 538 | 539 | def test_assert_less(self): 540 | assert_less(4, 5) 541 | with _assert_raises_assertion("5 is not less than 5"): 542 | assert_less(5, 5) 543 | with _assert_raises_assertion("'foo' is not less than 'bar'"): 544 | assert_less("foo", "bar") 545 | with _assert_raises_assertion("6 is not less than 5;6;5"): 546 | assert_less(6, 5, "{msg};{first};{second}") 547 | 548 | # assert_less_equal() 549 | 550 | def test_assert_less_equal(self): 551 | assert_less_equal(4, 5) 552 | assert_less_equal(5, 5) 553 | with _assert_raises_assertion( 554 | "'foo' is not less than or equal to 'bar'" 555 | ): 556 | assert_less_equal("foo", "bar") 557 | with _assert_raises_assertion("6 is not less than or equal to 5;6;5"): 558 | assert_less_equal(6, 5, "{msg};{first};{second}") 559 | 560 | # assert_greater() 561 | 562 | def test_assert_greater(self): 563 | assert_greater(5, 4) 564 | with _assert_raises_assertion("5 is not greater than 5"): 565 | assert_greater(5, 5) 566 | with _assert_raises_assertion("'bar' is not greater than 'foo'"): 567 | assert_greater("bar", "foo") 568 | with _assert_raises_assertion("5 is not greater than 6;5;6"): 569 | assert_greater(5, 6, "{msg};{first};{second}") 570 | 571 | # assert_greater_equal() 572 | 573 | def test_assert_greater_equal(self): 574 | assert_greater_equal(5, 4) 575 | assert_greater_equal(5, 5) 576 | with _assert_raises_assertion( 577 | "'bar' is not greater than or equal to 'foo'" 578 | ): 579 | assert_greater_equal("bar", "foo") 580 | with _assert_raises_assertion( 581 | "5 is not greater than or equal to 6;5;6" 582 | ): 583 | assert_greater_equal(5, 6, "{msg};{first};{second}") 584 | 585 | # assert_regex() 586 | 587 | def test_assert_regex__matches_string(self): 588 | assert_regex("This is a test text", "is.*test") 589 | 590 | def test_assert_regex__matches_regex(self): 591 | regex = re.compile("is.*test") 592 | assert_regex("This is a test text", regex) 593 | 594 | def test_assert_regex__does_not_match_string__default_message(self): 595 | with _assert_raises_assertion( 596 | "'This is a test text' does not match 'not found'" 597 | ): 598 | assert_regex("This is a test text", "not found") 599 | 600 | def test_assert_regex__does_not_match_regex__default_message(self): 601 | regex = re.compile(r"not found") 602 | with _assert_raises_assertion( 603 | "'This is a test text' does not match 'not found'" 604 | ): 605 | assert_regex("This is a test text", regex) 606 | 607 | def test_assert_regex__does_not_match_string__custom_message(self): 608 | with _assert_raises_assertion( 609 | "'Wrong text' does not match 'not found';" 610 | "'Wrong text';'not found'" 611 | ): 612 | assert_regex( 613 | "Wrong text", r"not found", "{msg};{text!r};{pattern!r}" 614 | ) 615 | 616 | def test_assert_regex__does_not_match_regex__custom_message(self): 617 | regex = re.compile(r"not found") 618 | with _assert_raises_assertion( 619 | "'Wrong text' does not match 'not found';'Wrong text';" 620 | "'not found'" 621 | ): 622 | assert_regex("Wrong text", regex, "{msg};{text!r};{pattern!r}") 623 | 624 | # assert_not_regex() 625 | 626 | def test_assert_not_regex__does_not_match_string(self): 627 | assert_not_regex("This is a test text", "no match") 628 | 629 | def test_assert_not_regex__does_not_match_regex(self): 630 | regex = re.compile("no match") 631 | assert_not_regex("This is a test text", regex) 632 | 633 | def test_assert_not_regex__matches_string__default_message(self): 634 | with _assert_raises_assertion( 635 | "'This is a test text' matches 'is.*test'" 636 | ): 637 | assert_not_regex("This is a test text", "is.*test") 638 | 639 | def test_assert_not_regex__matches_regex__default_message(self): 640 | regex = re.compile("is.*test") 641 | with _assert_raises_assertion( 642 | "'This is a test text' matches 'is.*test'" 643 | ): 644 | assert_not_regex("This is a test text", regex) 645 | 646 | def test_assert_not_regex__matches_string__custom_message(self): 647 | with _assert_raises_assertion( 648 | "'This is a test text' matches 'is.*test';" 649 | "'This is a test text';'is.*test'" 650 | ): 651 | assert_not_regex( 652 | "This is a test text", 653 | "is.*test", 654 | "{msg};{text!r};{pattern!r}", 655 | ) 656 | 657 | def test_assert_not_regex__matches_regex__custom_message(self): 658 | regex = re.compile("is.*test") 659 | with _assert_raises_assertion( 660 | "'This is a test text' matches 'is.*test';'This is a test text';" 661 | "'is.*test'" 662 | ): 663 | assert_not_regex( 664 | "This is a test text", regex, "{msg};{text!r};{pattern!r}" 665 | ) 666 | 667 | # assert_is() 668 | 669 | def test_assert_is__same(self): 670 | x = _DummyObject() 671 | assert_is(x, x) 672 | 673 | def test_assert_is__not_same__default_message(self): 674 | with _assert_raises_assertion("'x' is not 'y'"): 675 | assert_is("x", "y") 676 | 677 | def test_assert_is__equal_but_not_same__custom_message(self): 678 | x = "x" 679 | y = _DummyObject("y") 680 | with _assert_raises_assertion("'x' is not ;'x';y"): 681 | assert_is(x, y, "{msg};{first!r};{second.value}") 682 | 683 | # assert_is_not() 684 | 685 | def test_assert_is_not__not_same(self): 686 | x = _DummyObject() 687 | y = _DummyObject() 688 | assert_is_not(x, y) 689 | 690 | def test_assert_is_not__same__default_message(self): 691 | x = _DummyObject("x") 692 | with _assert_raises_assertion("both arguments refer to "): 693 | assert_is_not(x, x) 694 | 695 | def test_assert_is_not__same__custom_message(self): 696 | x = _DummyObject("x") 697 | with _assert_raises_assertion("both arguments refer to ;x;x"): 698 | assert_is_not(x, x, "{msg};{first.value};{second.value}") 699 | 700 | # assert_in() 701 | 702 | def test_assert_in__contains(self): 703 | assert_in("foo", ["foo", "bar", "baz"]) 704 | 705 | def test_assert_in__does_not_contain__default_message(self): 706 | with _assert_raises_assertion("'foo' not in []"): 707 | assert_in("foo", []) 708 | 709 | def test_assert_in__does_not_contain__custom_message(self): 710 | with _assert_raises_assertion("'foo' not in [];'foo';[]"): 711 | assert_in("foo", [], "{msg};{first!r};{second!r}") 712 | 713 | # assert_not_in() 714 | 715 | def test_assert_not_in__does_not_contain(self): 716 | assert_not_in("foo", []) 717 | 718 | def test_assert_not_in__does_contain__default_message(self): 719 | with _assert_raises_assertion("'foo' is in ['foo', 'bar', 'baz']"): 720 | assert_not_in("foo", ["foo", "bar", "baz"]) 721 | 722 | def test_assert_not_in__does_contain__custom_message(self): 723 | with _assert_raises_assertion("'foo' is in ['foo', 'bar'];'foo';bar"): 724 | assert_not_in("foo", ["foo", "bar"], "{msg};{first!r};{second[1]}") 725 | 726 | # assert_count_equal() 727 | 728 | def test_assert_count_equal__equal(self): 729 | with assert_succeeds(AssertionError): 730 | assert_count_equal(["a"], ["a"]) 731 | 732 | def test_assert_count_equal__equal_differing_types(self): 733 | with assert_succeeds(AssertionError): 734 | assert_count_equal(["a"], {"a"}) 735 | 736 | def test_assert_count_equal__ignore_order(self): 737 | with assert_succeeds(AssertionError): 738 | assert_count_equal(["a", "b"], ["b", "a"]) 739 | 740 | def test_assert_count_equal__missing_from_sequence1(self): 741 | with _assert_raises_assertion("missing from sequence 1: 'a'"): 742 | assert_count_equal([], {"a"}) 743 | 744 | def test_assert_count_equal__multiple_missing_from_sequence1(self): 745 | with _assert_raises_assertion("missing from sequence 1: 'b', 'c'"): 746 | assert_count_equal(["a"], ["a", "b", "c"]) 747 | 748 | def test_assert_count_equal__respect_duplicates(self): 749 | with _assert_raises_assertion("missing from sequence 1: 'a'"): 750 | assert_count_equal({"a"}, ["a", "a"]) 751 | 752 | def test_assert_count_equal__missing_from_sequence2(self): 753 | with _assert_raises_assertion("missing from sequence 2: 'a', 'c'"): 754 | assert_count_equal(["a", "b", "c"], ["b"]) 755 | 756 | def test_assert_count_equal__missing_from_both(self): 757 | msg = "missing from sequence 1: 'd'; missing from sequence 2: 'b', 'c'" 758 | with _assert_raises_assertion(msg): 759 | assert_count_equal(["a", "b", "c"], ["a", "d"]) 760 | 761 | def test_assert_count_equal__custom_message(self): 762 | with _assert_raises_assertion("missing from sequence 1: 'a';[];['a']"): 763 | assert_count_equal([], ["a"], "{msg};{first};{second}") 764 | 765 | # assert_between() 766 | 767 | def test_assert_between__within_range(self): 768 | assert_between(0, 10, 0) 769 | assert_between(0, 10, 10) 770 | assert_between(0, 10, 5) 771 | 772 | def test_assert_between__too_low__default_message(self): 773 | with _assert_raises_assertion("-1 is not between 0 and 10"): 774 | assert_between(0, 10, -1) 775 | 776 | def test_assert_between__too_high__custom_message(self): 777 | with _assert_raises_assertion("11 is not between 0 and 10;0;10;11"): 778 | assert_between(0, 10, 11, "{msg};{lower};{upper};{expr}") 779 | 780 | # assert_is_instance() 781 | 782 | def _is_instance_message(self, expr, expected_type, real_type): 783 | expected_message = ( 784 | "{!r} is an instance of , expected {}".format( 785 | expr, real_type, expected_type 786 | ) 787 | ) 788 | if sys.version_info[0] < 3: 789 | return expected_message.replace("class", "type") 790 | else: 791 | return expected_message 792 | 793 | def test_assert_is_instance__single_type(self): 794 | assert_is_instance(4, int) 795 | assert_is_instance(OSError(), Exception) 796 | 797 | def test_assert_is_instance__multiple_types(self): 798 | assert_is_instance(4, (str, int)) 799 | 800 | def test_assert_is_instance__default_message(self): 801 | expected_message = self._is_instance_message( 802 | "my string", "", "'str'" 803 | ) 804 | with _assert_raises_assertion(expected_message): 805 | assert_is_instance("my string", int) 806 | 807 | def test_assert_is_instance__custom_message_single_type(self): 808 | expected_message = self._is_instance_message( 809 | "my string", "", "'str'" 810 | ) 811 | expected = "{};my string;(,)".format(expected_message) 812 | expected = expected.replace("class", self._type_string) 813 | with _assert_raises_assertion(expected): 814 | assert_is_instance("my string", int, "{msg};{obj};{types}") 815 | 816 | def test_assert_is_instance__custom_message_multiple_types(self): 817 | expected_message = self._is_instance_message( 818 | "my string", "(, )", "'str'" 819 | ) 820 | expected = "{};my string;(, )".format( 821 | expected_message 822 | ) 823 | expected = expected.replace("class", self._type_string) 824 | with _assert_raises_assertion(expected): 825 | assert_is_instance( 826 | "my string", (int, float), "{msg};{obj};{types}" 827 | ) 828 | 829 | # assert_not_is_instance() 830 | 831 | def _not_is_instance_message(self, obj): 832 | expected_message = "{!r} is an instance of {}".format( 833 | obj, obj.__class__ 834 | ) 835 | if sys.version_info[0] < 3: 836 | expected_message = expected_message.replace("class", "type") 837 | expected_message = expected_message.replace( 838 | "type 'OSError'", "type 'exceptions.OSError'" 839 | ) 840 | return expected_message 841 | 842 | def test_assert_not_is_instance__single_type(self): 843 | assert_not_is_instance(4, str) 844 | 845 | def test_assert_not_is_instance__multiple_types(self): 846 | assert_not_is_instance(4, (str, bytes)) 847 | 848 | def test_assert_not_is_instance__default_message(self): 849 | obj = OSError() 850 | expected_message = self._not_is_instance_message(obj) 851 | with _assert_raises_assertion(expected_message): 852 | assert_not_is_instance(obj, Exception) 853 | 854 | def test_assert_not_is_instance__custom_message__single_type(self): 855 | msg = self._not_is_instance_message("Foo") 856 | expected = "{};Foo;(,)".format(msg) 857 | expected = expected.replace("class", self._type_string) 858 | with _assert_raises_assertion(expected): 859 | assert_not_is_instance("Foo", str, "{msg};{obj};{types!r}") 860 | 861 | def test_assert_not_is_instance__custom_message__multiple_types(self): 862 | msg = self._not_is_instance_message("Foo") 863 | expected = "{};Foo;(, )".format(msg) 864 | expected = expected.replace("class", self._type_string) 865 | with _assert_raises_assertion(expected): 866 | assert_not_is_instance("Foo", (str, int), "{msg};{obj};{types!r}") 867 | 868 | # assert_has_attr() 869 | 870 | def test_assert_has_attr__has_attribute(self): 871 | d = _DummyObject() 872 | assert_has_attr(d, "value") 873 | 874 | def test_assert_has_attr__does_not_have_attribute__default_message(self): 875 | d = _DummyObject() 876 | with _assert_raises_assertion(" does not have attribute 'foo'"): 877 | assert_has_attr(d, "foo") 878 | 879 | def test_assert_has_attr__does_not_have_attribute__custom_message(self): 880 | d = _DummyObject() 881 | expected = " does not have attribute 'foo';;foo" 882 | with _assert_raises_assertion(expected): 883 | assert_has_attr(d, "foo", msg_fmt="{msg};{obj!r};{attribute}") 884 | 885 | # assert_datetime_about_now() 886 | 887 | def test_assert_datetime_about_now__close(self): 888 | assert_datetime_about_now(datetime.now()) 889 | 890 | def test_assert_datetime_about_now__none__default_message(self): 891 | expected_message = r"^None is not a valid date/time$" 892 | with assert_raises_regex(AssertionError, expected_message): 893 | assert_datetime_about_now(None) 894 | 895 | def test_assert_datetime_about_now__none__custom_message(self): 896 | dt = datetime.now().date().isoformat() 897 | expected = "None is not a valid date/time;None;{}".format(dt) 898 | with _assert_raises_assertion(expected): 899 | assert_datetime_about_now( 900 | None, msg_fmt="{msg};{actual!r};{now:%Y-%m-%d}" 901 | ) 902 | 903 | def test_assert_datetime_about_now__too_low(self): 904 | then = datetime.now() - timedelta(minutes=1) 905 | with assert_raises(AssertionError): 906 | assert_datetime_about_now(then) 907 | 908 | def test_assert_datetime_about_now__too_high(self): 909 | then = datetime.now() + timedelta(minutes=1) 910 | with assert_raises(AssertionError): 911 | assert_datetime_about_now(then) 912 | 913 | def test_assert_datetime_about_now__default_message(self): 914 | then = datetime(1990, 4, 13, 12, 30, 15) 915 | expected_message = ( 916 | r"^datetime.datetime\(1990, 4, 13, 12, 30, 15\) " 917 | "is not close to current date/time$" 918 | ) 919 | with assert_raises_regex(AssertionError, expected_message): 920 | assert_datetime_about_now(then) 921 | 922 | def test_assert_datetime_about_now__custom_message(self): 923 | then = datetime(1990, 4, 13, 12, 30, 15) 924 | now = datetime.now().date().isoformat() 925 | expected = ( 926 | "datetime.datetime(1990, 4, 13, 12, 30, 15) " 927 | "is not close to current date/time;12:30;{}".format(now) 928 | ) 929 | with _assert_raises_assertion(expected): 930 | assert_datetime_about_now( 931 | then, msg_fmt="{msg};{actual:%H:%M};{now:%Y-%m-%d}" 932 | ) 933 | 934 | # assert_datetime_about_now_utc() 935 | 936 | def test_assert_datetime_about_now_utc__close(self): 937 | assert_datetime_about_now_utc( 938 | datetime.now(timezone.utc).replace(tzinfo=None) 939 | ) 940 | 941 | def test_assert_datetime_about_now_utc__none__default_message(self): 942 | expected_message = r"^None is not a valid date/time$" 943 | with assert_raises_regex(AssertionError, expected_message): 944 | assert_datetime_about_now_utc(None) 945 | 946 | def test_assert_datetime_about_now_utc__none__custom_message(self): 947 | dt = datetime.now(timezone.utc).date().isoformat() 948 | expected = "None is not a valid date/time;None;{}".format(dt) 949 | with _assert_raises_assertion(expected): 950 | assert_datetime_about_now_utc( 951 | None, msg_fmt="{msg};{actual!r};{now:%Y-%m-%d}" 952 | ) 953 | 954 | def test_assert_datetime_about_now_utc__too_low(self): 955 | then = datetime.now(timezone.utc).replace(tzinfo=None) - timedelta( 956 | minutes=1 957 | ) 958 | with assert_raises(AssertionError): 959 | assert_datetime_about_now_utc(then) 960 | 961 | def test_assert_datetime_about_now_utc__too_high(self): 962 | then = datetime.now(timezone.utc).replace(tzinfo=None) + timedelta( 963 | minutes=1 964 | ) 965 | with assert_raises(AssertionError): 966 | assert_datetime_about_now_utc(then) 967 | 968 | def test_assert_datetime_about_now_utc__default_message(self): 969 | then = datetime(1990, 4, 13, 12, 30, 15) 970 | expected_message = ( 971 | r"datetime.datetime\(1990, 4, 13, 12, 30, 15\) " 972 | r"is not close to current UTC date/time$" 973 | ) 974 | with assert_raises_regex(AssertionError, expected_message): 975 | assert_datetime_about_now_utc(then) 976 | 977 | def test_assert_datetime_about_now_utc__custom_message(self): 978 | then = datetime(1990, 4, 13, 12, 30, 15) 979 | now = datetime.now(timezone.utc).date().isoformat() 980 | expected = ( 981 | "datetime.datetime(1990, 4, 13, 12, 30, 15) " 982 | "is not close to current UTC date/time;12:30;{}".format(now) 983 | ) 984 | with _assert_raises_assertion(expected): 985 | assert_datetime_about_now_utc( 986 | then, msg_fmt="{msg};{actual:%H:%M};{now:%Y-%m-%d}" 987 | ) 988 | 989 | # assert_raises() 990 | 991 | def test_assert_raises__raises_right_exception(self): 992 | with assert_raises(KeyError): 993 | raise KeyError() 994 | 995 | def test_assert_raises__exc_val(self): 996 | exc = KeyError() 997 | with assert_raises(KeyError) as context: 998 | raise exc 999 | assert_is(exc, context.exc_val) 1000 | 1001 | def test_assert_raises__exc_val_within_context(self): 1002 | with assert_raises(RuntimeError): 1003 | with assert_raises(KeyError) as context: 1004 | context.exc_val 1005 | 1006 | def test_assert_raises__raises_subclass(self): 1007 | class MyError(IndexError): 1008 | pass 1009 | 1010 | with assert_raises(IndexError): 1011 | raise MyError() 1012 | 1013 | def test_assert_raises__exception_not_raised__default_message(self): 1014 | with _assert_raises_assertion("KeyError not raised"): 1015 | with assert_raises(KeyError): 1016 | pass 1017 | 1018 | def test_assert_raises__exception_not_raised__custom_message(self): 1019 | expected = "KeyError not raised;KeyError;KeyError" 1020 | with _assert_raises_assertion(expected): 1021 | with assert_raises( 1022 | KeyError, msg_fmt="{msg};{exc_type.__name__};{exc_name}" 1023 | ): 1024 | pass 1025 | 1026 | def test_assert_raises__wrong_exception_raised(self): 1027 | try: 1028 | with assert_raises(IndexError): 1029 | raise KeyError() 1030 | except KeyError: 1031 | pass 1032 | except Exception as exc: 1033 | fail(str(exc) + " was raised") 1034 | else: 1035 | fail("no exception raised") 1036 | 1037 | def test_assert_raises__add_test_called(self): 1038 | called = Box(False) 1039 | 1040 | def extra_test(exc): 1041 | assert_is_instance(exc, KeyError) 1042 | called.value = True 1043 | 1044 | with assert_raises(KeyError) as context: 1045 | context.add_test(extra_test) 1046 | raise KeyError() 1047 | assert_true(called.value, "extra_test() was not called") 1048 | 1049 | def test_assert_raises__add_test_not_called(self): 1050 | called = Box(False) 1051 | 1052 | def extra_test(_): 1053 | called.value = True 1054 | 1055 | with assert_raises(AssertionError): 1056 | with assert_raises(KeyError) as context: 1057 | context.add_test(extra_test) 1058 | assert_false(called.value, "extra_test() was unexpectedly called") 1059 | 1060 | # assert_raises_regex() 1061 | 1062 | def test_assert_raises_regex__raises_right_exception(self): 1063 | with assert_raises_regex(KeyError, r"test.*"): 1064 | raise KeyError("test message") 1065 | 1066 | def test_assert_raises_regex__raises_right_exception__compiled(self): 1067 | with assert_raises_regex(KeyError, re.compile(r"test.*")): 1068 | raise KeyError("test message") 1069 | 1070 | def test_assert_raises_regex__exception_not_raised__default_message(self): 1071 | with _assert_raises_assertion("KeyError not raised"): 1072 | with assert_raises_regex(KeyError, r"test"): 1073 | pass 1074 | 1075 | def test_assert_raises_regex__exception_not_raised__custom_message(self): 1076 | expected = "KeyError not raised;KeyError;KeyError;'';test" 1077 | with _assert_raises_assertion(expected): 1078 | msg_fmt = "{msg};{exc_type.__name__};{exc_name};{text!r};{pattern}" 1079 | with assert_raises_regex(KeyError, r"test", msg_fmt=msg_fmt): 1080 | pass 1081 | 1082 | def test_assert_raises_regex__no_message__default_message(self): 1083 | with _assert_raises_assertion("KeyError without message"): 1084 | with assert_raises_regex(KeyError, r"test"): 1085 | raise KeyError() 1086 | 1087 | def test_assert_raises_regex__no_message__custom_message(self): 1088 | expected = "KeyError without message;KeyError;KeyError;None;test" 1089 | with _assert_raises_assertion(expected): 1090 | msg_fmt = "{msg};{exc_type.__name__};{exc_name};{text!r};{pattern}" 1091 | with assert_raises_regex(KeyError, r"test", msg_fmt=msg_fmt): 1092 | raise KeyError() 1093 | 1094 | def test_assert_raises_regex__wrong_exception_raised(self): 1095 | try: 1096 | with assert_raises_regex(IndexError, "test message"): 1097 | raise KeyError("test message") 1098 | except KeyError: 1099 | pass 1100 | except Exception as exc: 1101 | fail(str(exc) + " was raised") 1102 | else: 1103 | fail("no exception raised") 1104 | 1105 | def test_assert_raises_regex__wrong_error__default_message(self): 1106 | with _assert_raises_assertion("'wrong message' does not match 'test'"): 1107 | with assert_raises_regex(KeyError, r"test"): 1108 | raise KeyError("wrong message") 1109 | 1110 | def test_assert_raises_regex__wrong_error__pattern_default_message(self): 1111 | with _assert_raises_assertion("'wrong message' does not match 'test'"): 1112 | with assert_raises_regex(KeyError, re.compile(r"test")): 1113 | raise KeyError("wrong message") 1114 | 1115 | def test_assert_raises_regex__wrong_error__custom_message(self): 1116 | expected = ( 1117 | "'wrong message' does not match 'test';KeyError;KeyError;" 1118 | "'wrong message';test" 1119 | ) 1120 | with _assert_raises_assertion(expected): 1121 | msg_fmt = "{msg};{exc_type.__name__};{exc_name};{text!r};{pattern}" 1122 | with assert_raises_regex(KeyError, r"test", msg_fmt=msg_fmt): 1123 | raise KeyError("wrong message") 1124 | 1125 | # assert_raises_errno() 1126 | 1127 | def test_assert_raises_errno__right_errno(self): 1128 | with assert_raises_errno(OSError, 20): 1129 | raise OSError(20, "Test error") 1130 | 1131 | def test_assert_raises_errno__no_exception_raised__default_message(self): 1132 | with _assert_raises_assertion("OSError not raised"): 1133 | with assert_raises_errno(OSError, 20): 1134 | pass 1135 | 1136 | def test_assert_raises_errno__no_exception_raised__custom_message(self): 1137 | expected = "OSError not raised;OSError;OSError;20;None" 1138 | with _assert_raises_assertion(expected): 1139 | msg_fmt = ( 1140 | "{msg};{exc_type.__name__};{exc_name};{expected_errno};" 1141 | "{actual_errno}" 1142 | ) 1143 | with assert_raises_errno(OSError, 20, msg_fmt=msg_fmt): 1144 | pass 1145 | 1146 | def test_assert_raises_errno__wrong_class_raised(self): 1147 | class RightClass(OSError): 1148 | pass 1149 | 1150 | class WrongClass(OSError): 1151 | pass 1152 | 1153 | try: 1154 | with assert_raises_errno(RightClass, 20): 1155 | raise WrongClass(20, "Test error") 1156 | except WrongClass: 1157 | pass 1158 | else: 1159 | raise AssertionError("WrongClass was not raised") 1160 | 1161 | def test_assert_raises_errno__wrong_errno__default_message(self): 1162 | with _assert_raises_assertion("wrong errno: 20 != 1"): 1163 | with assert_raises_errno(OSError, 20): 1164 | raise OSError(1, "Test error") 1165 | 1166 | def test_assert_raises_errno__wrong_errno__custom_message(self): 1167 | expected = "wrong errno: 20 != 1;OSError;OSError;20;1" 1168 | with _assert_raises_assertion(expected): 1169 | msg_fmt = ( 1170 | "{msg};{exc_type.__name__};{exc_name};{expected_errno};" 1171 | "{actual_errno}" 1172 | ) 1173 | with assert_raises_errno(OSError, 20, msg_fmt=msg_fmt): 1174 | raise OSError(1, "Test error") 1175 | 1176 | # assert_succeeds() 1177 | 1178 | def test_assert_succeeds__no_exception_raised(self): 1179 | with assert_succeeds(KeyError): 1180 | pass 1181 | 1182 | def test_assert_succeeds__expected_exception__default_message(self): 1183 | with _assert_raises_assertion("KeyError was unexpectedly raised"): 1184 | with assert_succeeds(KeyError): 1185 | raise KeyError() 1186 | 1187 | def test_assert_succeeds__expected_exception__custom_message(self): 1188 | expected = ( 1189 | "KeyError was unexpectedly raised;KeyError;KeyError;test error" 1190 | ) 1191 | with _assert_raises_assertion(expected): 1192 | msg_fmt = ( 1193 | "{msg};{exc_type.__name__};{exc_name};{exception.args[0]}" 1194 | ) 1195 | with assert_succeeds(KeyError, msg_fmt=msg_fmt): 1196 | raise KeyError("test error") 1197 | 1198 | def test_assert_succeeds__unexpected_exception(self): 1199 | try: 1200 | with assert_succeeds(ValueError): 1201 | raise KeyError() 1202 | except KeyError: 1203 | pass 1204 | else: 1205 | raise AssertionError("KeyError was not raised") 1206 | 1207 | # assert_warns() 1208 | 1209 | def test_assert_warns__default_message(self): 1210 | with assert_raises_regex(AssertionError, r"^ImportWarning not issued"): 1211 | with assert_warns(ImportWarning): 1212 | pass 1213 | 1214 | def test_assert_warns__custom_message(self): 1215 | exception = "ImportWarning not issued;ImportWarning;ImportWarning" 1216 | with _assert_raises_assertion(exception): 1217 | msg_fmt = "{msg};{exc_type.__name__};{exc_name}" 1218 | with assert_warns(ImportWarning, msg_fmt=msg_fmt): 1219 | pass 1220 | 1221 | def test_assert_warns__warned(self): 1222 | with assert_succeeds(AssertionError): 1223 | with assert_warns(FutureWarning): 1224 | warn("foo", FutureWarning) 1225 | 1226 | def test_assert_warns__not_warned(self): 1227 | with assert_raises(AssertionError): 1228 | with assert_warns(ImportWarning): 1229 | pass 1230 | 1231 | def test_assert_warns__wrong_type(self): 1232 | with assert_raises(AssertionError): 1233 | with assert_warns(ImportWarning): 1234 | warn("foo", UnicodeWarning) 1235 | 1236 | def test_assert_warns__multiple_warnings(self): 1237 | with assert_succeeds(AssertionError): 1238 | with assert_warns(UserWarning): 1239 | warn("foo", UnicodeWarning) 1240 | warn("bar", UserWarning) 1241 | warn("baz", FutureWarning) 1242 | 1243 | def test_assert_warns__warning_handler_deinstalled_on_success(self): 1244 | with catch_warnings(record=True) as warnings: 1245 | with assert_warns(UserWarning): 1246 | warn("foo", UserWarning) 1247 | assert warnings is not None 1248 | assert_equal(0, len(warnings)) 1249 | warn("bar", UserWarning) 1250 | assert_equal(1, len(warnings)) 1251 | 1252 | def test_assert_warns__warning_handler_deinstalled_on_failure(self): 1253 | with catch_warnings(record=True) as warnings: 1254 | try: 1255 | with assert_warns(UserWarning): 1256 | pass 1257 | except AssertionError: 1258 | pass 1259 | assert warnings is not None 1260 | assert_equal(0, len(warnings)) 1261 | warn("bar", UserWarning) 1262 | assert_equal(1, len(warnings)) 1263 | 1264 | def test_assert_warns__add_test_called(self): 1265 | called = Box(False) 1266 | 1267 | def extra_test(warning): 1268 | assert_is(warning.category, UserWarning) 1269 | called.value = True 1270 | return True 1271 | 1272 | with assert_warns(UserWarning) as context: 1273 | context.add_test(extra_test) 1274 | warn("bar", UserWarning) 1275 | assert_true(called.value, "extra_test() was not called") 1276 | 1277 | def test_assert_warns__add_test_not_called(self): 1278 | called = Box(False) 1279 | 1280 | def extra_test(_: Warning) -> bool: 1281 | called.value = True 1282 | return False 1283 | 1284 | with assert_raises(AssertionError): 1285 | with assert_warns(UserWarning) as context: 1286 | context.add_test(extra_test) 1287 | assert_false(called.value, "extra_test() was unexpectedly called") 1288 | 1289 | # assert_warns_regex() 1290 | 1291 | def test_assert_warns_regex__warned(self): 1292 | with assert_succeeds(AssertionError): 1293 | with assert_warns_regex(FutureWarning, r"fo+"): 1294 | warn("foo", FutureWarning) 1295 | 1296 | def test_assert_warns_regex__warning_text_matches_in_the_middle(self): 1297 | with assert_succeeds(AssertionError): 1298 | with assert_warns_regex(FutureWarning, r"o"): 1299 | warn("foo", FutureWarning) 1300 | 1301 | def test_assert_warns_regex__not_warned(self): 1302 | with assert_raises(AssertionError): 1303 | with assert_warns_regex(UserWarning, r"foo"): 1304 | pass 1305 | 1306 | def test_assert_warns_regex__wrong_type(self): 1307 | with assert_raises(AssertionError): 1308 | with assert_warns_regex(ImportWarning, r"foo"): 1309 | warn("foo", UnicodeWarning) 1310 | 1311 | def test_assert_warns_regex__wrong_message(self): 1312 | with assert_raises(AssertionError): 1313 | with assert_warns_regex(UnicodeWarning, r"foo"): 1314 | warn("bar", UnicodeWarning) 1315 | 1316 | def test_assert_warns_regex__multiple_warnings(self): 1317 | with assert_succeeds(AssertionError): 1318 | with assert_warns_regex(UserWarning, r"bar2"): 1319 | warn("foo", UnicodeWarning) 1320 | warn("bar1", UserWarning) 1321 | warn("bar2", UserWarning) 1322 | warn("bar3", UserWarning) 1323 | warn("baz", FutureWarning) 1324 | 1325 | def test_assert_warns_regex__warning_handler_deinstalled_on_success(self): 1326 | with catch_warnings(record=True) as warnings: 1327 | with assert_warns_regex(UserWarning, r"foo"): 1328 | warn("foo", UserWarning) 1329 | assert warnings is not None 1330 | assert_equal(0, len(warnings)) 1331 | warn("bar", UserWarning) 1332 | assert_equal(1, len(warnings)) 1333 | 1334 | def test_assert_warns_regex__warning_handler_deinstalled_on_failure(self): 1335 | with catch_warnings(record=True) as warnings: 1336 | try: 1337 | with assert_warns_regex(UserWarning, r""): 1338 | pass 1339 | except AssertionError: 1340 | pass 1341 | assert warnings is not None 1342 | assert_equal(0, len(warnings)) 1343 | warn("bar", UserWarning) 1344 | assert_equal(1, len(warnings)) 1345 | 1346 | def test_assert_warns_regex__not_issued__default_message(self): 1347 | with _assert_raises_assertion( 1348 | "no UserWarning matching 'foo.*bar' issued" 1349 | ): 1350 | with assert_warns_regex(UserWarning, r"foo.*bar"): 1351 | pass 1352 | 1353 | def test_assert_warns_regex__not_issued__custom_message(self): 1354 | expected = "no ImportWarning matching 'abc' issued;ImportWarning;ImportWarning;abc" 1355 | with _assert_raises_assertion(expected): 1356 | msg_fmt = "{msg};{exc_type.__name__};{exc_name};{pattern}" 1357 | with assert_warns_regex(ImportWarning, r"abc", msg_fmt=msg_fmt): 1358 | pass 1359 | 1360 | def test_assert_warns_regex__wrong_message__default_message(self): 1361 | with _assert_raises_assertion( 1362 | "no UserWarning matching 'foo.*bar' issued" 1363 | ): 1364 | with assert_warns_regex(UserWarning, r"foo.*bar"): 1365 | pass 1366 | 1367 | def test_assert_warns_regex__wrong_message__custom_message(self): 1368 | expected = ( 1369 | "no UserWarning matching 'foo.*bar' issued;UserWarning;" 1370 | "UserWarning;foo.*bar" 1371 | ) 1372 | with _assert_raises_assertion(expected): 1373 | msg_fmt = "{msg};{exc_type.__name__};{exc_name};{pattern}" 1374 | with assert_warns_regex(UserWarning, r"foo.*bar", msg_fmt=msg_fmt): 1375 | pass 1376 | 1377 | # assert_json_subset() 1378 | 1379 | def test_assert_json_subset__different_types(self): 1380 | with _assert_raises_assertion("element $ differs: {} != []"): 1381 | assert_json_subset({}, []) 1382 | 1383 | def test_assert_json_subset__empty_objects(self): 1384 | with assert_succeeds(AssertionError): 1385 | assert_json_subset({}, {}) 1386 | 1387 | def test_assert_json_subset__objects_equal(self): 1388 | with assert_succeeds(AssertionError): 1389 | assert_json_subset( 1390 | {"foo": 3, "bar": "abc"}, {"bar": "abc", "foo": 3} 1391 | ) 1392 | 1393 | def test_assert_json_subset__one_key_missing_from_first_object(self): 1394 | with assert_succeeds(AssertionError): 1395 | assert_json_subset({"foo": 3}, {"foo": 3, "bar": 3}) 1396 | 1397 | def test_assert_json_subset__one_key_missing_from_second_object(self): 1398 | with _assert_raises_assertion("element 'bar' missing from element $"): 1399 | assert_json_subset({"foo": 3, "bar": 3}, {"foo": 3}) 1400 | 1401 | def test_assert_json_subset__multiple_keys_missing_from_second_object( 1402 | self, 1403 | ): 1404 | with _assert_raises_assertion( 1405 | "elements 'bar', 'baz', and 'foo' missing from element $" 1406 | ): 1407 | assert_json_subset({"foo": 3, "bar": 3, "baz": 3}, {}) 1408 | 1409 | def test_assert_json_subset__value_differs(self): 1410 | with _assert_raises_assertion("element $['foo'] differs: 3 != 4"): 1411 | assert_json_subset({"foo": 3}, {"foo": 4}) 1412 | 1413 | def test_assert_json_subset__empty_lists(self): 1414 | with assert_succeeds(AssertionError): 1415 | assert_json_subset([], []) 1416 | 1417 | def test_assert_json_subset__different_sized_lists(self): 1418 | with _assert_raises_assertion("JSON array $ differs in size: 2 != 1"): 1419 | assert_json_subset([1, 2], [1]) 1420 | with _assert_raises_assertion("JSON array $ differs in size: 1 != 2"): 1421 | assert_json_subset([1], [1, 2]) 1422 | 1423 | def test_assert_json_subset__different_list_values(self): 1424 | with _assert_raises_assertion("element $[0] differs: {} != []"): 1425 | assert_json_subset([{}], [[]]) 1426 | 1427 | def test_assert_json_subset__fundamental_types_differ(self): 1428 | with _assert_raises_assertion("element $[0] differs: 1 != 'foo'"): 1429 | assert_json_subset([1], ["foo"]) 1430 | 1431 | def test_assert_json_subset__fundamental_values_differ(self): 1432 | with _assert_raises_assertion("element $[0] differs: 'bar' != 'foo'"): 1433 | assert_json_subset(["bar"], ["foo"]) 1434 | 1435 | def test_assert_json_subset__none(self): 1436 | with assert_succeeds(AssertionError): 1437 | assert_json_subset([None], [None]) 1438 | with _assert_raises_assertion("element $[0] differs: 42 != None"): 1439 | assert_json_subset([42], [None]) 1440 | with _assert_raises_assertion("element $[0] differs: None != 42"): 1441 | assert_json_subset([None], [42]) 1442 | 1443 | def test_assert_json_subset__compare_int_and_float(self): 1444 | with assert_succeeds(AssertionError): 1445 | assert_json_subset([42], [42.0]) 1446 | assert_json_subset([42.0], [42]) 1447 | 1448 | def test_assert_json_subset__unsupported_type(self): 1449 | msg = "unsupported type <{} 'set'>".format(self._type_string) 1450 | with assert_raises_regex(TypeError, msg): 1451 | assert_json_subset([set()], [set()]) 1452 | 1453 | def test_assert_json_subset__subtypes(self): 1454 | with assert_succeeds(AssertionError): 1455 | assert_json_subset(OrderedDict(), {}) 1456 | assert_json_subset({}, OrderedDict()) 1457 | 1458 | def test_assert_json_subset__second_is_string(self): 1459 | with assert_succeeds(AssertionError): 1460 | assert_json_subset({}, "{ }") 1461 | 1462 | def test_assert_json_subset__second_is_unsupported_json_string(self): 1463 | msg = "second must decode to dict or list, not <{} 'int'>".format( 1464 | self._type_string 1465 | ) 1466 | with _assert_raises_assertion(msg): 1467 | assert_json_subset({}, "42") 1468 | 1469 | def test_assert_json_subset__second_is_invalid_json_string(self): 1470 | with assert_raises(JSONDecodeError): 1471 | assert_json_subset({}, ",") 1472 | 1473 | def test_assert_json_subset__second_is_bytes(self): 1474 | with assert_succeeds(AssertionError): 1475 | assert_json_subset(["föo"], '["föo"]'.encode("utf-8")) 1476 | 1477 | def test_assert_json_subset__second_is_latin1_bytes(self): 1478 | with assert_raises(UnicodeDecodeError): 1479 | assert_json_subset(["föo"], '["föo"]'.encode("iso-8859-1")) 1480 | 1481 | def test_assert_json_subset__invalid_type(self): 1482 | with assert_raises_regex( 1483 | TypeError, "second must be dict, list, str, or bytes" 1484 | ): 1485 | assert_json_subset({}, 42) # type: ignore[arg-type] 1486 | 1487 | def test_assert_json_subset__element_name_not_str(self) -> None: 1488 | with assert_raises_regex( 1489 | TypeError, 1490 | "12 is not a valid object member name", 1491 | ): 1492 | assert_json_subset({12: 34}, "{}") 1493 | 1494 | def test_assert_json_subset__presence_check(self) -> None: 1495 | with assert_succeeds(AssertionError): 1496 | assert_json_subset({"foo": Present}, {"foo": "bar"}) 1497 | with assert_succeeds(AssertionError): 1498 | assert_json_subset({"foo": Present()}, {"foo": "bar"}) 1499 | with assert_raises_regex( 1500 | AssertionError, 1501 | r"element 'foo' missing from element \$", 1502 | ): 1503 | assert_json_subset({"foo": Present}, {}) 1504 | with assert_raises_regex( 1505 | AssertionError, 1506 | r"element 'foo' missing from element \$", 1507 | ): 1508 | assert_json_subset({"foo": Present()}, {}) 1509 | with assert_succeeds(AssertionError): 1510 | assert_json_subset({"foo": Absent}, {}) 1511 | with assert_succeeds(AssertionError): 1512 | assert_json_subset({"foo": Absent()}, {}) 1513 | with assert_raises_regex( 1514 | AssertionError, 1515 | r"spurious member 'foo' in object \$", 1516 | ): 1517 | assert_json_subset({"foo": Absent}, {"foo": "bar"}) 1518 | with assert_raises_regex( 1519 | AssertionError, 1520 | r"spurious member 'foo' in object \$", 1521 | ): 1522 | assert_json_subset({"foo": Absent()}, {"foo": "bar"}) 1523 | 1524 | def test_assert_json_subset__existence_check_old(self) -> None: 1525 | with catch_warnings(): 1526 | simplefilter("ignore") 1527 | with assert_succeeds(AssertionError): 1528 | assert_json_subset({Exists("foo"): True}, {"foo": "bar"}) 1529 | with assert_raises_regex( 1530 | AssertionError, 1531 | r"element 'foo' missing from element \$", 1532 | ): 1533 | assert_json_subset({Exists("foo"): True}, {}) 1534 | with assert_succeeds(AssertionError): 1535 | assert_json_subset({Exists("foo"): False}, {}) 1536 | with assert_raises_regex( 1537 | AssertionError, 1538 | r"spurious member 'foo' in object \$", 1539 | ): 1540 | assert_json_subset({Exists("foo"): False}, {"foo": "bar"}) 1541 | --------------------------------------------------------------------------------