From 60b349b022793412e7f5cb76f51438b44b35255b Mon Sep 17 00:00:00 2001 From: Artem Malyshev Date: Sat, 2 Jan 2021 06:01:47 +0300 Subject: [PATCH] feat: implement callable positional argument #4 --- src/_primitives/argument.py | 15 ++++++++ src/_primitives/callable.py | 65 +++++++++++++++++++++++++++++++--- src/primitives/__init__.py | 4 +-- tests/test_callable.py | 69 +++++++++++++++++++++++++++++++++++-- 4 files changed, 143 insertions(+), 10 deletions(-) create mode 100644 src/_primitives/argument.py diff --git a/src/_primitives/argument.py b/src/_primitives/argument.py new file mode 100644 index 0000000..e4dc98e --- /dev/null +++ b/src/_primitives/argument.py @@ -0,0 +1,15 @@ +from _primitives.exceptions import PrimitiveError + + +class Argument: + """An object appropriate to fake function arguments.""" + + def __init__(self, value): + self.value = value + + def check(self, arg): + """Check argument match.""" + if arg != self.value: + raise PrimitiveError( + f"Called with argument {arg!r} while expected {self.value!r}" + ) diff --git a/src/_primitives/callable.py b/src/_primitives/callable.py index 1404510..d4bfe00 100644 --- a/src/_primitives/callable.py +++ b/src/_primitives/callable.py @@ -1,12 +1,67 @@ +from _primitives.argument import Argument +from _primitives.exceptions import PrimitiveError + + class Callable: """An object appropriate to fake functions and methods.""" def __init__(self, *args): - if args: - self.return_value = args[0] - else: - self.return_value = None + arguments, values = _arguments(args) + self.check = _Check(arguments) + self.return_value = _return_value(values) - def __call__(self): + def __call__(self, *args, **kwargs): """Return predefined value.""" + self.check(args, kwargs) return self.return_value + + +def _arguments(args): + arguments = [] + values = [] + for arg in args: + if isinstance(arg, Argument): + arguments.append(arg) + else: + values.append(arg) + return arguments, values + + +def _return_value(args): + if len(args) > 1: + raise PrimitiveError("'Callable' object should have only one return value") + elif args: + return args[0] + + +class _Check: + def __init__(self, arguments): + self.arguments = arguments + + def __call__(self, args, kwargs): + if kwargs: + raise PrimitiveError("Positional arguments can not be called as keyword") + iterator = _Iterator(args) + for argument in self.arguments: + value = iterator.get() + argument.check(value) + iterator.last() + + +class _Iterator: + def __init__(self, args): + self.state = iter(args) + + def get(self): + try: + return next(self.state) + except StopIteration: + raise PrimitiveError("Called with less arguments than expected") + + def last(self): + try: + next(self.state) + except StopIteration: + pass + else: + raise PrimitiveError("Called with more arguments than expected") diff --git a/src/primitives/__init__.py b/src/primitives/__init__.py index bd0f7a2..4d47a07 100644 --- a/src/primitives/__init__.py +++ b/src/primitives/__init__.py @@ -1,5 +1,5 @@ """Fake objects designed with OOP in mind.""" +from _primitives.argument import Argument from _primitives.callable import Callable - -__all__ = ["Callable"] +__all__ = ["Argument", "Callable"] diff --git a/tests/test_callable.py b/tests/test_callable.py index 2de2f57..be05257 100644 --- a/tests/test_callable.py +++ b/tests/test_callable.py @@ -1,12 +1,15 @@ """Tests related to `primitives.Callable` function.""" +import pytest + +from primitives import Argument from primitives import Callable -from primitives.exceptions import PrimitiveError # noqa: F401 +from primitives.exceptions import PrimitiveError -def test_empty_callable_object(): +def test_callable_object_return_null(): """Empty `Callable` object should return null. - An object is callable when return value was not specified. + A callable object is empty when return value was not specified. """ func = Callable() @@ -17,3 +20,63 @@ def test_callable_object_return_value(): """`Callable` object should return value passed to it constructor.""" func = Callable("Hello, John") assert func() == "Hello, John" + + +def test_callable_object_misconfigured(): + """`Callable` object should protect from multiple return values.""" + with pytest.raises(PrimitiveError) as exc_info: + Callable(1, 2) + + assert str(exc_info.value) == "'Callable' object should have only one return value" + + +def test_callable_object_null_argument(): + """`Callable` object should return null even if `Argument` was passed.""" + func = Callable(Argument("John")) + assert func("John") is None + + with pytest.raises(PrimitiveError) as exc_info: + func("Kate") + + assert str(exc_info.value) == "Called with argument 'Kate' while expected 'John'" + + +def test_callable_object_return_value_argument(): + """`Callable`object should return value even if `Argument` was passed.""" + func = Callable("Hello, John", Argument("John")) + assert func("John") == "Hello, John" + + with pytest.raises(PrimitiveError) as exc_info: + func("Kate") + + assert str(exc_info.value) == "Called with argument 'Kate' while expected 'John'" + + +def test_callable_object_positional_argument_prevent_keyword(): + """Positional `Argument` should provent usage of keyword argument.""" + func = Callable(Argument("John")) + + with pytest.raises(PrimitiveError) as exc_info: + func(a="John") + + assert str(exc_info.value) == "Positional arguments can not be called as keyword" + + +def test_callable_object_positional_argument_not_enough(): + """Raise error if mock specified more arguments than user passed.""" + func = Callable(Argument("John"), Argument("Kate")) + + with pytest.raises(PrimitiveError) as exc_info: + func("John") + + assert str(exc_info.value) == "Called with less arguments than expected" + + +def test_callable_object_positional_argument_overflow(): + """Raise error if user passed more arguments than mock specified.""" + func = Callable(Argument("John")) + + with pytest.raises(PrimitiveError) as exc_info: + func("John", 1) + + assert str(exc_info.value) == "Called with more arguments than expected"