Skip to content

Commit

Permalink
feat: add conjectures lots of
Browse files Browse the repository at this point in the history
  • Loading branch information
danielknell committed May 6, 2021
1 parent 30b40f3 commit bf9a69c
Show file tree
Hide file tree
Showing 10 changed files with 807 additions and 69 deletions.
338 changes: 337 additions & 1 deletion poetry.lock

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,25 @@ python = "^3.9"

[tool.poetry.dev-dependencies]
pytest = "^5.2"
black = "^21.5b0"
pylint = "^2.8.2"
mypy = "^0.812"
isort = "^5.8.0"

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

[tool.isort]
multi_line_output = 3
include_trailing_comma = true
force_grid_wrap = 0
use_parentheses = true
ensure_newline_before_comments = true
line_length = 88

[tool.pylint.messages_control]
disable = "C0330, C0326"

[tool.pylint.format]
max-line-length = "88"
106 changes: 38 additions & 68 deletions src/conjecture/__init__.py
Original file line number Diff line number Diff line change
@@ -1,70 +1,40 @@
from __future__ import annotations

import typing

Predicate = typing.Callable[[object], bool]


class Conjecture:
def __init__(self, predicate: typing.Optional[Predicate] = None) -> None:
self._predicate = predicate or self.predicate

def predicate(self, value: object) -> bool:
raise NotImplementedError()

def __eq__(self, other: object) -> bool:
return self._predicate(other)

def __ne__(self, other: object) -> bool:
return not self._predicate(other)

def __or__(self, other) -> Conjecture:
return any_of(self, other)

def __and__(self, other) -> Conjecture:
return all_of(self, other)

def __invert__(self) -> Conjecture:
return Conjecture(lambda value: not self._predicate(value))


class AnyConjecture(Conjecture):
def __init__(self, conjectures: typing.Iterable[Conjecture]) -> None:
super().__init__()
self.conjectures = conjectures

def predicate(self, value: object) -> None:
for other in self.conjectures:
if value == other:
return True

return False


class AllConjecture(Conjecture):
def __init__(self, conjectures: typing.Iterable[Conjecture]) -> None:
super().__init__()
self.conjectures = conjectures

def predicate(self, value: object) -> None:
for other in self.conjectures:
if value != other:
return False

return True


def has(predicate: Predicate):
return Conjecture(predicate)


def any_of(*conjectures: Conjecture) -> Conjecture:
return AnyConjecture(conjectures)


def all_of(*conjectures: Conjecture) -> Conjecture:
return AllConjecture(conjectures)
"""
conjecture
a pythonic assertion framework
"""
from __future__ import annotations

def is_between(minimum, maximum) -> Conjecture:
return Conjecture(lambda value: minimum <= value <= maximum)
from conjecture.base import AllOfConjecture, AnyOfConjecture, Conjecture
from conjecture.general import all_of, any_of, anything, has, none
from conjecture.object import instance_of
from conjecture.rich import (
equal_to,
greater_than,
greater_than_or_equal_to,
less_than,
less_than_or_equal_to,
)
from conjecture.sized import empty, length
from conjecture.string import ends_with, starts_with

__all__ = (
"Conjecture",
"AnyOfConjecture",
"AllOfConjecture",
"all_of",
"any_of",
"anything",
"empty",
"ends_with",
"equal_to",
"greater_than_or_equal_to",
"greater_than",
"has",
"instance_of",
"length",
"less_than_or_equal_to",
"less_than",
"none",
"starts_with",
)
145 changes: 145 additions & 0 deletions src/conjecture/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
"""
base conjecture classes
"""
from __future__ import annotations

import collections.abc
import typing

Proof = typing.Callable[[object], bool]


class Conjecture:
"""
A conjecture describing another object
Conjectures can be used to describe an object to which they are to be compared
against. They assert equality when the proof function resolves truely.
>>> assert 5 == conjecture.greater_than(0) & conjecture.less_than(10)
There are many helpful conjecture factories with their proofs already defined.
:param proof: a callback that asserts some small fact of the passed object
"""

def __init__(self, proof: typing.Optional[Proof] = None) -> None:
self._proof = proof

def resolve(self, value: object) -> bool:
"""
Resolve conjecture
This is an abstract method can either be overwritten in a subclass or by
providing a proof method to the constructor.
:param value: the value the conjecture is evaluated against
:return: whether the conjecture resolved truely
"""

