Skip to content

Commit 36878cd

Browse files
committed
Initial commit
0 parents  commit 36878cd

File tree

10 files changed

+361
-0
lines changed

10 files changed

+361
-0
lines changed

LICENSE.txt

Whitespace-only changes.

README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
fassert: Fuzzy assert
2+
---------------------
3+
4+
Assert in your tests only a subset of data that matters
5+
6+
```
7+
from fassert import fassert
8+
9+
# Usage: fassert(<data>, <template>)
10+
11+
fassert("This is expected", "This is expected")
12+
13+
fassert("This matches as well", re.compile(r"[thismachewl ]+", re.I))
14+
15+
fassert(
16+
{"key": "value", "key2": "value2"},
17+
{"key": "value"}
18+
) # key2: value2 is ignored as it's not defined in the template
19+
20+
fassert(
21+
{"key": "value"},
22+
# You can nest and combine the fuzzy matching types in containers
23+
{re.compile(r"[a-z]{3}"): "value"}
24+
)
25+
26+
# Template can contain callables as well
27+
fassert("value", lambda x: x == "value")
28+
29+
try:
30+
fassert({"a": "b"}, {"c": "d"}) # This will fail, {"c":"d"} is not in the target data
31+
except AssertionError:
32+
pass # All the examples within the try block above will raise this exception
33+
```
34+
35+
In fassert, you can define a template to match your data against.
36+
When you use a type that is a container (e.g. list, tuple, dict, etc...), then only the data that you defined in the template will be asserted.
37+
All the addition data in the container will be ignored

fassert/__init__.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import inspect
2+
from abc import ABC, abstractmethod
3+
from typing import Pattern, Sequence, Literal
4+
5+
6+
class FuzzyAssert:
7+
def __init__(self):
8+
self.eval_functions = True
9+
self.regex_allowed = True
10+
self.check_minimum_sequence_length = True
11+
self.fuzzy_sequence_types = False
12+
13+
def match(self, data, template) -> Literal[True]:
14+
"""
15+
Attempt to match the template onto the data.
16+
Each item in a `template` must have a match in the `data`.
17+
Extra items/data inside the `data` (if it is a container type) is ignored.
18+
19+
:param data: Data to match against, extra data, container types or other python types is ignored
20+
:param template: Template to match against, everything defined here must have a match
21+
:return: True if `template` matches the `data`
22+
"""
23+
if inspect.isfunction(template):
24+
if self.eval_functions and template(data):
25+
return True
26+
raise AssertionError("Template function does not match the data")
27+
elif self.regex_allowed and type(data) == str and isinstance(template, Pattern):
28+
if not template.match(data):
29+
raise AssertionError(
30+
"Template regex `{}` does not match the data".format(repr(template))
31+
)
32+
else:
33+
return True
34+
# This must be before generic test of Sequence types because str/bytes are also considered as sequences
35+
elif isinstance(data, (str, bytes)) and isinstance(template, (str, bytes)):
36+
if data == template:
37+
return True
38+
else:
39+
raise AssertionError("Template does not match the data")
40+
elif isinstance(data, Sequence) and isinstance(template, Sequence) and (
41+
self.fuzzy_sequence_types or (type(data) == type(template))
42+
):
43+
if self.check_minimum_sequence_length and len(template) > len(data):
44+
raise AssertionError(
45+
"Template sequence length is higher then the length of the data: `{}`".format(
46+
template
47+
)
48+
)
49+
elif len(template) == 0:
50+
return True
51+
52+
for template_item in template:
53+
for data_item in data:
54+
try:
55+
self.match(data_item, template_item)
56+
break
57+
except AssertionError:
58+
continue
59+
else:
60+
raise AssertionError(
61+
"Sequence item from the `template` not found inside the `data`: `{}`".format(
62+
template_item
63+
)
64+
)
65+
return True
66+
67+
if type(data) != type(template):
68+
raise AssertionError(f"Template type `{type(template)}` does not match the type of data `{type(data)}`")
69+
elif isinstance(data, dict):
70+
for template_key, template_value in template.items():
71+
for data_key, data_value in data.items():
72+
try:
73+
self.match(data_key, template_key)
74+
self.match(data_value, template_value)
75+
break
76+
except AssertionError:
77+
continue
78+
else:
79+
raise AssertionError(
80+
"Could not find a matching key/value for dictionary item `{}`:`{}`".format(
81+
repr(template_key), repr(template_value)
82+
)
83+
)
84+
return True
85+
else:
86+
if data == template:
87+
return True
88+
else:
89+
raise AssertionError(
90+
"Target data does not match the template: `{}`".format(
91+
repr(template)
92+
)
93+
)
94+
95+
96+
class FassertInterface(ABC):
97+
@abstractmethod
98+
def __fassert__(self, other, matcher: FuzzyAssert) -> bool:
99+
...
100+
101+
102+
def fassert(data, template) -> bool:
103+
return FuzzyAssert().match(data, template)

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[build-system]
2+
requires = ["setuptools>=42"]
3+
build-backend = "setuptools.build_meta"

