Starting natural language plugin.

This commit is contained in:
Ad5001 2024-10-16 22:18:53 +02:00
parent 34caf20593
commit 8fab9d8e52
Signed by: Ad5001
GPG key ID: EF45F9C6AFE20160
12 changed files with 593 additions and 81 deletions

View file

View file

@ -0,0 +1,21 @@
"""
* 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 .spy import Spy
from .that import that

View file

@ -0,0 +1,30 @@
"""
* 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/>.
"""
class Assertion(Exception):
def __init__(self, assertion: bool, message: str):
self.assertion = assertion
self.message = message
def __str__(self):
return self.message
def __bool__(self):
if not self.assertion:
raise self
return self.assertion

View file

@ -0,0 +1,101 @@
"""
* 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 Self
from tests.plugins.natural.interfaces.assertion import Assertion
from tests.plugins.natural.interfaces.utils import repr_
class AssertionInterface:
"""
Most basic assertion interface.
You probably want to use BaseAssertionInterface
"""
def __init__(self, value):
self._value = value
@property
def was(self) -> Self:
return self
@property
def be(self) -> Self:
return self
@property
def been(self) -> Self:
return self
@property
def have(self) -> Self:
return self
@property
def has(self) -> Self:
return self
@property
def a(self) -> Self:
return self
@property
def an(self) -> 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):
"""
Interface created for when its value should be checked for equality
"""
def __init__(self, value):
super().__init__(value)
def __call__(self, value) -> Assertion:
return Assertion(value == self._value, f"The value ({repr_(self._value)}) is not equal to {repr(value)}.")
def to(self, value) -> Self:
return self(value)
def of(self, value) -> Self:
return self(value)
class BaseAssertionInterface(AssertionInterface):
@property
def equals(self) -> EqualAssertionInterface:
"""
Checks if the current value is equal to the provided value
"""
return EqualAssertionInterface(self._value)
@property
def is_equal(self) -> EqualAssertionInterface:
"""
Checks if the current value is equal to the provided value
"""
return self.equals

View file

@ -0,0 +1,61 @@
"""
* 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 tests.plugins.natural.interfaces.assertion import Assertion
from tests.plugins.natural.interfaces.base import BaseAssertionInterface
from tests.plugins.natural.interfaces.int import IntInterface
from tests.plugins.natural.interfaces.utils import repr_
class FixedIteratorInterface(BaseAssertionInterface):
@property
def length(self) -> IntInterface:
return IntInterface(len(self._value))
def elements(self, *elements) -> Assertion:
tests = [elem for elem in elements if elem in self._value]
return Assertion(
len(tests) == 0,
f"This value ({repr_(self._value)}) does not have elements ({repr_(tests)})"
)
def element(self, element) -> Assertion:
return Assertion(
element in self._value,
f"This value ({repr_(self._value)}) does not have element ({repr_(element)})"
)
def contains(self, *elements) -> Assertion:
return self.elements(*elements)
class BoolInterface(BaseAssertionInterface):
@property
def is_true(self):
return Assertion(
self._value == True,
f"The value ({repr_(self._value)}) is not True."
)
@property
def is_false(self):
return Assertion(
self._value == False,
f"The value ({repr_(self._value)}) is not False."
)
class StringInterface(LengthInterface):
pass

View file

@ -0,0 +1,229 @@
"""
* 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 Self
from tests.plugins.natural.interfaces.assertion import Assertion
from tests.plugins.natural.interfaces.base import BaseAssertionInterface, EqualAssertionInterface, AssertionInterface
from tests.plugins.natural.interfaces.utils import repr_
class IntComparisonAssertionInterface(AssertionInterface):
def __init__(self, value):
super().__init__(value)
self._compare_to = None
def _compare(self) -> Assertion:
raise RuntimeError(f"No comparison method defined in {type(self).__name__}.")
def __bool__(self) -> bool:
return bool(self._compare())
def __call__(self, compare_to: int) -> Self:
if self._compare_to is None:
self._compare_to = int(compare_to)
else:
self._compare_to *= int(compare_to)
return self
@property
def time(self) -> Self:
return self
@property
def times(self) -> Self:
return self
@property
def never(self) -> Self:
return self(0)
@property
def once(self) -> Self:
return self(1)
@property
def twice(self) -> Self:
return self(2)
@property
def thrice(self) -> Self:
return self(3)
@property
def zero(self) -> Self:
return self(0)
@property
def one(self) -> Self:
return self(1)
@property
def two(self) -> Self:
return self(2)
@property
def three(self) -> Self:
return self(3)
@property
def four(self) -> Self:
return self(4)
@property
def five(self) -> Self:
return self(5)
@property
def six(self) -> Self:
return self(6)
@property
def seven(self) -> Self:
return self(7)
@property
def eight(self) -> Self:
return self(8)
@property
def nine(self) -> Self:
return self(9)
@property
def ten(self) -> Self:
return self(10)
@property
def twenty(self) -> Self:
return self(20)
@property
def thirty(self) -> Self:
return self(30)
@property
def forty(self) -> Self:
return self(40)
@property
def fifty(self) -> Self:
return self(50)
@property
def sixty(self) -> Self:
return self(60)
@property
def seventy(self) -> Self:
return self(70)
@property
def eighty(self) -> Self:
return self(70)
@property
def ninety(self) -> Self:
return self(70)
@property
def hundred(self) -> Self:
return self(100)
@property
def thousand(self) -> Self:
return self(1_000)
@property
def million(self) -> Self:
return self(1_000_000)
@property
def billion(self) -> Self:
return self(1_000_000_000)
class LessThanComparisonInterface(IntComparisonAssertionInterface):
def _compare(self) -> Assertion:
return Assertion(
self._value < self._compare_to,
f"The value ({repr_(self._value)}) is not less than to {repr_(self._compare_to)}."
)
class MoreThanComparisonInterface(IntComparisonAssertionInterface):
def _compare(self) -> Assertion:
return Assertion(
self._value > self._compare_to,
f"The value ({repr_(self._value)}) is not more than to {repr_(self._compare_to)}."
)
class AtLeastComparisonInterface(IntComparisonAssertionInterface):
def _compare(self) -> Assertion:
return Assertion(
self._value >= self._compare_to,
f"The value ({repr_(self._value)}) is not at least to {repr_(self._compare_to)}."
)
class AtMostComparisonInterface(IntComparisonAssertionInterface):
def _compare(self) -> Assertion:
return Assertion(
self._value <= self._compare_to,
f"The value ({repr_(self._value)}) is not at least to {repr_(self._compare_to)}."
)
class EqualComparisonInterface(IntComparisonAssertionInterface):
def _compare(self) -> Assertion:
return Assertion(
self._value == self._compare_to,
f"The value ({repr_(self._value)}) is not equal to {repr_(self._compare_to)}."
)
def to(self) -> Self:
return self
def of(self) -> Self:
return self
class IntInterface(AssertionInterface):
def less_than(self) -> LessThanComparisonInterface:
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
def equals(self) -> EqualComparisonInterface:
return EqualComparisonInterface(self._value)
@property
def is_equal(self) -> EqualComparisonInterface:
return EqualComparisonInterface(self._value)
@property
def exactly(self) -> EqualComparisonInterface:
return EqualComparisonInterface(self._value)

