Finishing natural language plugin.

This commit is contained in:
Ad5001 2024-10-17 02:08:24 +02:00
parent 8fab9d8e52
commit ef465b34e7
Signed by: Ad5001
GPG key ID: EF45F9C6AFE20160
10 changed files with 751 additions and 320 deletions

View file

@ -18,4 +18,5 @@
from .spy import Spy from .spy import Spy
from .that import that from .that import that
from .interfaces.base import Assertion

View file

@ -17,14 +17,23 @@
""" """
class Assertion(Exception): class Assertion(Exception):
def __init__(self, assertion: bool, message: str): def __init__(self, assertion: bool, message: str, invert: bool):
self.assertion = assertion self.assertion = assertion
self.message = message self.message = message
self.invert = invert
def _invert_message(self):
for verb in ('is', 'was', 'has', 'have'):
for negative in ("n't", ' not', ' never', ' no'):
self.message = self.message.replace(f"{verb}{negative}", verb.upper())
def __str__(self): def __str__(self):
return self.message return self.message
def __bool__(self): def __bool__(self):
if not self.assertion: if not self.invert and not self.assertion:
raise self raise self
return self.assertion if self.invert and self.assertion:
self._invert_message()
raise self
return True # Raises otherwise.

View file

@ -15,10 +15,10 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
from typing import Self from typing import Self, Callable, Any
from tests.plugins.natural.interfaces.assertion import Assertion from .assertion import Assertion
from tests.plugins.natural.interfaces.utils import repr_ from .utils import repr_
class AssertionInterface: class AssertionInterface:
@ -26,15 +26,91 @@ class AssertionInterface:
Most basic assertion interface. Most basic assertion interface.
You probably want to use BaseAssertionInterface You probably want to use BaseAssertionInterface
""" """
def __init__(self, value):
def __init__(self, value, parent: Self = None):
self._value = value self._value = value
self._parent = parent
if parent is None:
self.__not = False
@property @property
def was(self) -> Self: def _not(self) -> bool:
"""
Internal state of whether the expression was negated.
Use "not_" to set it.
:return:
"""
return self.__not if self._parent is None else self._parent._not
@_not.setter
def _not(self, value: bool):
if self._not is True:
raise RuntimeError("Cannot call is_not or was_not twice in the same statement.")
if self._parent is None:
self.__not = True
else:
self._parent._not = True
def instance_of(self, type_: type) -> Assertion:
"""
Checks if the current value is equal to the provided value
"""
value_type_name = type(self._value).__name__
if not isinstance(type_, type):
raise RuntimeError("Provided 'type' provided is not a class.")
return Assertion(
isinstance(self._value, type_),
f"The value ({value_type_name} {repr_(self._value)}) is not a {type_.__name__}.",
self._not
)
def __call__(self, condition: Callable[[Any], bool]) -> Assertion:
"""
Apply condition to value that returns whether or not the value is valid.
"""
return Assertion(
condition(self._value),
f"The value ({repr_(self._value)}) did not match given conditions.",
self._not
)
"""
NOT Properties.
"""
@property
def NOT(self) -> Self:
self._not = True
return self return self
@property @property
def be(self) -> Self: def not_(self) -> Self:
self._not = True
return self
@property
def never(self) -> Self:
self._not = True
return self
"""
Chain self properties to sound natural
"""
@property
def that(self) -> Self:
return self
@property
def is_(self) -> Self:
return self
@property
def does(self) -> Self:
return self
@property
def was(self) -> Self:
return self return self
@property @property
@ -57,31 +133,25 @@ class AssertionInterface:
def an(self) -> Self: def an(self) -> Self:
return self return self
def is_a(self, type_) -> Assertion:
"""
Checks if the current value is equal to the provided value
"""
value_type_name = type(self._value).__name__
return Assertion(isinstance(type_, type_), f"The value ({value_type_name} {repr_(self._value)}) is not a {type_.__name__}.")
class EqualAssertionInterface(AssertionInterface): class EqualAssertionInterface(AssertionInterface):
""" """
Interface created for when its value should be checked for equality Interface created for when its value should be checked for equality
""" """
def __init__(self, value):
super().__init__(value) def __init__(self, value, parent: AssertionInterface = None):
super().__init__(value, parent)
def __call__(self, value) -> Assertion: def __call__(self, value) -> Assertion:
return Assertion(value == self._value, f"The value ({repr_(self._value)}) is not equal to {repr(value)}.") return Assertion(
value == self._value,
def to(self, value) -> Self: f"The value {repr_(self._value)} is different from {repr(value)}.",
return self(value) self._not
)
def of(self, value) -> Self:
return self(value)
@property
def to(self) -> Self:
return self
class BaseAssertionInterface(AssertionInterface): class BaseAssertionInterface(AssertionInterface):
@ -91,10 +161,10 @@ class BaseAssertionInterface(AssertionInterface):
""" """
Checks if the current value is equal to the provided value Checks if the current value is equal to the provided value
""" """
return EqualAssertionInterface(self._value) return EqualAssertionInterface(self._value, self)
@property @property
def is_equal(self) -> EqualAssertionInterface: def equal(self) -> EqualAssertionInterface:
""" """
Checks if the current value is equal to the provided value Checks if the current value is equal to the provided value
""" """

