Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[mypy]
exclude = venv/
150 changes: 150 additions & 0 deletions project/decorators.py
Original file line number Diff line number Diff line change
@@ -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
18 changes: 0 additions & 18 deletions tests/test_basic.py

This file was deleted.

126 changes: 126 additions & 0 deletions tests/test_decorators.py
Original file line number Diff line number Diff line change
@@ -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] # Исходный список не изменился