diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 00000000..75a2e367 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,31 @@ +name: Run Tests + +on: + [ push, pull_request ] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Check out code + uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.8' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + - name: Install mypy + run: | + python -m pip install mypy + - name: Run mypy + run: | + mypy . + - name: Run tests + run: | + python ./scripts/run_tests.py diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 00000000..624cd195 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,2 @@ +[mypy] +exclude = venv/ diff --git a/project/decorators.py b/project/decorators.py new file mode 100644 index 00000000..68d48417 --- /dev/null +++ b/project/decorators.py @@ -0,0 +1,150 @@ +import functools +import copy +from collections import deque +from typing import Any, Callable, Deque, Dict + + +def curry_explicit(function: Callable, arity: int) -> Callable: + """ + Каррирует функцию с явно заданной арностью. + Параметры: + function: исходная функция для каррирования + arity: количество аргументов для полного применения + Возвращает: + Каррированную функцию + Исключения: + ValueError при отрицательной arity или избыточном количестве аргументов + """ + if arity < 0: + raise ValueError("Arity cannot be negative") + + def curried(*args): + if len(args) > arity: + raise ValueError("Too many arguments provided") + if len(args) == arity: + return function(*args) + return lambda *next_args: curried(*(args + next_args)) + + return curried + + +def uncurry_explicit(function: Callable, arity: int) -> Callable: + """ + Обратное преобразование каррированной функции. + Параметры: + function: каррированная функция + arity: количество аргументов для полного применения + Возвращает: + Обычную функцию с фиксированным количеством аргументов + Исключения: + ValueError при отрицательной arity или неверном количестве аргументов + """ + if arity < 0: + raise ValueError("Arity cannot be negative") + + def uncurried(*args): + if len(args) != arity: + raise ValueError("Incorrect number of arguments provided") + result = function + for arg in args: + result = result(arg) + return result + + return uncurried + + +def cache_results(size: int = 0): + """ + Декоратор для кэширования результатов вызовов функции. + Параметры: + size: максимальный размер кэша (0 = без ограничений) + Особенности: + - Использует LRU стратегию при size > 0 + - Ключ формируется из позиционных и именованных аргументов + """ + + def decorator(func: Callable): + cache: Dict[Any, Any] = {} + order: Deque[Any] = deque() + + @functools.wraps(func) + def wrapper(*args, **kwargs): + key = (args, frozenset(kwargs.items())) + if key in cache: + return cache[key] + result = func(*args, **kwargs) + cache[key] = result + if size > 0: + if len(order) >= size: + oldest_key = order.popleft() + del cache[oldest_key] + order.append(key) + return result + + return wrapper + + return decorator + + +class Evaluated: + """ + Класс для отложенного вычисления значений по умолчанию. + При вызове возвращает результат обернутой функции. + """ + + def __init__(self, func: Callable[[], Any]): + self.func = func + + def __call__(self): + return self.func() + + +class Isolated: + """ + Маркер для изолированных значений по умолчанию. + Гарантирует глубокое копирование аргументов при каждом вызове. + """ + + +def smart_args(func: Callable) -> Callable: + """ + Умный обработчик аргументов функции с расширенными возможностями: + - Автоматическое копирование объектов, помеченных Isolated + - Ленивые вычисления значений, обернутых в Evaluated + - Сохранение сигнатуры исходной функции + """ + defaults = func.__defaults__ or () + param_names = func.__code__.co_varnames[: func.__code__.co_argcount] + default_dict = dict(zip(param_names[-len(defaults) :], defaults)) + + @functools.wraps(func) + def wrapper(*args, **kwargs): + new_kwargs = kwargs.copy() + sig = func.__code__.co_varnames[: func.__code__.co_argcount] + args_dict = dict(zip(sig, args)) + + for param in sig: + if param in args_dict: + default_value = default_dict.get(param) + if isinstance(default_value, Isolated): + new_kwargs[param] = copy.deepcopy(args_dict[param]) + else: + new_kwargs[param] = args_dict[param] + elif param in new_kwargs: + default_value = default_dict.get(param) + if isinstance(default_value, Isolated): + new_kwargs[param] = copy.deepcopy(new_kwargs[param]) + elif isinstance(new_kwargs[param], Evaluated): + new_kwargs[param] = new_kwargs[param]() + elif param in default_dict: + default_value = default_dict[param] + if isinstance(default_value, Evaluated): + new_kwargs[param] = default_value() + elif isinstance(default_value, Isolated): + new_kwargs[param] = [] + else: + new_kwargs[param] = default_value + + return func(**new_kwargs) + + return wrapper diff --git a/tests/test_basic.py b/tests/test_basic.py deleted file mode 100644 index 4811167b..00000000 --- a/tests/test_basic.py +++ /dev/null @@ -1,18 +0,0 @@ -import pytest -import project # on import will print something from __init__ file - - -def setup_module(module): - print("basic setup module") - - -def teardown_module(module): - print("basic teardown module") - - -def test_1(): - assert 1 + 1 == 2 - - -def test_2(): - assert "1" + "1" == "11" diff --git a/tests/test_decorators.py b/tests/test_decorators.py new file mode 100644 index 00000000..b328d7b4 --- /dev/null +++ b/tests/test_decorators.py @@ -0,0 +1,126 @@ +import pytest +from collections import deque +import functools +import copy +from typing import Callable, Any + +from project.decorators import ( + curry_explicit, + uncurry_explicit, + cache_results, + smart_args, + Evaluated, + Isolated, +) + + +def test_curry_explicit_basic(): + def add(a, b, c): + return a + b + c + + curried = curry_explicit(add, 3) + assert curried(1)(2)(3) == 6 + assert curried(1, 2)(3) == 6 + assert curried(1, 2, 3) == 6 + + +def test_curry_explicit_negative_arity(): + with pytest.raises(ValueError, match="Arity cannot be negative"): + curry_explicit(lambda x: x, -1) + + +def test_curry_explicit_too_many_args(): + curried = curry_explicit(lambda x, y: x + y, 2) + with pytest.raises(ValueError, match="Too many arguments provided"): + curried(1, 2, 3) + + +def test_uncurry_explicit_basic(): + def add(a): + return lambda b: a + b + + uncurried = uncurry_explicit(add, 2) + assert uncurried(2, 3) == 5 + + +def test_uncurry_explicit_wrong_args(): + uncurried = uncurry_explicit(lambda x: lambda y: x + y, 2) + with pytest.raises(ValueError, match="Incorrect number of arguments provided"): + uncurried(1) + + +def test_uncurry_explicit_negative_arity(): + with pytest.raises(ValueError, match="Arity cannot be negative"): + uncurry_explicit(lambda x: x, -1) + + +def test_cache_results_basic(): + call_count = 0 + + @cache_results() + def add(a, b): + nonlocal call_count + call_count += 1 + return a + b + + assert add(1, 2) == 3 + assert add(1, 2) == 3 + assert call_count == 1 + + +def test_cache_results_with_size(): + call_count = 0 + + @cache_results(size=1) + def add(a, b): + nonlocal call_count + call_count += 1 + return a + b + + assert add(1, 2) == 3 + assert add(1, 2) == 3 + assert call_count == 1 + assert add(2, 3) == 5 + assert add(1, 2) == 3 + assert call_count == 3 + + +def test_smart_args_basic(): + def func(a, b=1): + return a + b + + smart = smart_args(func) + assert smart(2) == 3 + assert smart(2, b=2) == 4 + + +def test_smart_args_evaluated(): + call_count = 0 + + def get_value(): + nonlocal call_count + call_count += 1 + return 5 + + def func(a, b=Evaluated(get_value)): + return a + b + + smart = smart_args(func) + assert smart(3) == 8 + assert call_count == 1 + assert smart(3) == 8 + assert call_count == 2 # Evaluated вызывается каждый раз + + +def test_smart_args_isolated(): + def func(a, b=Isolated()): + b.append(1) + return a, b + + smart = smart_args(func) + lst = [0] + result1 = smart(5, lst) + result2 = smart(5, lst) + assert result1 == (5, [0, 1]) + assert result2 == (5, [0, 1]) + assert lst == [0] # Исходный список не изменился