From 7ab4875c63d297f951ef287fb41954f0eef677c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B0=D1=80=D1=8C=D1=8F=20=D0=A2=D0=BE=D1=80=D0=B3?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0?= Date: Mon, 8 Dec 2025 21:27:24 +0300 Subject: [PATCH 1/3] Added functions Curry and Uncurry --- src/functions_curry_7/curry_uncurry.py | 85 ++++++++++++ tests/test_curry.py | 179 +++++++++++++++++++++++++ 2 files changed, 264 insertions(+) create mode 100644 src/functions_curry_7/curry_uncurry.py create mode 100644 tests/test_curry.py diff --git a/src/functions_curry_7/curry_uncurry.py b/src/functions_curry_7/curry_uncurry.py new file mode 100644 index 0000000..18f18b3 --- /dev/null +++ b/src/functions_curry_7/curry_uncurry.py @@ -0,0 +1,85 @@ +def curry(func, arity): + """ + Каррирование функции с заданной арностью. + + Args: func - исходная функция, + arity - арность функции (кол-во ожид. аргументов). + + return: рекурсивно с пустого кортежа собираем все аргументы + и вызываем для них исходную функцию. Её результат возвращаем. + + raises: ValueError - если арность отрицательная. + """ + + if arity < 0: + raise ValueError("Арность не должна быть <0") + + def curried(args_received): + """ + Рекурсивная функция, которая копит аргументы. + + args_received: type - tuple, уже полученные аргументы + + return: если все аргументы получены - результат данной функции, + иначе - новая функция, ждущая след. аргумент. + """ + if len(args_received) == arity: + return func(*args_received) + + def next_arg(arg): + """ + Функция, в которой мы получаем новый аргумент + и добавлем его к кортежу полученных аргументов. + + return: curried(аргументы + новый) и проверяем, + достаточно ли аргументов для подсчета результата. + """ + new_args = args_received + (arg,) + return curried(new_args) + + return next_arg + + if arity == 0: + return lambda: func() + + return curried(()) + + +def uncurry(curried_func, arity): + """ + Функция декаррирования каррированной функции с заданной арностью. + + Args: curried_func - каррированная функция, + arity - арность функции. + + return: внутренняя декаррирующая функция, принимающая все аргументы в виде кортежа. + + raise: ValueError - если передана отрицательная арность. + """ + if arity < 0: + raise ValueError("Арность не должна быть <0") + + def uncurried(*args): + """ + Функция, принимающая все аргументы сразу. + + *args - все аргументы функции. + + return: последовательно применяем все аргументы к каррированной + функции и возвращаем результат. + + raise: TypeError при неверном кол-ве аргументов. + """ + if len(args) != arity: + raise TypeError(f"Функция ожидает {arity} аргументов, получено {len(args)}") + + result = curried_func + for arg in args: + result = result(arg) + + if arity == 0: + return result() + + return result + + return uncurried diff --git a/tests/test_curry.py b/tests/test_curry.py new file mode 100644 index 0000000..59195f7 --- /dev/null +++ b/tests/test_curry.py @@ -0,0 +1,179 @@ +import pytest +from hypothesis import given +from hypothesis import strategies as st + +from functions_curry_7.curry_uncurry import curry, uncurry + +# unit тесты + + +def test_curry_basic(): + """Тест каррирования функции с 3 аргументами.""" + + def sum_3(a, b, c): + return a + b + c + + curried = curry(sum_3, 3) + result = curried(1)(2)(3) + assert result == 6, f"Ожидалось 6, получено {result}" + + +def test_curry_partial_application(): + """Тест частичного применения каррированной функции.""" + + def multiply(a, b): + return a * b + + curried = curry(multiply, 2) + double = curried(2) + + assert double(5) == 10 + assert double(6) == 12 + assert callable(double), "Частично примененная функция должна быть вызываемой" + + +def test_uncurry_basic(): + """Тест обратимости""" + + def add_4(a, b, c, d): + return a + b + c + d + + curried = curry(add_4, 4) + uncurried = uncurry(curried, 4) + re_curried = curry(uncurried, 4) + + assert curried(1)(2)(3)(4) == 10 + assert uncurried(1, 2, 3, 4) == 10 + assert re_curried(1)(2)(3)(4) == 10 + + +def test_zero_arity(): + """Тестирование функций с нулевой арностью""" + + def constant(): + return 42 + + curried_const = curry(constant, 0) + assert curried_const() == 42 + + uncurried_const = uncurry(curried_const, 0) + assert uncurried_const() == 42 + + +def test_single_argument(): + """Тестирование функций с одним аргументом""" + + def identity(x): + return x + + curried_id = curry(identity, 1) + assert curried_id(5) == 5 + + uncurried_id = uncurry(curried_id, 1) + assert uncurried_id(10) == 10 + + +# тесты ошибок + + +def test_curry_negative_arity(): + """Тест ошибки при отрицательной арности в curry.""" + + def func(a): + return a + + with pytest.raises(ValueError, match="Арность не должна быть <0"): + curry(func, -1) + + with pytest.raises(ValueError, match="Арность не должна быть <0"): + curry(func, -100) + + +def test_uncurry_negative_arity(): + """Тест ошибки при отрицательной арности в uncurry.""" + + def func(a): + return a + + curried = curry(func, 1) + + with pytest.raises(ValueError, match="Арность не должна быть <0"): + uncurry(curried, -1) + + +def test_uncurry_wrong_number_of_args(): + """Тест ошибки при передаче неправильного количества аргументов в uncurry.""" + + def sum_3(a, b, c): + return a + b + c + + curried = curry(sum_3, 3) + uncurried = uncurry(curried, 3) + + with pytest.raises(TypeError, match="Функция ожидает 3 аргументов"): + uncurried(1, 2) + + with pytest.raises(TypeError, match="Функция ожидает 3 аргументов"): + uncurried(1, 2, 3, 4) + + +# Property-based тесты с hypothesis + +integers = st.integers(min_value=-100, max_value=100) +small_lists = st.lists(integers, min_size=0, max_size=5) + + +@given(a=integers, b=integers, c=integers) +def test_curry_uncurry_inverse(a: int, b: int, c: int): + """ + Property-based тест: curry и uncurry обратны друг другу. + Для любых a, b, c результаты должны совпадать. + """ + + def sum_3(x, y, z): + return x + y + z + + curried = curry(sum_3, 3) + uncurried = uncurry(curried, 3) + + assert curried(a)(b)(c) == uncurried(a, b, c) + assert curried(a)(b)(c) == a + b + c + + +@given(args=st.tuples(integers, integers, integers)) +def test_curry_preserves_behavior(args): + """ + Property-based тест: каррированная функция ведет себя так же, + как исходная. + """ + + def sum_3(x, y, z): + return x + y + z + + curried = curry(sum_3, 3) + a, b, c = args + + direct_result = sum_3(a, b, c) + curried_result = curried(a)(b)(c) + + assert direct_result == curried_result + + +@given(x=integers, y=integers, z=integers, w=integers) +def test_partial_application_property(x, y, z, w): + """ + Property-based тест: частичное применение работает корректно. + """ + + def add4(a, b, c, d): + return a + b + c + d + + curried_add4 = curry(add4, 4) + partially_applied = curried_add4(x)(y) + + assert callable(partially_applied) + + result = partially_applied(z)(w) + + assert result == x + y + z + w + assert result == add4(x, y, z, w) From 55634bb836ea3a241182119df85b46db129e938f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B0=D1=80=D1=8C=D1=8F=20=D0=A2=D0=BE=D1=80=D0=B3?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0?= Date: Mon, 8 Dec 2025 22:05:51 +0300 Subject: [PATCH 2/3] Added installing hypothesis in workflows --- .github/workflows/tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 64c840a..f1536a3 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -26,6 +26,7 @@ jobs: run: | python -m pip install --upgrade pip pip install pytest + pip install pytest hypothesis - name: Run tests run: | From 2f008a56c8d35723289a79f63fff24ec8e0c083d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=B0=D1=80=D1=8C=D1=8F=20=D0=A2=D0=BE=D1=80=D0=B3?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0?= Date: Tue, 9 Dec 2025 01:24:18 +0300 Subject: [PATCH 3/3] Updated curry function --- pyproject.toml | 1 + src/functions_curry_7/curry_uncurry.py | 5 +++++ tests/test_curry.py | 2 -- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e88c832..d3cca8b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,7 @@ target-version = "py312" "RET", # Хорошие практики возврата "SIM", # Общие правила упрощения ] +extend-ignore = ["I001"] [tool.pytest.ini_options] testpaths = ["tests"] diff --git a/src/functions_curry_7/curry_uncurry.py b/src/functions_curry_7/curry_uncurry.py index 18f18b3..04e8c4a 100644 --- a/src/functions_curry_7/curry_uncurry.py +++ b/src/functions_curry_7/curry_uncurry.py @@ -11,6 +11,11 @@ def curry(func, arity): raises: ValueError - если арность отрицательная. """ + actual_argcount = func.__code__.co_argcount + + if arity > actual_argcount: + raise ValueError(f"Арность {arity} превышает количество параметров функции") + if arity < 0: raise ValueError("Арность не должна быть <0") diff --git a/tests/test_curry.py b/tests/test_curry.py index 59195f7..38898ea 100644 --- a/tests/test_curry.py +++ b/tests/test_curry.py @@ -40,11 +40,9 @@ def add_4(a, b, c, d): curried = curry(add_4, 4) uncurried = uncurry(curried, 4) - re_curried = curry(uncurried, 4) assert curried(1)(2)(3)(4) == 10 assert uncurried(1, 2, 3, 4) == 10 - assert re_curried(1)(2)(3)(4) == 10 def test_zero_arity():