if not self._proof:
raise NotImplementedError()

return self._proof(value)

def __eq__(self, other: object) -> bool:
"""
Resolve conjecture via equality
A conjecture can be resolved via `==` or `!=` comparison operators.
:param other: the value the conjecture is evaluated against
:return: whether the conjecture resolved truely
"""

return self.resolve(other)

def __invert__(self) -> Conjecture:
"""
Invert conjecture
Invert the resolution of a conjecture
:return: the inverse conjecture
"""

return Conjecture(lambda value: not self.resolve(value))

def __or__(self, other: Conjecture) -> Conjecture:
"""
Combine using any_of
:param other: another conjecture
:return: a conjecture that either of the combined conjectures will
resolve truely
"""

if not isinstance(other, Conjecture):
raise ValueError(f"Conjecture cannot be combined with {other!r}")

return AnyOfConjecture((self, other))

def __and__(self, other: Conjecture) -> Conjecture:
"""
Combine using all_of
:param other: another conjecture
:return: a conjecture that both of the combined conjectures will resolve truely
"""

if not isinstance(other, Conjecture):
raise ValueError(f"Conjecture cannot be combined with {other!r}")

return AllOfConjecture((self, other))


class AnyOfConjecture(Conjecture):
"""
Any of Conjecture
An any of conjecture will resolve truely if any of the passed conjectures
resolve truely themselves.
:param conjectures: a tuple of conjectures
"""

# pylint: disable=too-few-public-methods

def __init__(self, conjectures: collections.abc.Iterable[Conjecture]) -> None:
super().__init__()
self.conjectures = conjectures

def resolve(self, value: object) -> bool:
for other in self.conjectures:
if value == other:
return True

return False


class AllOfConjecture(Conjecture):
"""
All of Conjecture
An all of conjecture will resolve truely only when all of the passed conjectures
resolve truely themselves.
:param conjectures: a tuple of conjectures
"""

# pylint: disable=too-few-public-methods

def __init__(self, conjectures: collections.abc.Iterable[Conjecture]) -> None:
super().__init__()
self.conjectures = conjectures

def resolve(self, value: object) -> bool:
for other in self.conjectures:
if value != other:
return False

return True
71 changes: 71 additions & 0 deletions src/conjecture/general.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""
general conjectures
"""
import conjecture.base


def none() -> conjecture.base.Conjecture:
"""
None
Propose that the value is None
>>> assert value == conjecture.none()
:return: a conjecture object
"""

return conjecture.base.Conjecture(lambda x: x is None)


def anything() -> conjecture.base.Conjecture:
"""
Anything
Propose that the value meerly exists
>>> assert value == conjecture.anything()
:return: a conjecture object
"""

return conjecture.base.Conjecture(lambda x: True)


def any_of(*conjectures: conjecture.base.Conjecture) -> conjecture.base.Conjecture:
"""
Any of
Propose any of the conjectures resolve truely
>>> assert value == conjecture.any_of(conjecture1, conjecture2)
:return: a conjecture object
"""
return conjecture.base.AnyOfConjecture(conjectures)


def all_of(*conjectures: conjecture.base.Conjecture) -> conjecture.base.Conjecture:
"""
All of
Propose all of the conjectures resolve truely
>>> assert value == conjecture.any_of(conjecture1, conjecture2)
:return: a conjecture object
"""
return conjecture.base.AllOfConjecture(conjectures)


def has(proof: conjecture.base.Proof) -> conjecture.base.Conjecture:
"""
Has
Propose a custom proof function
>>> assert value == conjecture.has(lambda x: x > 5)
:return: a conjecture object
"""
return conjecture.base.Conjecture(proof)
24 changes: 24 additions & 0 deletions src/conjecture/object.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""
object conjectures
"""
import typing

import conjecture.base


def instance_of(
value: typing.Union[tuple[type, ...], type]
) -> conjecture.base.Conjecture:
"""
Instance of
Propose that value is instance of the provided type(s)
>>> assert value == conjecture.instance_of((str, int))
:param value: a type or tuple of types to check
:return: a conjecture object
"""

return conjecture.base.Conjecture(lambda x: isinstance(x, value))
Empty file added src/conjecture/py.typed
Empty file.
Loading

0 comments on commit bf9a69c

Please sign in to comment.