Skip to content

Commit f226043

Browse files
committed
- finish support for FassertInterface
- extend readme - fix and extend tests
1 parent 05a5a37 commit f226043

File tree

8 files changed

+176
-23
lines changed

8 files changed

+176
-23
lines changed

README.md

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
[![PyPI version](https://badge.fury.io/py/fassert.svg)](https://badge.fury.io/py/fassert)
2+
![No dependencies](https://img.shields.io/badge/ZERO-Dependencies-blue)
3+
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
4+
15
^fassert$: Fuzzy assert
26
---------------------
37

@@ -38,7 +42,9 @@ try:
3842
fassert([1, 2, 3], [4])
3943
fassert("string", b"string") # bytes != string in fassert
4044
except AssertionError:
41-
pass # All the examples within the try block above will raise this exception
45+
pass
46+
else:
47+
raise RuntimeError("All examples within the try block must raise the AssertionError")
4248
```
4349

4450
In fassert, you can define a template to match your data against.
@@ -90,3 +96,36 @@ Bellow is an overview of the configurable options and their default values
9096
| regex_allowed | True | Enable matching regexes from template agains strings in the data |
9197
| fuzzy_sequence_types | False | Ignore types of similar sequence types when matching the template (e.g. tuple vs list) |
9298
| check_minimum_sequence_length | True | Check that the data has a minimum length of greater or equal to the template |
99+
100+
101+
You can also define custom data types such as following:
102+
103+
```
104+
from fassert import fassert, FassertInterface, FuzzyAssert
105+
106+
class IsNumberEvenOrEqual(FassertInterface):
107+
def __init__(self, value):
108+
self.value = value
109+
110+
def __fassert__(self, other: Any, matcher: FuzzyAssert, as_template: bool) -> Literal[True]:
111+
if self.value == other:
112+
return True
113+
elif isinstance(other, (int, float)) and int(other) % 2 == 0:
114+
return True
115+
116+
raise AssertionError("Data does not match the template")
117+
118+
119+
# In these examples the parameter `as_template` would be set to True as the data type is used as a template for matching
120+
fassert(10, IsNumberEvenOrEqual(15))
121+
fassert(15, IsNumberEvenOrEqual(15))
122+
fassert(42.0, IsNumberEvenOrEqual(15))
123+
124+
try:
125+
fassert(15, IsNumberEvenOrEqual(17))
126+
fassert("some_string", IsNumberEvenOrEqual(15))
127+
except AssertionError:
128+
pass
129+
else:
130+
raise RuntimeError("All examples within the try block must raise the AssertionError")
131+
```

fassert/__init__.py

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import inspect
22
from abc import ABC, abstractmethod
3-
from typing import Pattern, Sequence, Literal
3+
from typing import Pattern, Sequence, Literal, Any
44

5-
__version__ = "0.2"
5+
__version__ = "0.3"
66

77

88
class FuzzyAssert:
@@ -12,7 +12,7 @@ def __init__(self):
1212
self.check_minimum_sequence_length = True
1313
self.fuzzy_sequence_types = False
1414

15-
def match(self, data, template) -> Literal[True]:
15+
def match(self, data: Any, template: Any) -> Literal[True]:
1616
"""
1717
Attempt to match the template onto the data.
1818
Each item in a `template` must have a match in the `data`.
@@ -24,7 +24,12 @@ def match(self, data, template) -> Literal[True]:
2424
:return: True if `template` matches the `data`
2525
:rtype: Literal[True]
2626
"""
27-
if inspect.isfunction(template):
27+
if isinstance(template, FassertInterface):
28+
return template.__fassert__(other=data, matcher=self, as_template=True)
29+
elif isinstance(data, FassertInterface):
30+
return data.__fassert__(other=template, matcher=self, as_template=False)
31+
32+
elif inspect.isfunction(template):
2833
if self.eval_functions and template(data):
2934
return True
3035
raise AssertionError("Template function does not match the data")
@@ -41,8 +46,10 @@ def match(self, data, template) -> Literal[True]:
4146
return True
4247
else:
4348
raise AssertionError("Template does not match the data")
44-
elif isinstance(data, Sequence) and isinstance(template, Sequence) and (
45-
self.fuzzy_sequence_types or (type(data) == type(template))
49+
elif (
50+
isinstance(data, Sequence)
51+
and isinstance(template, Sequence)
52+
and (self.fuzzy_sequence_types or (type(data) == type(template)))
4653
):
4754
if self.check_minimum_sequence_length and len(template) > len(data):
4855
raise AssertionError(
@@ -69,7 +76,9 @@ def match(self, data, template) -> Literal[True]:
6976
return True
7077

7178
if type(data) != type(template):
72-
raise AssertionError(f"Template type `{type(template)}` does not match the type of data `{type(data)}`")
79+
raise AssertionError(
80+
f"Template type `{type(template)}` does not match the type of data `{type(data)}`"
81+
)
7382
elif isinstance(data, dict):
7483
for template_key, template_value in template.items():
7584
for data_key, data_value in data.items():
@@ -99,9 +108,21 @@ def match(self, data, template) -> Literal[True]:
99108

100109
class FassertInterface(ABC):
101110
@abstractmethod
102-
def __fassert__(self, other, matcher: FuzzyAssert) -> bool:
111+
def __fassert__(
112+
self, other: Any, matcher: FuzzyAssert, as_template: bool
113+
) -> Literal[True]:
114+
"""
115+
Add a support for matching custom defined types in a recursive way
116+
117+
:param other: other data structure to match against
118+
:param matcher: An instance of the FuzzyAssert object
119+
:param as_template: Flag to indicate if you are (self) the template matching the data or the other way around
120+
:raises AssertionError: When `self` does not match the `other`
121+
:return: True if data is matching otherwise an AssertionError is raised
122+
:rtype Literal[True]:
123+
"""
103124
...
104125

105126

106-
def fassert(data, template) -> bool:
127+
def fassert(data: Any, template: Any) -> Literal[True]:
107128
return FuzzyAssert().match(data, template)

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ version = attr: fassert.__version__
44
author = Martin Carnogursky
55
author_email = admin@sourcecode.ai
66
long_description = file: README.md
7+
long_description_content_type = text/markdown
78
url = https://github.com/SourceCode-AI/fassert
89
classifiers =
910
Intended Audience :: Developers

tests/test_configuration.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,11 @@ def test_fuzzy_sequence_types(self):
3737
def test_check_minimum_sequence_length(self):
3838
test_data = (
3939
([""], ["", ""]),
40-
(["test"], [re.compile(".{4}"), "test", re.compile("^test$")])
40+
(["test"], [re.compile(".{4}"), "test", re.compile("^test$")]),
4141
)
4242

4343
fasserter = FuzzyAssert()
4444

45-
4645
for data, template in test_data:
4746
with self.subTest(data=data, template=template):
4847
fasserter.check_minimum_sequence_length = True
@@ -51,13 +50,12 @@ def test_check_minimum_sequence_length(self):
5150
fasserter.check_minimum_sequence_length = False
5251
self.assertIs(fasserter.match(data, template), True)
5352

54-
5553
def test_regex_allowed(self):
5654
test_data = (
5755
("", re.compile("")),
5856
("test", re.compile("^test$")),
5957
("test", re.compile("test")),
60-
("test", re.compile(".{4}"))
58+
("test", re.compile(".{4}")),
6159
)
6260

6361
fasserter = FuzzyAssert()

tests/test_custom_types.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import unittest
2+
3+
from fassert import fassert, FuzzyAssert, FassertInterface
4+
5+
6+
class ByteStringMatcher(FassertInterface):
7+
def __init__(self, value):
8+
self.value = value
9+
10+
def __fassert__(self, other, matcher, as_template):
11+
if isinstance(self.value, bytes) and isinstance(other, str):
12+
if self.value == other.encode():
13+
return True
14+
elif isinstance(self.value, str) and isinstance(other, bytes):
15+
if self.value.encode() == other:
16+
return True
17+
elif self.value == other:
18+
return True
19+
20+
raise AssertionError("Data does not match the template")
21+
22+
def __repr__(self):
23+
return f"<ByteStringMatcher({repr(self.value)})>"
24+
25+
26+
class CheckIsTemplate(FassertInterface):
27+
def __fassert__(self, other, matcher, as_template):
28+
if as_template is not True:
29+
raise AssertionError("This must be matched as template")
30+
return True
31+
32+
33+
class CustomTypesTest(unittest.TestCase):
34+
def test_byte_string_matcher(self):
35+
test_data = (
36+
(b"test", "test"),
37+
(b"", ""),
38+
)
39+
40+
for data, template in test_data:
41+
with self.subTest(data=data, template=template):
42+
# Normally these types would not match
43+
self.assertRaises(AssertionError, fassert, data, template)
44+
# Our custom interface fuzzy matches the bytes to string
45+
self.assertIs(fassert(ByteStringMatcher(data), template), True)
46+
47+
with self.subTest(data=template, template=data):
48+
self.assertRaises(AssertionError, fassert, template, data)
49+
self.assertIs(fassert(ByteStringMatcher(template), data), True)
50+
51+
def test_check_is_template(self):
52+
checker = CheckIsTemplate()
53+
54+
for data in ("", [], {}, 42, b"test", set()):
55+
with self.subTest(data=data):
56+
self.assertIs(fassert(data, checker), True)
57+
self.assertRaises(AssertionError, fassert, checker, data)

tests/test_iterables.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def test_exact_list(self):
1616
("a", "42", None),
1717
set(),
1818
{"a"},
19-
{"a", 42, None}
19+
{"a", 42, None},
2020
)
2121
for data in test_data:
2222
with self.subTest(data=data):

tests/test_misc.py

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,81 @@
11
import re
22
import unittest
3+
from typing import Literal, Any
34

45

56
class ReadmeTest(unittest.TestCase):
67
# Copy-paste the example code from README into this test
78
def test_readme_main_example(self):
89
from fassert import fassert
910

10-
1111
# Usage: fassert(<data>, <template>)
1212

1313
fassert("This is expected", "This is expected")
1414

1515
fassert("This matches as well", re.compile(r"[thismachewl ]+", re.I))
1616

1717
fassert(
18-
{"key": "value", "key2": "value2"},
19-
{"key": "value"}
18+
{"key": "value", "key2": "value2"}, {"key": "value"}
2019
) # key2: value2 is ignored as it's not defined in the template
2120

2221
fassert(
2322
{"key": "value", "abc": "value"},
2423
# You can nest and combine the fuzzy matching types in containers
25-
{re.compile(r"[a-z]{3}"): "value"}
24+
{re.compile(r"[a-z]{3}"): "value"},
2625
)
2726

2827
fassert(
2928
[1, {"very": {"nested": {"dictionary": "data"}}}, {"not": "this"}],
3029
# Isn't this cool?
31-
[{"very": {re.compile(".{6}"): {"dictionary": lambda x: len(x) == 4}}}]
30+
[{"very": {re.compile(".{6}"): {"dictionary": lambda x: len(x) == 4}}}],
3231
)
3332

3433
# Template can contain callables as well
3534
fassert("value", lambda x: x == "value")
3635

3736
try:
3837
fassert("expected", "to not match")
39-
fassert({"a": "b"}, {"c": "d"}) # This will fail, {"c":"d"} is not in the target data
38+
fassert(
39+
{"a": "b"}, {"c": "d"}
40+
) # This will fail, {"c":"d"} is not in the target data
4041
fassert([1, 2, 3], [4])
4142
fassert("string", b"string") # bytes != string in fassert
4243
except AssertionError:
43-
pass # All the examples within the try block above will raise this exception
44+
pass
45+
else:
46+
raise RuntimeError(
47+
"All examples within the try block must raise the AssertionError"
48+
)
49+
50+
# Copy paste the custom type example in the advanced usage section over here
51+
def test_advanced_custom_type_example(self):
52+
from fassert import fassert, FassertInterface, FuzzyAssert
53+
54+
class IsNumberEvenOrEqual(FassertInterface):
55+
def __init__(self, value):
56+
self.value = value
57+
58+
def __fassert__(
59+
self, other: Any, matcher: FuzzyAssert, as_template: bool
60+
) -> Literal[True]:
61+
if self.value == other:
62+
return True
63+
elif isinstance(other, (int, float)) and int(other) % 2 == 0:
64+
return True
65+
66+
raise AssertionError("Data does not match the template")
67+
68+
# In these examples the parameter `as_template` would be set to True as the data type is used as a template for matching
69+
fassert(10, IsNumberEvenOrEqual(15))
70+
fassert(15, IsNumberEvenOrEqual(15))
71+
fassert(42.0, IsNumberEvenOrEqual(15))
72+
73+
try:
74+
fassert(15, IsNumberEvenOrEqual(17))
75+
fassert("some_string", IsNumberEvenOrEqual(15))
76+
except AssertionError:
77+
pass
78+
else:
79+
raise RuntimeError(
80+
"All examples within the try block must raise the AssertionError"
81+
)

tests/test_primitives.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ def test_same_type_different_value_not_matching(self):
4444
with self.subTest(data=data, template=template):
4545
self.assertRaises(AssertionError, fassert, data, template)
4646

47-
4847
def test_different_types_not_matching(self):
4948
test_data = (
5049
("", ()),

0 commit comments

Comments
 (0)