Skip to content

Commit bf9a69c

Browse files
committed
feat: add conjectures lots of
1 parent 30b40f3 commit bf9a69c

File tree

10 files changed

+807
-69
lines changed

10 files changed

+807
-69
lines changed

poetry.lock

Lines changed: 337 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,25 @@ python = "^3.9"
99

1010
[tool.poetry.dev-dependencies]
1111
pytest = "^5.2"
12+
black = "^21.5b0"
13+
pylint = "^2.8.2"
14+
mypy = "^0.812"
15+
isort = "^5.8.0"
1216

1317
[build-system]
1418
requires = ["poetry-core>=1.0.0"]
1519
build-backend = "poetry.core.masonry.api"
20+
21+
[tool.isort]
22+
multi_line_output = 3
23+
include_trailing_comma = true
24+
force_grid_wrap = 0
25+
use_parentheses = true
26+
ensure_newline_before_comments = true
27+
line_length = 88
28+
29+
[tool.pylint.messages_control]
30+
disable = "C0330, C0326"
31+
32+
[tool.pylint.format]
33+
max-line-length = "88"

src/conjecture/__init__.py

Lines changed: 38 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,40 @@
1-
from __future__ import annotations
2-
3-
import typing
4-
5-
Predicate = typing.Callable[[object], bool]
6-
7-
8-
class Conjecture:
9-
def __init__(self, predicate: typing.Optional[Predicate] = None) -> None:
10-
self._predicate = predicate or self.predicate
11-
12-
def predicate(self, value: object) -> bool:
13-
raise NotImplementedError()
14-
15-
def __eq__(self, other: object) -> bool:
16-
return self._predicate(other)
17-
18-
def __ne__(self, other: object) -> bool:
19-
return not self._predicate(other)
20-
21-
def __or__(self, other) -> Conjecture:
22-
return any_of(self, other)
23-
24-
def __and__(self, other) -> Conjecture:
25-
return all_of(self, other)
26-
27-
def __invert__(self) -> Conjecture:
28-
return Conjecture(lambda value: not self._predicate(value))
29-
30-
31-
class AnyConjecture(Conjecture):
32-
def __init__(self, conjectures: typing.Iterable[Conjecture]) -> None:
33-
super().__init__()
34-
self.conjectures = conjectures
35-
36-
def predicate(self, value: object) -> None:
37-
for other in self.conjectures:
38-
if value == other:
39-
return True
40-
41-
return False
42-
43-
44-
class AllConjecture(Conjecture):
45-
def __init__(self, conjectures: typing.Iterable[Conjecture]) -> None:
46-
super().__init__()
47-
self.conjectures = conjectures
48-
49-
def predicate(self, value: object) -> None:
50-
for other in self.conjectures:
51-
if value != other:
52-
return False
53-
54-
return True
55-
56-
57-
def has(predicate: Predicate):
58-
return Conjecture(predicate)
59-
60-
61-
def any_of(*conjectures: Conjecture) -> Conjecture:
62-
return AnyConjecture(conjectures)
63-
64-
65-
def all_of(*conjectures: Conjecture) -> Conjecture:
66-
return AllConjecture(conjectures)
1+
"""
2+
conjecture
673
4+
a pythonic assertion framework
5+
"""
6+
from __future__ import annotations
687

69-
def is_between(minimum, maximum) -> Conjecture:
70-
return Conjecture(lambda value: minimum <= value <= maximum)
8+
from conjecture.base import AllOfConjecture, AnyOfConjecture, Conjecture
9+
from conjecture.general import all_of, any_of, anything, has, none
10+
from conjecture.object import instance_of
11+
from conjecture.rich import (
12+
equal_to,
13+
greater_than,
14+
greater_than_or_equal_to,
15+
less_than,
16+
less_than_or_equal_to,
17+
)
18+
from conjecture.sized import empty, length
19+
from conjecture.string import ends_with, starts_with
20+
21+
__all__ = (
22+
"Conjecture",
23+
"AnyOfConjecture",
24+
"AllOfConjecture",
25+
"all_of",
26+
"any_of",
27+
"anything",
28+
"empty",
29+
"ends_with",
30+
"equal_to",
31+
"greater_than_or_equal_to",
32+
"greater_than",
33+
"has",
34+
"instance_of",
35+
"length",
36+
"less_than_or_equal_to",
37+
"less_than",
38+
"none",
39+
"starts_with",
40+
)