View file

@ -0,0 +1,27 @@
"""
* 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 PySide6.QtQml import QJSValue
def repr_(data):
if isinstance(data, QJSValue):
variant = data.toVariant()
return f"QJSValue<{type(variant).__name__}>({repr(variant)})"
else:
return repr(data)

View file

@ -17,35 +17,29 @@
"""
from typing import Callable, Self
from PySide6.QtQml import QJSValue
from tests.plugins.natural.interfaces.base import Assertion, repr_, AssertionInterface
from tests.plugins.natural.interfaces.int import IntComparisonAssertionInterface
PRINT_PREFIX = (" " * 24)
class SpyAssertionFailed(Exception):
def __init__(self, message, calls):
self.message = message + "\n"
class SpyAssertion(Assertion):
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 repr(self, data):
if isinstance(data, QJSValue):
variant = data.toVariant()
return f"QJSValue<{type(variant).__name__}>({repr(variant)})"
else:
return repr(data)
def render_calls(self, calls):
lines = [f"{PRINT_PREFIX}{len(calls)} registered call(s):"]
for call in calls:
repr_args = [self.repr(arg) for arg in call[0]]
repr_kwargs =[f"{key}={self.repr(arg)}" for key, arg in call[1].items()]
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)
def __str__(self):
return self.message
class Methods:
AT_LEAST_ONCE = "AT_LEAST_ONCE"
@ -55,18 +49,19 @@ class Methods:
MORE_THAN = "AT_LEAST"
LESS_THAN = "AT_MOST"
class CalledInterface:
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
self.__times = None
def __apply_method(self, calls):
required = self.__times
required = self._compare_to
calls_count = len(calls)
match self.__method:
case Methods.AT_LEAST_ONCE:
@ -91,61 +86,59 @@ class CalledInterface:
raise RuntimeError(f"Unknown method {self.__method}.")
return compare, error
def __bool__(self):
def __bool__(self) -> bool:
"""
Converts to boolean on assertion.
"""
compare, error = self.__apply_method(self.__calls)
if not compare:
raise SpyAssertionFailed(error+".")
return compare
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.__times = int(args[0])
self._compare_to = int(args[0])
if self.__method == Methods.AT_LEAST_ONCE:
self.__method = Methods.EXACTLY
return self
@property
def never(self) -> Self:
return self(0)
@property
def once(self) -> Self:
return self(1)
@property
def twice(self) -> Self:
return self(2)
@property
def thrice(self) -> Self:
return self(3)
@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
@ -155,6 +148,7 @@ class CalledInterface:
"""
Class properties.
"""
def __match_calls_for_condition(self, condition: Callable[[list, dict], bool]) -> tuple[bool, str]:
calls = []
for call in self.__calls:
@ -163,12 +157,12 @@ class CalledInterface:
compare, error = self.__apply_method(calls)
return compare, error
def with_arguments(self, *args, **kwargs):
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
@ -179,55 +173,49 @@ class CalledInterface:
for key, arg in kwargs.items()
))
return args_matched and kwargs_matched
compare, error = self.__match_calls_for_condition(some_args_matched)
if not compare:
repr_args = ', '.join([repr(arg) for arg in args])
repr_kwargs = ', '.join([f"{key}={repr(arg)}" for key, arg in kwargs.items()])
raise SpyAssertionFailed(f"{error} with arguments ({repr_args}) and keyword arguments ({repr_kwargs}).", self.__calls)
return compare
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]):
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)
if not compare:
raise SpyAssertionFailed(f"{error} with arguments matching given conditions.", self.__calls)
return compare
msg = f"{error} with arguments matching given conditions."
return SpyAssertion(compare, msg, self.__calls)
def with_exact_arguments(self, *args, **kwargs):
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)
if not compare:
repr_args = ', '.join([repr(arg) for arg in args])
repr_kwargs = ', '.join([f"{key}={repr(arg)}" for key, arg in kwargs.items()])
raise SpyAssertionFailed(f"{error} with exact arguments ({repr_args}) and keyword arguments ({repr_kwargs}).", self.__calls)
return compare
msg = f"{error} with exact arguments ({repr_args}) and keyword arguments ({repr_kwargs})."
return SpyAssertion(compare, msg, self.__calls)
def with_no_argument(self):
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)
if not compare:
raise SpyAssertionFailed(f"{error} with no arguments.", self.__calls)
return compare
return SpyAssertion(compare, f"{error} with no arguments.", self.__calls)
class Spy:
class Spy(AssertionInterface):
"""
Class to spy into method calls with natural language expressions.
"""
def __init__(self, function: Callable = None):
super().__init__(function)
self.function = function
self.calls = []
@ -237,7 +225,7 @@ class Spy:
self.function(*args, **kwargs)
@property
def was_called(self) -> CalledInterface:
def called(self) -> CalledInterface:
"""
Returns a boolean-able interface to check conditions for a given number of
time the spy was called.
@ -245,7 +233,7 @@ class Spy:
return CalledInterface(self.calls)
@property
def was_not_called(self) -> CalledInterface:
def not_called(self) -> CalledInterface:
"""
Returns a boolean-able interface to check that conditions were never fulfilled
in the times the spy was called.