setup.cfg

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[metadata]
2+
name = fassert
3+
version = 0.1
4+
author = Martin Carnogursky
5+
author_email = admin@sourcecode.ai
6+
7+
classifiers =
8+
Programming Language :: Python :: 3
9+
Operating System :: OS Independent
10+
11+
12+
[options]
13+
packages = find:
14+
python_requires = >=3.8

tests/__init__.py

Whitespace-only changes.

tests/test_configuration.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import unittest
2+
import re
3+
4+
from fassert import FuzzyAssert
5+
6+
7+
class ConfigurationTest(unittest.TestCase):
8+
def test_eval_function(self):
9+
data = "test"
10+
template = lambda x: len(x) == 4
11+
12+
fasserter = FuzzyAssert()
13+
14+
self.assertIs(fasserter.eval_functions, True)
15+
fasserter.match(data, template)
16+
17+
fasserter.eval_functions = False
18+
self.assertRaises(AssertionError, fasserter.match, data, template)
19+
20+
def test_fuzzy_sequence_types(self):
21+
test_data = (
22+
((), []),
23+
(set(), ()),
24+
(set(), []),
25+
)
26+
27+
fasserter = FuzzyAssert()
28+
self.assertIs(fasserter.fuzzy_sequence_types, False)
29+
30+
for data, template in test_data:
31+
with self.subTest(data=data, template=template):
32+
self.assertRaises(AssertionError, fasserter.match, data, template)
33+
34+
with self.subTest(data=template, template=data):
35+
self.assertRaises(AssertionError, fasserter.match, template, data)
36+
37+
def test_check_minimum_sequence_length(self):
38+
test_data = (
39+
([""], ["", ""]),
40+
(["test"], [re.compile(".{4}"), "test", re.compile("^test$")])
41+
)
42+
43+
fasserter = FuzzyAssert()
44+
45+
46+
for data, template in test_data:
47+
with self.subTest(data=data, template=template):
48+
fasserter.check_minimum_sequence_length = True
49+
self.assertRaises(AssertionError, fasserter.match, data, template)
50+
51+
fasserter.check_minimum_sequence_length = False
52+
self.assertIs(fasserter.match(data, template), True)
53+
54+
55+
def test_regex_allowed(self):
56+
test_data = (
57+
("", re.compile("")),
58+
("test", re.compile("^test$")),
59+
("test", re.compile("test")),
60+
("test", re.compile(".{4}"))
61+
)
62+
63+
fasserter = FuzzyAssert()
64+
65+
for data, template in test_data:
66+
with self.subTest(data=data, template=template):
67+
fasserter.regex_allowed = False
68+
self.assertRaises(AssertionError, fasserter.match, data, template)
69+
70+
fasserter.regex_allowed = True
71+
self.assertIs(fasserter.match(data, template), True)
72+
73+
with self.subTest(data=template, template=data):
74+
fasserter.regex_allowed = True
75+
# a string would never match regex, e.g. the regex matches are one way only from template to data
76+
self.assertRaises(AssertionError, fasserter.match, template, data)