src/conjecture/base.py

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
"""
2+
base conjecture classes
3+
"""
4+
from __future__ import annotations
5+
6+
import collections.abc
7+
import typing
8+
9+
Proof = typing.Callable[[object], bool]
10+
11+
12+
class Conjecture:
13+
"""
14+
A conjecture describing another object
15+
16+
Conjectures can be used to describe an object to which they are to be compared
17+
against. They assert equality when the proof function resolves truely.
18+
19+
>>> assert 5 == conjecture.greater_than(0) & conjecture.less_than(10)
20+
21+
There are many helpful conjecture factories with their proofs already defined.
22+
23+
:param proof: a callback that asserts some small fact of the passed object
24+
"""
25+
26+
def __init__(self, proof: typing.Optional[Proof] = None) -> None:
27+
self._proof = proof
28+
29+
def resolve(self, value: object) -> bool:
30+
"""
31+
Resolve conjecture
32+
33+
This is an abstract method can either be overwritten in a subclass or by
34+
providing a proof method to the constructor.
35+
36+
:param value: the value the conjecture is evaluated against
37+
38+
:return: whether the conjecture resolved truely
39+
"""
40+
41+
if not self._proof:
42+
raise NotImplementedError()
43+
44+
return self._proof(value)
45+
46+
def __eq__(self, other: object) -> bool:
47+
"""
48+
Resolve conjecture via equality
49+
50+
A conjecture can be resolved via `==` or `!=` comparison operators.
51+
52+
:param other: the value the conjecture is evaluated against
53+
54+
:return: whether the conjecture resolved truely
55+
"""
56+
57+
return self.resolve(other)
58+
59+
def __invert__(self) -> Conjecture:
60+
"""
61+
Invert conjecture
62+
63+
Invert the resolution of a conjecture
64+
65+
:return: the inverse conjecture
66+
"""
67+
68+
return Conjecture(lambda value: not self.resolve(value))
69+
70+
def __or__(self, other: Conjecture) -> Conjecture:
71+
"""
72+
Combine using any_of
73+
74+
:param other: another conjecture
75+
76+
:return: a conjecture that either of the combined conjectures will
77+
resolve truely
78+
"""
79+
80+
if not isinstance(other, Conjecture):
81+
raise ValueError(f"Conjecture cannot be combined with {other!r}")
82+
83+
return AnyOfConjecture((self, other))
84+
85+
def __and__(self, other: Conjecture) -> Conjecture:
86+
"""
87+
Combine using all_of
88+
89+
:param other: another conjecture
90+
91+
:return: a conjecture that both of the combined conjectures will resolve truely
92+
"""
93+
94+
if not isinstance(other, Conjecture):
95+
raise ValueError(f"Conjecture cannot be combined with {other!r}")
96+
97+
return AllOfConjecture((self, other))
98+
99+
100+
class AnyOfConjecture(Conjecture):
101+
"""
102+
Any of Conjecture
103+
104+
An any of conjecture will resolve truely if any of the passed conjectures
105+
resolve truely themselves.
106+
107+
:param conjectures: a tuple of conjectures
108+
"""
109+
110+
# pylint: disable=too-few-public-methods
111+
112+
def __init__(self, conjectures: collections.abc.Iterable[Conjecture]) -> None:
113+
super().__init__()
114+
self.conjectures = conjectures
115+
116+
def resolve(self, value: object) -> bool:
117+
for other in self.conjectures:
118+
if value == other:
119+
return True
120+
121+
return False
122+
123+
124+
class AllOfConjecture(Conjecture):
125+
"""
126+
All of Conjecture
127+
128+
An all of conjecture will resolve truely only when all of the passed conjectures
129+
resolve truely themselves.
130+
131+
:param conjectures: a tuple of conjectures
132+
"""
133+
134+
# pylint: disable=too-few-public-methods
135+
136+
def __init__(self, conjectures: collections.abc.Iterable[Conjecture]) -> None:
137+
super().__init__()
138+
self.conjectures = conjectures
139+
140+
def resolve(self, value: object) -> bool:
141+
for other in self.conjectures:
142+
if value != other:
143+
return False
144+
145+
return True

src/conjecture/general.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
"""
2+
general conjectures
3+
"""
4+
import conjecture.base
5+
6+
7+
def none() -> conjecture.base.Conjecture:
8+
"""
9+
None
10+
11+
Propose that the value is None
12+
13+
>>> assert value == conjecture.none()
14+
15+
:return: a conjecture object
16+
"""
17+
18+
return conjecture.base.Conjecture(lambda x: x is None)
19+
20+
21+
def anything() -> conjecture.base.Conjecture:
22+
"""
23+
Anything
24+
25+
Propose that the value meerly exists
26+
27+
>>> assert value == conjecture.anything()
28+
29+
:return: a conjecture object
30+
"""
31+
32+
return conjecture.base.Conjecture(lambda x: True)
33+
34+
35+
def any_of(*conjectures: conjecture.base.Conjecture) -> conjecture.base.Conjecture:
36+
"""
37+
Any of
38+
39+
Propose any of the conjectures resolve truely
40+
41+
>>> assert value == conjecture.any_of(conjecture1, conjecture2)
42+
43+
:return: a conjecture object
44+
"""
45+
return conjecture.base.AnyOfConjecture(conjectures)
46+
47+
48+
def all_of(*conjectures: conjecture.base.Conjecture) -> conjecture.base.Conjecture:
49+
"""
50+
All of
51+
52+
Propose all of the conjectures resolve truely
53+
54+
>>> assert value == conjecture.any_of(conjecture1, conjecture2)
55+
56+
:return: a conjecture object
57+
"""
58+
return conjecture.base.AllOfConjecture(conjectures)
59+
60+
61+
def has(proof: conjecture.base.Proof) -> conjecture.base.Conjecture:
62+
"""
63+
Has
64+
65+
Propose a custom proof function
66+
67+
>>> assert value == conjecture.has(lambda x: x > 5)
68+
69+
:return: a conjecture object
70+
"""
71+
return conjecture.base.Conjecture(proof)

src/conjecture/object.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
"""
2+
object conjectures
3+
"""
4+
import typing
5+
6+
import conjecture.base
7+
8+
9+
def instance_of(
10+
value: typing.Union[tuple[type, ...], type]
11+
) -> conjecture.base.Conjecture:
12+
"""
13+
Instance of
14+
15+
Propose that value is instance of the provided type(s)
16+
17+
>>> assert value == conjecture.instance_of((str, int))
18+
19+
:param value: a type or tuple of types to check
20+
21+
:return: a conjecture object
22+
"""
23+
24+
return conjecture.base.Conjecture(lambda x: isinstance(x, value))

src/conjecture/py.typed

Whitespace-only changes.

0 commit comments

Comments
 (0)