View file

@ -15,47 +15,69 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
from tests.plugins.natural.interfaces.assertion import Assertion from .assertion import Assertion
from tests.plugins.natural.interfaces.base import BaseAssertionInterface from .base import BaseAssertionInterface
from tests.plugins.natural.interfaces.int import IntInterface from .int import NumberInterface
from tests.plugins.natural.interfaces.utils import repr_ from .utils import repr_
class FixedIteratorInterface(BaseAssertionInterface): class FixedIteratorInterface(BaseAssertionInterface):
@property @property
def length(self) -> IntInterface: def length(self) -> NumberInterface:
return IntInterface(len(self._value)) return NumberInterface(len(self._value), self)
def elements(self, *elements) -> Assertion: def elements(self, *elements) -> Assertion:
tests = [elem for elem in elements if elem in self._value] tests = [repr_(elem) for elem in elements if elem not in self._value]
return Assertion( return Assertion(
len(tests) == 0, len(tests) == 0,
f"This value ({repr_(self._value)}) does not have elements ({repr_(tests)})" f"This value ({repr_(self._value)}) does not have elements {', '.join(tests)}.",
self._not
) )
def element(self, element) -> Assertion: def element(self, element) -> Assertion:
return Assertion( return Assertion(
element in self._value, element in self._value,
f"This value ({repr_(self._value)}) does not have element ({repr_(element)})" f"This value ({repr_(self._value)}) does not have element {repr_(element)}.",
self._not
) )
def contains(self, *elements) -> Assertion: def contains(self, *elements) -> Assertion:
return self.elements(*elements) """
Check if the element(s) are contained in the iterator.
"""
if len(elements) == 1:
return self.element(elements[0])
else:
return self.elements(*elements)
def contain(self, *elements):
"""
Check if the element(s) are contained in the iterator.
"""
return self.contains(*elements)
class BoolInterface(BaseAssertionInterface): class BoolInterface(BaseAssertionInterface):
@property @property
def is_true(self): def true(self):
return Assertion( return Assertion(
self._value == True, self._value == True,
f"The value ({repr_(self._value)}) is not True." f"The value ({repr_(self._value)}) is not True.",
self._not
) )
@property @property
def is_false(self): def false(self):
return Assertion( return Assertion(
self._value == False, self._value == False,
f"The value ({repr_(self._value)}) is not False." f"The value ({repr_(self._value)}) is not False.",
self._not
) )
class StringInterface(LengthInterface):
class StringInterface(FixedIteratorInterface):
pass
class ListInterface(FixedIteratorInterface):
pass pass

View file