View file

@ -0,0 +1,57 @@
"""
* 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 overload, Generic, TypeVar
from .spy import Spy
from .interfaces.base import AssertionInterface, BaseAssertionInterface
from .interfaces.basic import StringInterface, BoolInterface
from .interfaces.int import IntInterface
Interface = TypeVar("Interface", bound=AssertionInterface)
MATCHES = [
(str, StringInterface),
(int, IntInterface),
(bool, BoolInterface),
]
@overload
def that(value: str) -> StringInterface: ...
@overload
def that(value: int) -> IntInterface: ...
@overload
def that(value: bool) -> BoolInterface: ...
@overload
def that[Interface](value: Interface) -> Interface: ...
def that(value: any) -> AssertionInterface:
if not isinstance(value, AssertionInterface):
interface = next((i for t, i in MATCHES if isinstance(value, t)), None)
if interface is not None:
value = interface(value)
else:
value = BaseAssertionInterface(value)
return value

View file

@ -17,11 +17,9 @@
"""
from time import sleep
import pytest
from PySide6.QtCore import QObject
from PySide6.QtQml import QJSValue
from spy import Spy
from tests.plugins.natural import that, Spy
from LogarithmPlotter.util.js import PyJSValue
from LogarithmPlotter.util.promise import PyPromise
@ -95,10 +93,10 @@ class TestPyPromise:
# Check on our spy.
with qtbot.waitSignal(promise.fulfilled, timeout=10000):
pass
assert spy_fulfilled.was_called.once
assert spy_fulfilled.was_not_called.with_arguments(3)
assert spy_fulfilled.was_called.with_arguments_matching(check_promise_result(3))
assert spy_rejected.was_not_called
assert that(spy_fulfilled).was.called.once
assert that(spy_fulfilled).was.not_called.with_arguments(3)
assert that(spy_fulfilled).was.called.with_arguments_matching(check_promise_result(3))
assert spy_rejected.was.not_called
def test_rejected(self, qtbot):
spy_fulfilled = Spy()
@ -106,10 +104,10 @@ class TestPyPromise:
promise = PyPromise(async_throw)
then_res = promise.then(spy_fulfilled, spy_rejected)
# Check if the return value is the same promise (so we can chain then)
assert then_res == promise
assert that(then_res).is_equal.to(promise)
# Check on our spies.
with qtbot.waitSignal(promise.rejected, timeout=10000):
pass
assert spy_rejected.was_called.once
assert spy_rejected.was_called.with_arguments("Exception('aaaa')")
assert spy_fulfilled.was_not_called
assert that(spy_rejected).was.called.once
assert that(spy_rejected).was.called.with_arguments("Exception('aaaa')")
assert that(spy_fulfilled).was.not_called