assertpy2 2.3.2__py3-none-any.whl → 2.3.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- assertpy2/__init__.py +3 -1
- assertpy2/assertpy.py +1 -1
- assertpy2/matchers.py +67 -0
- assertpy2/string.py +90 -0
- {assertpy2-2.3.2.dist-info → assertpy2-2.3.3.dist-info}/METADATA +51 -2
- {assertpy2-2.3.2.dist-info → assertpy2-2.3.3.dist-info}/RECORD +9 -9
- {assertpy2-2.3.2.dist-info → assertpy2-2.3.3.dist-info}/WHEEL +0 -0
- {assertpy2-2.3.2.dist-info → assertpy2-2.3.3.dist-info}/entry_points.txt +0 -0
- {assertpy2-2.3.2.dist-info → assertpy2-2.3.3.dist-info}/licenses/LICENSE +0 -0
assertpy2/__init__.py
CHANGED
|
@@ -12,7 +12,7 @@ from .assertpy import (
|
|
|
12
12
|
from .async_assertions import AsyncAssertionBuilder
|
|
13
13
|
from .errors import AssertionFailure, DiffEntry, DiffResult
|
|
14
14
|
from .file import contents_of
|
|
15
|
-
from .matchers import Matcher, match
|
|
15
|
+
from .matchers import Matcher, match, register_matcher, unregister_matcher
|
|
16
16
|
|
|
17
17
|
__all__ = [
|
|
18
18
|
"AssertionFailure",
|
|
@@ -28,7 +28,9 @@ __all__ = [
|
|
|
28
28
|
"contents_of",
|
|
29
29
|
"fail",
|
|
30
30
|
"match",
|
|
31
|
+
"register_matcher",
|
|
31
32
|
"remove_extension",
|
|
32
33
|
"soft_assertions",
|
|
33
34
|
"soft_fail",
|
|
35
|
+
"unregister_matcher",
|
|
34
36
|
]
|
assertpy2/assertpy.py
CHANGED
|
@@ -73,7 +73,7 @@ from .numeric import NumericMixin
|
|
|
73
73
|
from .snapshot import SnapshotMixin
|
|
74
74
|
from .string import StringMixin
|
|
75
75
|
|
|
76
|
-
__version__ = "2.3.
|
|
76
|
+
__version__ = "2.3.3"
|
|
77
77
|
|
|
78
78
|
__tracebackhide__ = True # clean tracebacks via py.test integration
|
|
79
79
|
contextlib.__tracebackhide__ = True # ty: ignore[unresolved-attribute] # pytest monkey-patch
|
assertpy2/matchers.py
CHANGED
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
4
|
import uuid as _uuid_mod
|
|
5
|
+
from collections.abc import Callable
|
|
5
6
|
from typing import Any, Protocol, runtime_checkable
|
|
6
7
|
|
|
7
8
|
|
|
@@ -547,6 +548,65 @@ class StructureMatcher(BaseMatcher):
|
|
|
547
548
|
return f"{{{', '.join(parts)}}}"
|
|
548
549
|
|
|
549
550
|
|
|
551
|
+
# --- Custom matcher registry ---
|
|
552
|
+
|
|
553
|
+
_custom_matchers: dict[str, Callable[..., BaseMatcher]] = {}
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
def register_matcher(name: str) -> Callable[[Callable[..., BaseMatcher]], Callable[..., BaseMatcher]]:
|
|
557
|
+
"""Register a custom matcher factory on the ``match`` namespace.
|
|
558
|
+
|
|
559
|
+
Args:
|
|
560
|
+
name: the name to register on ``match`` (e.g. ``"is_valid_email"``)
|
|
561
|
+
|
|
562
|
+
Returns:
|
|
563
|
+
A decorator that registers the wrapped function and returns it unchanged.
|
|
564
|
+
|
|
565
|
+
Examples:
|
|
566
|
+
Register a simple matcher::
|
|
567
|
+
|
|
568
|
+
@register_matcher("is_valid_email")
|
|
569
|
+
def is_valid_email():
|
|
570
|
+
return match.matches_regex(r"^[\\w.-]+@[\\w.-]+\\.\\w+$")
|
|
571
|
+
|
|
572
|
+
assert_that(email).satisfies(match.is_valid_email())
|
|
573
|
+
|
|
574
|
+
Register a parametrised matcher::
|
|
575
|
+
|
|
576
|
+
@register_matcher("has_status")
|
|
577
|
+
def has_status(expected: str):
|
|
578
|
+
return match.has_property("status", match.equal_to(expected))
|
|
579
|
+
|
|
580
|
+
assert_that(order).satisfies(match.has_status("active"))
|
|
581
|
+
"""
|
|
582
|
+
if not isinstance(name, str):
|
|
583
|
+
raise TypeError("name must be a string")
|
|
584
|
+
if not name.isidentifier():
|
|
585
|
+
raise ValueError(f"name must be a valid Python identifier, got {name!r}")
|
|
586
|
+
|
|
587
|
+
def decorator(func: Callable[..., BaseMatcher]) -> Callable[..., BaseMatcher]:
|
|
588
|
+
if not callable(func):
|
|
589
|
+
raise TypeError("func must be callable")
|
|
590
|
+
_custom_matchers[name] = func
|
|
591
|
+
return func
|
|
592
|
+
|
|
593
|
+
return decorator
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def unregister_matcher(name: str) -> None:
|
|
597
|
+
"""Remove a previously registered custom matcher.
|
|
598
|
+
|
|
599
|
+
Args:
|
|
600
|
+
name: the matcher name to remove
|
|
601
|
+
|
|
602
|
+
Raises:
|
|
603
|
+
KeyError: if the name is not registered
|
|
604
|
+
"""
|
|
605
|
+
if name not in _custom_matchers:
|
|
606
|
+
raise KeyError(f"no custom matcher registered with name {name!r}")
|
|
607
|
+
del _custom_matchers[name]
|
|
608
|
+
|
|
609
|
+
|
|
550
610
|
# --- Namespace ---
|
|
551
611
|
|
|
552
612
|
|
|
@@ -705,5 +765,12 @@ class _MatchNamespace:
|
|
|
705
765
|
def structure(spec: dict) -> StructureMatcher:
|
|
706
766
|
return StructureMatcher(spec)
|
|
707
767
|
|
|
768
|
+
def __getattr__(self, name: str) -> Callable[..., BaseMatcher]:
|
|
769
|
+
try:
|
|
770
|
+
factory = _custom_matchers[name]
|
|
771
|
+
except KeyError:
|
|
772
|
+
raise AttributeError(f"match has no matcher {name!r}") from None
|
|
773
|
+
return factory
|
|
774
|
+
|
|
708
775
|
|
|
709
776
|
match = _MatchNamespace()
|
assertpy2/string.py
CHANGED
|
@@ -519,3 +519,93 @@ class StringMixin(_MixinBase):
|
|
|
519
519
|
if not isinstance(self.val, str):
|
|
520
520
|
return self.error(f"Expected <{self.val}> to be unicode, but was <{type(self.val).__name__}>.")
|
|
521
521
|
return self
|
|
522
|
+
|
|
523
|
+
def extracting_group(self, pattern: str, group: int | str = 0) -> Self:
|
|
524
|
+
"""Search val for ``pattern`` and return a new builder whose val is the captured group.
|
|
525
|
+
|
|
526
|
+
Args:
|
|
527
|
+
pattern: the regular expression pattern (must contain at least one group)
|
|
528
|
+
group: the group index (int) or name (str) to extract. Defaults to ``0``
|
|
529
|
+
(the entire match).
|
|
530
|
+
|
|
531
|
+
Examples:
|
|
532
|
+
Usage with positional groups::
|
|
533
|
+
|
|
534
|
+
assert_that("status=200 path=/api").extracting_group(r"status=(\\d+)", 1).is_equal_to("200")
|
|
535
|
+
|
|
536
|
+
Usage with named groups::
|
|
537
|
+
|
|
538
|
+
assert_that("2024-01-15 ERROR").extracting_group(
|
|
539
|
+
r"(?P<level>\\w+)$", "level"
|
|
540
|
+
).is_equal_to("ERROR")
|
|
541
|
+
|
|
542
|
+
Returns:
|
|
543
|
+
AssertionBuilder: a **new** builder whose val is the extracted group string
|
|
544
|
+
|
|
545
|
+
Raises:
|
|
546
|
+
TypeError: if val is not a string or pattern is not a string
|
|
547
|
+
ValueError: if pattern is empty
|
|
548
|
+
AssertionError: if the pattern does not match val or the group does not exist
|
|
549
|
+
"""
|
|
550
|
+
if not isinstance(self.val, str):
|
|
551
|
+
raise TypeError("val is not a string")
|
|
552
|
+
if not isinstance(pattern, str):
|
|
553
|
+
raise TypeError("given pattern arg must be a string")
|
|
554
|
+
if len(pattern) == 0:
|
|
555
|
+
raise ValueError("given pattern arg must not be empty")
|
|
556
|
+
m = re.search(pattern, self.val)
|
|
557
|
+
if m is None:
|
|
558
|
+
return self.error(f"Expected <{self.val}> to match pattern <{pattern}>, but did not.")
|
|
559
|
+
try:
|
|
560
|
+
extracted = m.group(group)
|
|
561
|
+
except IndexError:
|
|
562
|
+
return self.error(f"Expected pattern <{pattern}> to have group <{group}>, but it does not.")
|
|
563
|
+
if extracted is None:
|
|
564
|
+
return self.error(
|
|
565
|
+
f"Expected group <{group}> of pattern <{pattern}> to be matched in <{self.val}>, but it was not."
|
|
566
|
+
)
|
|
567
|
+
return self.builder(extracted, self.description, self.kind)
|
|
568
|
+
|
|
569
|
+
def matches_with_groups(self, pattern: str) -> Self:
|
|
570
|
+
"""Search val for ``pattern`` and return a new builder whose val is the tuple of all groups.
|
|
571
|
+
|
|
572
|
+
If the pattern contains **named** groups, the builder val is a ``dict``
|
|
573
|
+
of ``{name: value}`` for all named groups. Otherwise it is the
|
|
574
|
+
``tuple`` returned by ``Match.groups()``.
|
|
575
|
+
|
|
576
|
+
Args:
|
|
577
|
+
pattern: the regular expression pattern with one or more groups
|
|
578
|
+
|
|
579
|
+
Examples:
|
|
580
|
+
Positional groups::
|
|
581
|
+
|
|
582
|
+
assert_that("2024-01-15 ERROR").matches_with_groups(
|
|
583
|
+
r"(\\d{4}-\\d{2}-\\d{2}) (\\w+)"
|
|
584
|
+
).is_length(2)
|
|
585
|
+
|
|
586
|
+
Named groups::
|
|
587
|
+
|
|
588
|
+
assert_that("status=200").matches_with_groups(
|
|
589
|
+
r"(?P<key>\\w+)=(?P<val>\\w+)"
|
|
590
|
+
).contains_key("key").contains_key("val")
|
|
591
|
+
|
|
592
|
+
Returns:
|
|
593
|
+
AssertionBuilder: a **new** builder whose val is the groups tuple or groupdict
|
|
594
|
+
|
|
595
|
+
Raises:
|
|
596
|
+
TypeError: if val is not a string or pattern is not a string
|
|
597
|
+
ValueError: if pattern is empty
|
|
598
|
+
AssertionError: if the pattern does not match val
|
|
599
|
+
"""
|
|
600
|
+
if not isinstance(self.val, str):
|
|
601
|
+
raise TypeError("val is not a string")
|
|
602
|
+
if not isinstance(pattern, str):
|
|
603
|
+
raise TypeError("given pattern arg must be a string")
|
|
604
|
+
if len(pattern) == 0:
|
|
605
|
+
raise ValueError("given pattern arg must not be empty")
|
|
606
|
+
m = re.search(pattern, self.val)
|
|
607
|
+
if m is None:
|
|
608
|
+
return self.error(f"Expected <{self.val}> to match pattern <{pattern}>, but did not.")
|
|
609
|
+
groupdict = m.groupdict()
|
|
610
|
+
result = groupdict if groupdict else m.groups()
|
|
611
|
+
return self.builder(result, self.description, self.kind)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: assertpy2
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.3
|
|
4
4
|
Summary: Fluent assertion library for Python with composable matchers, structural matching, and full type safety
|
|
5
5
|
Project-URL: Homepage, https://github.com/Solganis/assertpy2
|
|
6
6
|
Project-URL: Repository, https://github.com/Solganis/assertpy2
|
|
@@ -52,9 +52,11 @@ Description-Content-Type: text/markdown
|
|
|
52
52
|
<a href="https://pepy.tech/projects/assertpy2"><img src="https://static.pepy.tech/badge/assertpy2/month" alt="Downloads"></a>
|
|
53
53
|
<a href="https://pypi.org/project/assertpy2/"><img src="https://img.shields.io/pypi/pyversions/assertpy2" alt="Python"></a>
|
|
54
54
|
<a href="https://codecov.io/gh/Solganis/assertpy2"><img src="https://codecov.io/gh/Solganis/assertpy2/graph/badge.svg" alt="Coverage"></a>
|
|
55
|
+
<br>
|
|
56
|
+
<a href="https://github.com/Solganis/assertpy2/blob/main/docs/api.md"><img src="https://img.shields.io/badge/Docs-Read%20The%20Docs-black" alt="Documentation"></a>
|
|
55
57
|
<a href="https://docs.astral.sh/ruff/"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json" alt="Ruff"></a>
|
|
56
58
|
<a href="https://github.com/astral-sh/uv"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json" alt="uv"></a>
|
|
57
|
-
<a href="https://github.com/astral-sh/ty"><img src="https://img.shields.io/
|
|
59
|
+
<a href="https://github.com/astral-sh/ty"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ty/main/assets/badge/v0.json" alt="ty"></a>
|
|
58
60
|
<a href="https://scorecard.dev/viewer/?uri=github.com/Solganis/assertpy2"><img src="https://api.scorecard.dev/projects/github.com/Solganis/assertpy2/badge" alt="OpenSSF Scorecard"></a>
|
|
59
61
|
<a href="https://www.bestpractices.dev/projects/12990"><img src="https://www.bestpractices.dev/projects/12990/badge" alt="OpenSSF Best Practices"></a>
|
|
60
62
|
</p>
|
|
@@ -173,6 +175,8 @@ assert_that(items).is_type_of(list).is_length(3).contains("admin")
|
|
|
173
175
|
- **Snapshot testing**: store and compare data structures in JSON format, inspired by Jest.
|
|
174
176
|
- **Allure integration**: auto-attach structured diff and actual/expected data to Allure reports.
|
|
175
177
|
- **Behave step matchers**: ready-made parameter types (`PositiveInt`, `BoolLike`, etc.) for Behave step definitions.
|
|
178
|
+
- **Custom matchers**: register domain-specific matchers via `register_matcher()`, composable with `&`, `|`, `~`.
|
|
179
|
+
- **Regex group extraction**: `extracting_group()` and `matches_with_groups()` to assert on regex captures fluently.
|
|
176
180
|
- **Extensions**: add custom assertions via `add_extension()`.
|
|
177
181
|
- Strings, numbers, lists, tuples, sets, dicts, dates, booleans, objects, exceptions.
|
|
178
182
|
|
|
@@ -348,6 +352,51 @@ assert_that(fred).has_shoe_size(12)
|
|
|
348
352
|
assert_that({"a": 1, "b": 2, "c": 3}).snapshot()
|
|
349
353
|
```
|
|
350
354
|
|
|
355
|
+
### Custom matchers
|
|
356
|
+
|
|
357
|
+
Register domain-specific matchers on the `match` namespace with `register_matcher()`:
|
|
358
|
+
|
|
359
|
+
```py
|
|
360
|
+
from assertpy2 import assert_that, match, register_matcher
|
|
361
|
+
|
|
362
|
+
@register_matcher("is_valid_email")
|
|
363
|
+
def is_valid_email():
|
|
364
|
+
return match.matches_regex(r"^[\w.-]+@[\w.-]+\.\w+$")
|
|
365
|
+
|
|
366
|
+
# parametrised matchers
|
|
367
|
+
@register_matcher("has_status")
|
|
368
|
+
def has_status(expected: str):
|
|
369
|
+
return match.has_property("status", match.equal_to(expected))
|
|
370
|
+
|
|
371
|
+
# use everywhere matchers are accepted
|
|
372
|
+
assert_that("alice@example.com").satisfies(match.is_valid_email())
|
|
373
|
+
assert_that(users).extracting("email").each(match.is_valid_email())
|
|
374
|
+
assert_that(data).matches_structure({"email": match.is_valid_email()})
|
|
375
|
+
|
|
376
|
+
# composition works automatically
|
|
377
|
+
assert_that(email).satisfies(match.is_valid_email() & match.contains_string("@company.com"))
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
Remove with `unregister_matcher("is_valid_email")`.
|
|
381
|
+
|
|
382
|
+
### Regex group extraction
|
|
383
|
+
|
|
384
|
+
Extract regex groups and continue the fluent chain:
|
|
385
|
+
|
|
386
|
+
```py
|
|
387
|
+
log = "2024-01-15 ERROR status=500 path=/api/users"
|
|
388
|
+
|
|
389
|
+
# extract a positional group
|
|
390
|
+
assert_that(log).extracting_group(r"status=(\d+)", 1).is_equal_to("500")
|
|
391
|
+
|
|
392
|
+
# extract a named group
|
|
393
|
+
assert_that(log).extracting_group(r"(?P<level>\w+) status", "level").is_equal_to("ERROR")
|
|
394
|
+
|
|
395
|
+
# get all groups as a tuple or dict (named groups)
|
|
396
|
+
assert_that("key=value").matches_with_groups(r"(?P<k>\w+)=(?P<v>\w+)") \
|
|
397
|
+
.contains_entry({"k": "key"}).contains_entry({"v": "value"})
|
|
398
|
+
```
|
|
399
|
+
|
|
351
400
|
### Extensions
|
|
352
401
|
|
|
353
402
|
```py
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
assertpy2/__init__.py,sha256=
|
|
1
|
+
assertpy2/__init__.py,sha256=QAaBt_YH4owfEJ3BybdaBLuZlY2IUqs8dW0kWb0e2tA,791
|
|
2
2
|
assertpy2/_mixin_base.py,sha256=3g_bcRPNjZI6waSfvDcBXlCSuSQu7oc3E8qssJ4bTjU,2485
|
|
3
3
|
assertpy2/_typing.py,sha256=lHwVYbqdT8KXEd9UM5X40OmoTXY9wX74AXHFMUN42k0,8857
|
|
4
|
-
assertpy2/assertpy.py,sha256=
|
|
4
|
+
assertpy2/assertpy.py,sha256=HmGlrfvolnsK6DgVPq-Pfn0eFP6YOkidZJDkF3Mu-6s,20089
|
|
5
5
|
assertpy2/async_assertions.py,sha256=LXscgWxLTcGT9uz0Z3WlCmMngS0-AdbqRb7b27u36-c,2838
|
|
6
6
|
assertpy2/base.py,sha256=uz9NXGxwmPS1RFBqOFMS3lOy1UvGehT5gZF4KYoqRs0,25554
|
|
7
7
|
assertpy2/behave_matchers.py,sha256=UoRqYh8RECJLe8w0k622rcudP95fXku4n7aXBiQxGG0,2292
|
|
@@ -15,14 +15,14 @@ assertpy2/exception.py,sha256=I_zAJx-wZtTQkB1a4EhL5sniAqypedQn4FtFQY5G0qk,6752
|
|
|
15
15
|
assertpy2/extracting.py,sha256=kYPLCif4_1MUlJNMSid7EcaTz7SuHzPedspk1Jc_ysw,10367
|
|
16
16
|
assertpy2/file.py,sha256=rVUofKCUOLyPzFQctxwOhF0OZCrx0Evb361Air4JynM,9575
|
|
17
17
|
assertpy2/helpers.py,sha256=sxj9NmbHsFOsY-FGgyYLHe7dVhwBT6qX9wT6QoYK7TE,13688
|
|
18
|
-
assertpy2/matchers.py,sha256=
|
|
18
|
+
assertpy2/matchers.py,sha256=k94k7i5N_GVnmrfAxegsLGFubHHcw57PFP3zCuNQ-wU,22607
|
|
19
19
|
assertpy2/numeric.py,sha256=sHiSxbXF3XD9A16dB0gcMxe2bTrtPKJCxe1OLIvjSGY,21950
|
|
20
20
|
assertpy2/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
21
|
assertpy2/pytest_plugin.py,sha256=H68GieYEX9n9Exsgmuo1Y93zOYV1oVPfAhvNuNu8d3M,3390
|
|
22
22
|
assertpy2/snapshot.py,sha256=ccdkeXXz11bhEL90iL0ohWhHYJA95K85vhs0y6an1ys,9092
|
|
23
|
-
assertpy2/string.py,sha256=
|
|
24
|
-
assertpy2-2.3.
|
|
25
|
-
assertpy2-2.3.
|
|
26
|
-
assertpy2-2.3.
|
|
27
|
-
assertpy2-2.3.
|
|
28
|
-
assertpy2-2.3.
|
|
23
|
+
assertpy2/string.py,sha256=0Cc_ow28gTHZRqNrz6T8zxhbsyIl1gs1JCamX6jRoV8,23621
|
|
24
|
+
assertpy2-2.3.3.dist-info/METADATA,sha256=sgWV29WffqbQ0QC9o36nPKBn6pizq_Ap8OA5kAOYiNI,18507
|
|
25
|
+
assertpy2-2.3.3.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
26
|
+
assertpy2-2.3.3.dist-info/entry_points.txt,sha256=G7urMRSkSAWVlne6Jzwv_5U5I-Ru4IKKEUb1xADEDCY,47
|
|
27
|
+
assertpy2-2.3.3.dist-info/licenses/LICENSE,sha256=tugsGV7w81349vK3IDCgB1FqfiqRZMcJiyWY5N55t4A,1564
|
|
28
|
+
assertpy2-2.3.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|