@ -15,17 +15,47 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
from idlelib.configdialog import is_int
from math import log10, floor, ceil
from typing import Self from typing import Self
from tests.plugins.natural.interfaces.assertion import Assertion from .assertion import Assertion
from tests.plugins.natural.interfaces.base import BaseAssertionInterface, EqualAssertionInterface, AssertionInterface from .base import AssertionInterface
from tests.plugins.natural.interfaces.utils import repr_ from .utils import repr_
class IntComparisonAssertionInterface(AssertionInterface): class NumberComparisonAssertionInterface(AssertionInterface):
def __init__(self, value): def __init__(self, value, parent: AssertionInterface = None):
super().__init__(value) super().__init__(value, parent)
self._compare_to = None self._compare_stack = []
def _generate_compare_to(self) -> int:
"""
The number generated by the comparison stack.
E.g. can parse one.hundred.million.and.thirty.three.thousand.and.twelve.hundred.and.seven
as ['one', 'hundred', 'million', 'thirty', 'three', 'thousand', 'twelve', 'hundred', 'seven']
which results 100,034,207
"""
minus = len(self._compare_stack) > 0 and self._compare_stack[0] == -1
if len(self._compare_stack) < (2 if minus else 1):
raise RuntimeError("No number to compare the value to provided.")
if minus:
self._compare_stack.pop(0)
# Compute the number
add_stack = [self._compare_stack.pop(0)]
for element in self._compare_stack:
last_power = floor(log10(abs(add_stack[-1])))
current_power = floor(log10(abs(element)))
if last_power < current_power: # E.g. one hundred
add_stack[-1] *= element
elif last_power == 1 and current_power == 0: # E.g thirty four
add_stack[-1] += element
elif last_power > current_power: # E.g a hundred and five
add_stack.append(element)
else:
raise RuntimeError(f"Cannot chain two numbers with the same power ({add_stack[-1]} => {element}.")
total = sum(add_stack)
return -total if minus else total
def _compare(self) -> Assertion: def _compare(self) -> Assertion:
raise RuntimeError(f"No comparison method defined in {type(self).__name__}.") raise RuntimeError(f"No comparison method defined in {type(self).__name__}.")
@ -34,23 +64,26 @@ class IntComparisonAssertionInterface(AssertionInterface):
return bool(self._compare()) return bool(self._compare())
def __call__(self, compare_to: int) -> Self: def __call__(self, compare_to: int) -> Self:
if self._compare_to is None: if type(compare_to) not in (float, int):
self._compare_to = int(compare_to) raise RuntimeError(f"Cannot compare number ({self._value}) to non number ({repr_(compare_to)}).")
else: self._compare_stack.append(compare_to)
self._compare_to *= int(compare_to) return self
"""
Chain self properties
"""
@property
def and_(self) -> Self:
return self return self
@property @property
def time(self) -> Self: def AND(self) -> Self:
return self return self
@property """
def times(self) -> Self: Number shorthands
return self """
@property
def never(self) -> Self:
return self(0)
@property @property
def once(self) -> Self: def once(self) -> Self:
@ -64,6 +97,10 @@ class IntComparisonAssertionInterface(AssertionInterface):
def thrice(self) -> Self: def thrice(self) -> Self:
return self(3) return self(3)
@property
def minus(self) -> Self:
return self(-1)
@property @property
def zero(self) -> Self: def zero(self) -> Self:
return self(0) return self(0)
@ -108,6 +145,42 @@ class IntComparisonAssertionInterface(AssertionInterface):
def ten(self) -> Self: def ten(self) -> Self:
return self(10) return self(10)
@property
def eleven(self) -> Self:
return self(11)
@property
def twelve(self) -> Self:
return self(12)
@property
def thirteen(self) -> Self:
return self(13)
@property
def fourteen(self) -> Self:
return self(14)
@property
def fifteen(self) -> Self:
return self(15)
@property
def sixteen(self) -> Self:
return self(16)
@property
def seventeen(self) -> Self:
return self(17)
@property
def eighteen(self) -> Self:
return self(18)
@property
def nineteen(self) -> Self:
return self(19)
@property @property
def twenty(self) -> Self: def twenty(self) -> Self:
return self(20) return self(20)
@ -134,11 +207,11 @@ class IntComparisonAssertionInterface(AssertionInterface):
@property @property
def eighty(self) -> Self: def eighty(self) -> Self:
return self(70) return self(80)
@property @property
def ninety(self) -> Self: def ninety(self) -> Self:
return self(70) return self(90)
@property @property
def hundred(self) -> Self: def hundred(self) -> Self:
@ -157,73 +230,92 @@ class IntComparisonAssertionInterface(AssertionInterface):
return self(1_000_000_000) return self(1_000_000_000)
class LessThanComparisonInterface(IntComparisonAssertionInterface): class LessThanComparisonInterface(NumberComparisonAssertionInterface):
def _compare(self) -> Assertion: def _compare(self) -> Assertion:
compare = self._generate_compare_to()
return Assertion( return Assertion(
self._value < self._compare_to, self._value < compare,
f"The value ({repr_(self._value)}) is not less than to {repr_(self._compare_to)}." f"The value ({repr_(self._value)}) is not less than to {repr_(compare)}.",
self._not
) )
class MoreThanComparisonInterface(IntComparisonAssertionInterface):
class MoreThanComparisonInterface(NumberComparisonAssertionInterface):
def _compare(self) -> Assertion: def _compare(self) -> Assertion:
compare = self._generate_compare_to()
return Assertion( return Assertion(
self._value > self._compare_to, self._value > compare,
f"The value ({repr_(self._value)}) is not more than to {repr_(self._compare_to)}." f"The value ({repr_(self._value)}) is not more than to {repr_(compare)}.",
self._not
) )
class AtLeastComparisonInterface(IntComparisonAssertionInterface):
class AtLeastComparisonInterface(NumberComparisonAssertionInterface):
def _compare(self) -> Assertion: def _compare(self) -> Assertion:
compare = self._generate_compare_to()
return Assertion( return Assertion(
self._value >= self._compare_to, self._value >= compare,
f"The value ({repr_(self._value)}) is not at least to {repr_(self._compare_to)}." f"The value ({repr_(self._value)}) is not at least to {repr_(compare)}.",
self._not
) )
class AtMostComparisonInterface(IntComparisonAssertionInterface):
class AtMostComparisonInterface(NumberComparisonAssertionInterface):
def _compare(self) -> Assertion: def _compare(self) -> Assertion:
compare = self._generate_compare_to()
return Assertion( return Assertion(
self._value <= self._compare_to, self._value <= compare,
f"The value ({repr_(self._value)}) is not at least to {repr_(self._compare_to)}." f"The value ({repr_(self._value)}) is not at least to {repr_(compare)}.",
self._not
) )
class EqualComparisonInterface(IntComparisonAssertionInterface):
class EqualComparisonInterface(NumberComparisonAssertionInterface):
def _compare(self) -> Assertion: def _compare(self) -> Assertion:
compare = self._generate_compare_to()
return Assertion( return Assertion(
self._value == self._compare_to, self._value == compare,
f"The value ({repr_(self._value)}) is not equal to {repr_(self._compare_to)}." f"The value ({repr_(self._value)}) is not equal to {repr_(compare)}.",
self._not
) )
@property
def to(self) -> Self: def to(self) -> Self:
return self return self
def of(self) -> Self:
return self
class NumberInterface(AssertionInterface):
class IntInterface(AssertionInterface): def __call__(self, value):
def less_than(self) -> LessThanComparisonInterface: return EqualComparisonInterface(self._value, self)(value)
return LessThanComparisonInterface(self._value)
@property
def more_than(self) -> MoreThanComparisonInterface:
return MoreThanComparisonInterface(self._value)
@property
def at_least(self) -> AtLeastComparisonInterface:
return AtLeastComparisonInterface(self._value)
@property
def at_most(self) -> AtMostComparisonInterface:
return AtMostComparisonInterface(self._value)
@property @property
def equals(self) -> EqualComparisonInterface: def equals(self) -> EqualComparisonInterface:
return EqualComparisonInterface(self._value) return EqualComparisonInterface(self._value, self)
@property @property
def is_equal(self) -> EqualComparisonInterface: def equal(self) -> EqualComparisonInterface:
return EqualComparisonInterface(self._value) return EqualComparisonInterface(self._value, self)
@property @property
def exactly(self) -> EqualComparisonInterface: def exactly(self) -> EqualComparisonInterface:
return EqualComparisonInterface(self._value) return EqualComparisonInterface(self._value, self)
@property
def of(self) -> EqualComparisonInterface:
return EqualComparisonInterface(self._value, self)
@property
def less_than(self) -> LessThanComparisonInterface:
return LessThanComparisonInterface(self._value, self)
@property
def more_than(self) -> MoreThanComparisonInterface:
return MoreThanComparisonInterface(self._value, self)
@property
def at_least(self) -> AtLeastComparisonInterface:
return AtLeastComparisonInterface(self._value, self)
@property
def at_most(self) -> AtMostComparisonInterface:
return AtMostComparisonInterface(self._value, self)