tests/test_iterables.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import unittest
2+
3+
from fassert import fassert
4+
5+
6+
class IterablesTest(unittest.TestCase):
7+
def test_exact_list(self):
8+
test_data = (
9+
[],
10+
["a"],
11+
[42],
12+
["a", 42, None],
13+
(),
14+
("a",),
15+
(42),
16+
("a", "42", None),
17+
set(),
18+
{"a"},
19+
{"a", 42, None}
20+
)
21+
for data in test_data:
22+
with self.subTest(data=data):
23+
self.assertIs(fassert(data, data), True)
24+
25+
def test_subtest(self):
26+
test_data = (
27+
(["a", "b"], ["a"]),
28+
(("a", "b"), ("a",)),
29+
# TODO: ({"b", "a"}, {"a"})
30+
)
31+
for data, template in test_data:
32+
with self.subTest(data=data, template=template):
33+
self.assertIs(fassert(data, template), True)

tests/test_misc.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import re
2+
import unittest
3+
4+
5+
class ReadmeTest(unittest.TestCase):
6+
# Copy-paste the example code from README into this test
7+
def test_readme_main_example(self):
8+
from fassert import fassert
9+
10+
11+
# Usage: fassert(<data>, <template>)
12+
13+
fassert("This is expected", "This is expected")
14+
15+
fassert("This matches as well", re.compile(r"[thismacewl ]+", re.I))
16+
17+
fassert(
18+
{"key": "value", "key2": "value2"},
19+
{"key": "value"}
20+
) # key2: value2 is ignored as it's not defined in the template
21+
22+
fassert(
23+
{"key": "value"},
24+
# You can nest and combine the fuzzy matching types in containers
25+
{re.compile(r"[a-z]{3}"): "value"}
26+
)
27+
28+
# Template can contain callables as well
29+
fassert("value", lambda x: x == "value")
30+
31+
try:
32+
fassert({"a": "b"}, {"c": "d"}) # This will fail, {"c":"d"} is not in the target data
33+
except AssertionError:
34+
pass # All the examples within the try block above will raise this exception

tests/test_primitives.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import re
2+
import unittest
3+
4+
from fassert import fassert
5+
6+
7+
class MatchingPrimitivesTest(unittest.TestCase):
8+
def test_matching_primitives(self):
9+
test_data = (
10+
("", ""),
11+
("test_string", "test_string"),
12+
(b"test_bytes", b"test_bytes"),
13+
("", re.compile(r"^$")),
14+
("test", re.compile("[a-z]{4}")),
15+
(42, 42),
16+
(0, 0),
17+
(42.0, 42.0),
18+
(0.0, 0.0),
19+
(3.14, 3.14),
20+
(None, None),
21+
((), ()),
22+
([], []),
23+
(set(), set()),
24+
(dict(), dict()),
25+
)
26+
27+
for data, template in test_data:
28+
with self.subTest(data=data, template=template):
29+
assert fassert(data, template)
30+
31+
def test_same_type_different_value_not_matching(self):
32+
test_data = (
33+
("", "value"),
34+
("value", ""),
35+
(b"", b"value"),
36+
(0, 42),
37+
(42, 0),
38+
(1, 3),
39+
(42.0, 0.0),
40+
(0.0, 42.0),
41+
)
42+
43+
for data, template in test_data:
44+
with self.subTest(data=data, template=template):
45+
self.assertRaises(AssertionError, fassert, data, template)
46+
47+
48+
def test_different_types_not_matching(self):
49+
test_data = (
50+
("", ()),
51+
("", []),
52+
("", set()),
53+
(b"value", ""),
54+
)
55+
56+
for data, template in test_data:
57+
with self.subTest(data=data, template=template):
58+
self.assertRaises(AssertionError, fassert, data, template)
59+
60+
with self.subTest(data=template, template=data):
61+
self.assertRaises(AssertionError, fassert, template, data)

0 commit comments

Comments
 (0)