View file

@ -0,0 +1,218 @@
"""
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
* Copyright (C) 2021-2024 Ad5001
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from typing import Callable, Self
from .base import Assertion, repr_, AssertionInterface
from .int import NumberComparisonAssertionInterface
PRINT_PREFIX = (" " * 24)
class SpyAssertion(Assertion):
def __init__(self, assertion: bool, message: str, calls: list, invert: bool):
super().__init__(assertion, message + "\n", invert)
if len(calls) > 0:
self.message += self.render_calls(calls)
else:
self.message += f"{PRINT_PREFIX}0 registered calls."
def render_calls(self, calls):
lines = [f"{PRINT_PREFIX}{len(calls)} registered call(s):"]
for call in calls:
repr_args = [repr_(arg) for arg in call[0]]
repr_kwargs = [f"{key}={repr_(arg)}" for key, arg in call[1].items()]
lines.append(f" - {', '.join([*repr_args, *repr_kwargs])}")
return ("\n" + PRINT_PREFIX).join(lines)
class Methods:
AT_LEAST_ONCE = "AT_LEAST_ONCE"
EXACTLY = "EXACTLY"
AT_LEAST = "AT_LEAST"
AT_MOST = "AT_MOST"
MORE_THAN = "MORE_THAN"
LESS_THAN = "LESS_THAN"
class CalledInterface(NumberComparisonAssertionInterface):
"""
Internal class generated by Spy.called.
"""
def __init__(self, calls: list[tuple[list, dict]], parent: AssertionInterface):
super().__init__(len(calls), parent)
self.__calls = calls
self.__method = Methods.AT_LEAST_ONCE
def __apply_method(self, calls):
required = None if self._compare_stack == [] else self._generate_compare_to()
calls_count = len(calls)
match self.__method:
case Methods.AT_LEAST_ONCE:
compare = len(calls) >= 1
error = f"Method was not called"
case Methods.EXACTLY:
compare = len(calls) == required
error = f"Method was not called {required} times ({required} != {calls_count})"
case Methods.AT_LEAST:
compare = len(calls) >= required
error = f"Method was not called at least {required} times ({required} >= {calls_count})"
case Methods.AT_MOST:
compare = len(calls) <= required
error = f"Method was not called at most {required} times ({required} <= {calls_count})"
case Methods.MORE_THAN:
compare = len(calls) > required
error = f"Method was not called more than {required} times ({required} > {calls_count})"
case Methods.LESS_THAN:
compare = len(calls) < required
error = f"Method was not called less than {required} times ({required} < {calls_count})"
case _:
raise RuntimeError(f"Unknown method {self.__method}.")
return compare, error
def __bool__(self) -> bool:
"""
Converts to boolean on assertion.
"""
compare, error = self.__apply_method(self.__calls)
return bool(SpyAssertion(compare, error + ".", self.__calls, self._not))
"""
Chaining methods
"""
def __call__(self, compare_to: int) -> Self:
super().__call__(compare_to)
if self.__method == Methods.AT_LEAST_ONCE:
self.__method = Methods.EXACTLY
return self
@property
def at_least(self) -> Self:
if self.__method == Methods.AT_LEAST_ONCE:
self.__method = Methods.AT_LEAST
else:
raise RuntimeError(f"Cannot redefine method from {self.__method} to {Methods.AT_MOST}")
return self
@property
def at_most(self) -> Self:
if self.__method == Methods.AT_LEAST_ONCE:
self.__method = Methods.AT_MOST
else:
raise RuntimeError(f"Cannot redefine method from {self.__method} to {Methods.AT_MOST}")
return self
@property
def more_than(self) -> Self:
if self.__method == Methods.AT_LEAST_ONCE:
self.__method = Methods.MORE_THAN
else:
raise RuntimeError(f"Cannot redefine method from {self.__method} to {Methods.MORE_THAN}")
return self
@property
def less_than(self) -> Self:
if self.__method == Methods.AT_LEAST_ONCE:
self.__method = Methods.LESS_THAN
else:
raise RuntimeError(f"Cannot redefine method from {self.__method} to {Methods.LESS_THAN}")
return self
@property
def time(self) -> Self:
return self
@property
def times(self) -> Self:
return self
"""
Class properties.
"""
def __match_calls_for_condition(self, condition: Callable[[list, dict], bool]) -> tuple[bool, str]:
calls = []
for call in self.__calls:
if condition(call[0], call[1]):
calls.append(call)
compare, error = self.__apply_method(calls)
return compare, error
def with_arguments(self, *args, **kwargs) -> SpyAssertion:
"""
Checks if the Spy has been called the given number of times
with at least the given arguments.
"""
def some_args_matched(a, kw):
args_matched = all((
arg in a
for arg in args
))
kwargs_matched = all((
key in kw and kw[key] == arg
for key, arg in kwargs.items()
))
return args_matched and kwargs_matched
compare, error = self.__match_calls_for_condition(some_args_matched)
repr_args = ', '.join([repr(arg) for arg in args])
repr_kwargs = ', '.join([f"{key}={repr(arg)}" for key, arg in kwargs.items()])
msg = f"{error} with arguments ({repr_args}) and keyword arguments ({repr_kwargs})."
return SpyAssertion(compare, msg, self.__calls, self._not)
def with_arguments_matching(self, test_condition: Callable[[list, dict], bool]) -> SpyAssertion:
"""
Checks if the Spy has been called the given number of times
with arguments matching the given conditions.
"""
compare, error = self.__match_calls_for_condition(test_condition)
msg = f"{error} with arguments matching given conditions."
return SpyAssertion(compare, msg, self.__calls, self._not)
def with_exact_arguments(self, *args, **kwargs) -> SpyAssertion:
"""
Checks if the Spy has been called the given number of times
with all the given arguments.
"""
compare, error = self.__match_calls_for_condition(lambda a, kw: a == args and kw == kwargs)
repr_args = ', '.join([repr(arg) for arg in args])
repr_kwargs = ', '.join([f"{key}={repr(arg)}" for key, arg in kwargs.items()])
msg = f"{error} with exact arguments ({repr_args}) and keyword arguments ({repr_kwargs})."
return SpyAssertion(compare, msg, self.__calls, self._not)
def with_no_argument(self) -> SpyAssertion:
"""
Checks if the Spy has been called the given number of times
with all the given arguments.
"""
compare, error = self.__match_calls_for_condition(lambda a, kw: len(a) == 0 and len(kw) == 0)
return SpyAssertion(compare, f"{error} with no arguments.", self.__calls, self._not)
class SpyAssertionInterface(AssertionInterface):
@property
def called(self) -> CalledInterface:
"""
Returns a boolean-able interface to check conditions for a given number of
time the spy was called.
"""
return CalledInterface(self._value.calls, self)

View file

@ -15,207 +15,15 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
from typing import Callable, Self from typing import Callable
from tests.plugins.natural.interfaces.base import Assertion, repr_, AssertionInterface
from tests.plugins.natural.interfaces.int import IntComparisonAssertionInterface
PRINT_PREFIX = (" " * 24)
class SpyAssertion(Assertion): class Spy:
def __init__(self, assertion: bool, message: str, calls: list):
super().__init__(assertion, message + "\n")
if len(calls) > 0:
self.message += self.render_calls(calls)
else:
self.message += f"{PRINT_PREFIX}0 registered calls."
def render_calls(self, calls):
lines = [f"{PRINT_PREFIX}{len(calls)} registered call(s):"]
for call in calls:
repr_args = [repr_(arg) for arg in call[0]]
repr_kwargs = [f"{key}={repr_(arg)}" for key, arg in call[1].items()]
lines.append(f" - {', '.join([*repr_args, *repr_kwargs])}")
return ("\n" + PRINT_PREFIX).join(lines)
class Methods:
AT_LEAST_ONCE = "AT_LEAST_ONCE"
EXACTLY = "EXACTLY"
AT_LEAST = "AT_LEAST"
AT_MOST = "AT_MOST"
MORE_THAN = "AT_LEAST"
LESS_THAN = "AT_MOST"
class CalledInterface(IntComparisonAssertionInterface):
"""
Internal class generated by Spy.was_called.
"""
def __init__(self, calls: list[tuple[list, dict]]):
super().__init__(len(calls))
self.__calls = calls
self.__method = Methods.AT_LEAST_ONCE
def __apply_method(self, calls):
required = self._compare_to
calls_count = len(calls)
match self.__method:
case Methods.AT_LEAST_ONCE:
compare = len(calls) >= 1
error = "Method was not called"
case Methods.EXACTLY:
compare = len(calls) == required
error = f"Method was not called {required} times ({required} != {calls_count})"
case Methods.AT_LEAST:
compare = len(calls) >= required
error = f"Method was not called at least {required} times ({required} > {calls_count})"
case Methods.AT_MOST:
compare = len(calls) <= required
error = f"Method was not called at most {required} times ({required} < {calls_count})"
case Methods.MORE_THAN:
compare = len(calls) > required
error = f"Method was not called more than {required} times ({required} >= {calls_count})"
case Methods.LESS_THAN:
compare = len(calls) < required
error = f"Method was not called less than {required} times ({required} <= {calls_count})"
case _:
raise RuntimeError(f"Unknown method {self.__method}.")
return compare, error
def __bool__(self) -> bool:
"""
Converts to boolean on assertion.
"""
compare, error = self.__apply_method(self.__calls)
return bool(SpyAssertion(compare, error + ".", self.__calls))
"""
Chaining methods
"""
def __call__(self, *args, **kwargs) -> Self:
if len(args) != 1:
raise RuntimeError("Cannot call called interface with more than one argument.")
self._compare_to = int(args[0])
if self.__method == Methods.AT_LEAST_ONCE:
self.__method = Methods.EXACTLY
return self
@property
def at_least(self) -> Self:
if self.__method == Methods.AT_LEAST_ONCE:
self.__method = Methods.AT_LEAST
else:
raise RuntimeError(f"Cannot redefine method from {self.__method} to {Methods.AT_MOST}")
return self
@property
def at_most(self) -> Self:
if self.__method == Methods.AT_LEAST_ONCE:
self.__method = Methods.AT_MOST
else:
raise RuntimeError(f"Cannot redefine method from {self.__method} to {Methods.AT_MOST}")
return self
@property
def more_than(self) -> Self:
if self.__method == Methods.AT_LEAST_ONCE:
self.__method = Methods.MORE_THAN
else:
raise RuntimeError(f"Cannot redefine method from {self.__method} to {Methods.MORE_THAN}")
return self
@property
def less_than(self) -> Self:
if self.__method == Methods.AT_LEAST_ONCE:
self.__method = Methods.LESS_THAN
else:
raise RuntimeError(f"Cannot redefine method from {self.__method} to {Methods.LESS_THAN}")
return self
@property
def time(self) -> Self:
return self
@property
def times(self) -> Self:
return self
"""
Class properties.
"""
def __match_calls_for_condition(self, condition: Callable[[list, dict], bool]) -> tuple[bool, str]:
calls = []
for call in self.__calls:
if condition(call[0], call[1]):
calls.append(call)
compare, error = self.__apply_method(calls)
return compare, error
def with_arguments(self, *args, **kwargs) -> SpyAssertion:
"""
Checks if the Spy has been called the given number of times
with at least the given arguments.
"""
def some_args_matched(a, kw):
args_matched = all((
arg in a
for arg in args
))
kwargs_matched = all((
key in kw and kw[key] == arg
for key, arg in kwargs.items()
))
return args_matched and kwargs_matched
compare, error = self.__match_calls_for_condition(some_args_matched)
repr_args = ', '.join([repr(arg) for arg in args])
repr_kwargs = ', '.join([f"{key}={repr(arg)}" for key, arg in kwargs.items()])
msg = f"{error} with arguments ({repr_args}) and keyword arguments ({repr_kwargs})."
return SpyAssertion(compare, msg, self.__calls)
def with_arguments_matching(self, test_condition: Callable[[list, dict], bool]) -> SpyAssertion:
"""
Checks if the Spy has been called the given number of times
with arguments matching the given conditions.
"""
compare, error = self.__match_calls_for_condition(test_condition)
msg = f"{error} with arguments matching given conditions."
return SpyAssertion(compare, msg, self.__calls)
def with_exact_arguments(self, *args, **kwargs) -> SpyAssertion:
"""
Checks if the Spy has been called the given number of times
with all the given arguments.
"""
compare, error = self.__match_calls_for_condition(lambda a, kw: a == args and kw == kwargs)
repr_args = ', '.join([repr(arg) for arg in args])
repr_kwargs = ', '.join([f"{key}={repr(arg)}" for key, arg in kwargs.items()])
msg = f"{error} with exact arguments ({repr_args}) and keyword arguments ({repr_kwargs})."
return SpyAssertion(compare, msg, self.__calls)
def with_no_argument(self) -> SpyAssertion:
"""
Checks if the Spy has been called the given number of times
with all the given arguments.
"""
compare, error = self.__match_calls_for_condition(lambda a, kw: len(a) == 0 and len(kw) == 0)
return SpyAssertion(compare, f"{error} with no arguments.", self.__calls)
class Spy(AssertionInterface):
""" """
Class to spy into method calls with natural language expressions. Class to spy into method calls with natural language expressions.
""" """
def __init__(self, function: Callable = None): def __init__(self, function: Callable = None):
super().__init__(function)
self.function = function self.function = function
self.calls = [] self.calls = []
@ -223,20 +31,3 @@ class Spy(AssertionInterface):
self.calls.append((args, kwargs)) self.calls.append((args, kwargs))
if self.function is not None: if self.function is not None:
self.function(*args, **kwargs) self.function(*args, **kwargs)
@property
def called(self) -> CalledInterface:
"""
Returns a boolean-able interface to check conditions for a given number of
time the spy was called.
"""
return CalledInterface(self.calls)
@property
def not_called(self) -> CalledInterface:
"""
Returns a boolean-able interface to check that conditions were never fulfilled
in the times the spy was called.
"""
ret = CalledInterface(self.calls)
return ret(0)

View file

@ -17,17 +17,21 @@
""" """
from typing import overload, Generic, TypeVar from typing import overload, Generic, TypeVar
from .spy import Spy from . import Spy
from .interfaces.base import AssertionInterface, BaseAssertionInterface from .interfaces.base import AssertionInterface, BaseAssertionInterface
from .interfaces.basic import StringInterface, BoolInterface from .interfaces.basic import StringInterface, BoolInterface, ListInterface
from .interfaces.int import IntInterface from .interfaces.int import NumberInterface
from .interfaces.spy import SpyAssertionInterface
Interface = TypeVar("Interface", bound=AssertionInterface) Interface = TypeVar("Interface", bound=AssertionInterface)
MATCHES = [ MATCHES = [
(str, StringInterface), (str, StringInterface),
(int, IntInterface),
(bool, BoolInterface), (bool, BoolInterface),
(int, NumberInterface),
(float, NumberInterface),
(list, ListInterface),
(Spy, SpyAssertionInterface)
] ]
@ -36,11 +40,18 @@ def that(value: str) -> StringInterface: ...
@overload @overload
def that(value: int) -> IntInterface: ... def that(value: bool) -> BoolInterface: ...
@overload @overload
def that(value: bool) -> BoolInterface: ... def that(value: int) -> NumberInterface: ...
@overload
def that(value: float) -> NumberInterface: ...
@overload
def that(value: Spy) -> SpyAssertionInterface: ...
@overload @overload

View file

@ -0,0 +1,217 @@
"""
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
* Copyright (C) 2021-2024 Ad5001
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import pytest
from ..natural import that, Assertion, Spy
def test_string():
assert that("QWERTY").is_.an.instance_of(str)
assert that("QWERTY").is_.not_.an.instance_of(int)
assert that("QWERTY").is_.equal.to("QWERTY")
assert that("QWERTY").is_.NOT.equal.to("QWERTYUIOP")
assert that("QWERTY").is_.NOT.equal.to(3)
assert that("QWERTY").has.a.length.of(6)
assert that("QWERTY").does.NOT.have.a.length.of(7)
assert that("QWERTY").has.a.length.that.is_.NOT(5)
assert that("QWERTY").contains("WER")
assert that("QWERTY").contains("WER", "TY")
assert that("QWERTY").does.not_.contain("AZERTY")
with pytest.raises(Assertion):
assert that("QWERTY").is_.an.instance_of(int)
with pytest.raises(Assertion):
assert that("QWERTY").is_.equal.to(False)
with pytest.raises(Assertion):
assert that("QWERTY").has.a.length.of(1)
with pytest.raises(Assertion):
assert that("QWERTY").contains("AZERTY")
with pytest.raises(Assertion):
assert that("QWERTY").does.NOT.contain("QWE")
def test_bool():
assert that(True).is_.an.instance_of(bool)
assert that(True).is_.an.instance_of(int)
assert that(True).is_.NOT.an.instance_of(str)
assert that(True).equals(True)
assert that(True).is_.true
assert that(True).is_.NOT.false
assert that(False).is_.equal.to(False)
assert that(False).is_.false
assert that(False).is_.NOT.true
with pytest.raises(Assertion):
assert that(True).is_.false
with pytest.raises(Assertion):
assert that(True).is_.NOT.true
def test_int():
assert that(2).is_.an.instance_of(int)
assert that(2).is_.NOT.an.instance_of(bool)
assert that(2).is_.NOT.an.instance_of(str)
assert that(2).is_.more_than(1)
assert that(2).is_.NOT.less_than(1)
assert that(2).is_.less_than(3)
assert that(2).is_.NOT.more_than(3)
assert that(2).is_.at_least(1)
assert that(2).is_.NOT.at_most(1)
assert that(2).is_.at_most(3)
assert that(2).is_.NOT.at_least(3)
assert that(2).is_.at_most(2)
assert that(2).is_.at_least(2)
# Equality
assert that(2).is_(2)
assert that(2).was(2)
assert that(2).is_.exactly(2)
assert that(2).is_.equal.to(2)
assert that(2).equals(2)
assert that(2).is_.NOT(3)
assert that(2).does.NOT.equal(3)
def test_int_shorthands():
# Direct numbers
assert that(0).equals.zero
assert that(1).equals.one
assert that(2).equals.two
assert that(3).equals.three
assert that(4).equals.four
assert that(5).equals.five
assert that(6).equals.six
assert that(7).equals.seven
assert that(8).equals.eight
assert that(9).equals.nine
assert that(10).equals.ten
assert that(11).equals.eleven
assert that(12).equals.twelve
assert that(13).equals.thirteen
assert that(14).equals.fourteen
assert that(15).equals.fifteen
assert that(16).equals.sixteen
assert that(17).equals.seventeen
assert that(18).equals.eighteen
assert that(19).equals.nineteen
assert that(20).equals.twenty
assert that(30).equals.thirty
assert that(40).equals.forty
assert that(50).equals.fifty
assert that(60).equals.sixty
assert that(70).equals.seventy
assert that(80).equals.eighty
assert that(90).equals.ninety
assert that(100).equals.a.hundred
assert that(1000).equals.a.thousand
assert that(1_000_000).equals.a.million
def test_add_natural_complex():
# Test composed
assert that(34).equals.thirty.four
assert that(-34).equals.minus.thirty.four
assert that(100_033_207).equals.one.hundred.million.AND.thirty.three.thousand.AND.two.hundred.AND.seven
assert that(-1_200_033_207).equals.minus.one.billion.AND.two.hundred.million.AND.thirty.three.thousand.AND.two.hundred.AND.seven
assert that(7890).equals.seven.thousand.eight.hundred.and_.ninety
assert that(7890).equals.seventy.eight.hundred.and_.ninety
assert that(7890).equals(78)(100)(90)
with pytest.raises(RuntimeError):
assert that(1_000_000).equals.a.thousand.thousand
with pytest.raises(RuntimeError):
assert that(600).equals.one.twenty.thirty
with pytest.raises(RuntimeError):
assert that(2).equals
with pytest.raises(RuntimeError):
assert that(2).equals.one.minus.two
def test_spy():
spy = Spy()
assert that(spy).is_.an.instance_of(Spy)
assert that(spy).is_(callable)
# Check calls
assert that(spy).was.never.called
assert that(spy).was.called.zero.times
spy(30, arg="string")
assert that(spy).was.called
assert that(spy).was.called.once
assert that(spy).was.called.one.time
assert that(spy).was.NOT.called.more_than.once
assert that(spy).was.called.with_arguments(30)
assert that(spy).was.called.with_arguments_matching(lambda args, kwargs: len(args) == 1 and len(kwargs) == 1)
assert that(spy).was.NOT.called.with_arguments(50)
assert that(spy).was.NOT.called.with_exact_arguments(30)
assert that(spy).was.NOT.called.with_no_argument()
assert that(spy).was.called.with_exact_arguments(30, arg="string")
with pytest.raises(Assertion):
assert that(spy).was.called.with_arguments(50)
with pytest.raises(Assertion):
assert that(spy).was.called.with_exact_arguments(30)
with pytest.raises(Assertion):
assert that(spy).was.called.with_no_argument()
def test_spy_seral_calls():
spy = Spy(lambda *args, **kw: None)
obj = object()
spy()
spy(30, arg="string")
spy(obj, 30, example=obj, none=None)
assert that(spy).was.called
assert that(spy).was.called.more_than.once
assert that(spy).was.called.more_than.twice
assert that(spy).was.NOT.called.more_than.thrice
assert that(spy).was.called.at_most.thrice
assert that(spy).was.called.at_least.thrice
assert that(spy).was.called.three.times
assert that(spy).was.called.less_than(4).times
# Check arguments
assert that(spy).was.called.once.with_no_argument()
assert that(spy).was.called.at_most.once.with_no_argument()
assert that(spy).was.called.twice.with_arguments(30)
assert that(spy).was.NOT.called.less_than.twice.with_arguments(30)
assert that(spy).was.called.once.with_arguments(obj)
assert that(spy).was.called.once.with_arguments(arg="string")
assert that(spy).was.called.once.with_arguments(30, obj)
assert that(spy).was.called.once.with_arguments(none=None)
assert that(spy).was.NOT.called.with_arguments(None)
assert that(spy).was.NOT.called.with_arguments(obj, 30, arg="string")
with pytest.raises(Assertion):
assert that(spy).was.called.with_arguments(obj, 30, arg="string")
# Checking with exact arguments
assert that(spy).was.called.once.with_exact_arguments(30, arg="string")
assert that(spy).was.called.once.with_exact_arguments(obj, 30, example=obj, none=None)
assert that(spy).was.NOT.called.with_exact_arguments(obj, 30, arg="string")
with pytest.raises(Assertion):
assert that(spy).was.called.with_exact_arguments(obj, 30, arg="string")
# Check arguments matching
assert that(spy).has.NOT.been.called.with_arguments_matching(lambda a, kw: len(a) + len(kw) == 3)
assert that(spy).was.called.once.with_arguments_matching(lambda a, kw: len(a) + len(kw) == 2)
assert that(spy).was.called.once.with_arguments_matching(lambda a, kw: len(a) + len(kw) == 4)
with pytest.raises(Assertion):
assert that(spy).was.called.with_arguments_matching(lambda a, kw: len(a) + len(kw) == 3)
def test_wrongful_expressions():
spy = Spy()
with pytest.raises(RuntimeError):
assert that(3).is_.less_than("str")
with pytest.raises(RuntimeError):
assert that(3).does.NOT.NOT.equal(3)
with pytest.raises(RuntimeError):
assert that(3).is_.an.instance_of("non type")
with pytest.raises(RuntimeError):
assert that(spy).was.called.more_than.at_least.once
with pytest.raises(RuntimeError):
assert that(spy).was.called.more_than.at_most.once
with pytest.raises(RuntimeError):
assert that(spy).was.called.more_than.less_than.once
with pytest.raises(RuntimeError):
assert that(spy).was.called.more_than.more_than.once