From ccbc3c94c924f6089ed594c83affc53b988f962b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=BE=D1=84=D0=B8=D1=8F=20=D0=A8=D0=B2=D0=BE=D1=80?= =?UTF-8?q?=D0=BE=D0=B1?= Date: Mon, 3 Feb 2025 14:19:52 +0300 Subject: [PATCH 1/7] feat(decorators):Implemented currying, caching, and smart argument handling decorators --- project/decorators.py | 91 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 project/decorators.py diff --git a/project/decorators.py b/project/decorators.py new file mode 100644 index 00000000..a0d9d35d --- /dev/null +++ b/project/decorators.py @@ -0,0 +1,91 @@ +import functools +import copy +from collections import deque +from typing import Callable, Any + + +def curry_explicit(function: Callable, arity: int) -> Callable: + 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: + 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): + def decorator(func: Callable): + cache = {} + order = deque() + + @functools.wraps(func) + def wrapper(*args, **kwargs): + key = (args, frozenset(kwargs.items())) + if key in cache: + return cache[key] + result = func(*args, **kwargs) + if size > 0: + if len(order) >= size: + oldest_key = order.popleft() + cache.pop(oldest_key, None) + cache[key] = result + 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: + pass + +def smart_args(func: Callable) -> Callable: + defaults = func.__defaults__ or () + default_dict = {} + + for i, param in enumerate(func.__code__.co_varnames[:func.__code__.co_argcount]): + if i < len(defaults): + default_dict[param] = defaults[i] + + @functools.wraps(func) + def wrapper(*args, **kwargs): + new_kwargs = {} + for key, value in kwargs.items(): + if isinstance(default_dict.get(key, None), Evaluated): + new_kwargs[key] = default_dict[key]() + elif isinstance(default_dict.get(key, None), Isolated): + new_kwargs[key] = copy.deepcopy(value) + else: + new_kwargs[key] = value + return func(*args, **new_kwargs) + + return wrapper From 6ef9d168abd04cf43bfb860a0ca31691910deefa Mon Sep 17 00:00:00 2001 From: sofiashvorob Date: Sat, 5 Apr 2025 19:56:09 +0300 Subject: [PATCH 2/7] feat(test) tests added and some decorators fixed --- project/decorators.py | 55 +++++++++++------ tests/test_basic.py | 137 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 164 insertions(+), 28 deletions(-) diff --git a/project/decorators.py b/project/decorators.py index a0d9d35d..5f21348d 100644 --- a/project/decorators.py +++ b/project/decorators.py @@ -44,19 +44,17 @@ def wrapper(*args, **kwargs): if key in cache: return cache[key] result = func(*args, **kwargs) - if size > 0: + cache[key] = result # Сохраняем результат всегда + if size > 0: # Ограничиваем размер кэша только если size > 0 if len(order) >= size: oldest_key = order.popleft() cache.pop(oldest_key, None) - cache[key] = result order.append(key) return result return wrapper - return decorator - class Evaluated: def __init__(self, func: Callable[[], Any]): self.func = func @@ -68,24 +66,43 @@ def __call__(self): class Isolated: pass + def smart_args(func: Callable) -> Callable: defaults = func.__defaults__ or () default_dict = {} - - for i, param in enumerate(func.__code__.co_varnames[:func.__code__.co_argcount]): - if i < len(defaults): - default_dict[param] = defaults[i] + param_names = func.__code__.co_varnames[:func.__code__.co_argcount] + for i, param in enumerate(param_names[-len(defaults):]): + default_dict[param] = defaults[i] @functools.wraps(func) def wrapper(*args, **kwargs): - new_kwargs = {} - for key, value in kwargs.items(): - if isinstance(default_dict.get(key, None), Evaluated): - new_kwargs[key] = default_dict[key]() - elif isinstance(default_dict.get(key, None), Isolated): - new_kwargs[key] = copy.deepcopy(value) - else: - new_kwargs[key] = value - return func(*args, **new_kwargs) - - return wrapper + 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 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 \ No newline at end of file diff --git a/tests/test_basic.py b/tests/test_basic.py index 4811167b..31dfe21a 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -1,18 +1,137 @@ import pytest -import project # on import will print something from __init__ file +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 -def setup_module(module): - print("basic setup module") +class Evaluated: + def __init__(self, func: Callable[[], Any]): + self.func = func -def teardown_module(module): - print("basic teardown module") + def __call__(self): + return self.func() -def test_1(): - assert 1 + 1 == 2 +class Isolated: + pass + + +# Тесты для curry_explicit +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) + + +# Тесты для uncurry_explicit +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) + + +# Тесты для cache_results--- +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 # Кэш переполнен, первый результат удален + + +# Тесты для smart_args +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] # Исходный список не изменился -def test_2(): - assert "1" + "1" == "11" From 4dea84e785f6c1523a8ba564d3a7b400a5025d71 Mon Sep 17 00:00:00 2001 From: Sem4kok Date: Sat, 5 Apr 2025 19:59:23 +0300 Subject: [PATCH 3/7] feat(ci): ci added --- .github/workflows/ci.yaml | 31 +++++++++++++++++++++++++++++++ mypy.ini | 2 ++ 2 files changed, 33 insertions(+) create mode 100644 .github/workflows/ci.yaml create mode 100644 mypy.ini 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/ From 48304c2044b86a192f6edc7f783cc01a63c4a5a2 Mon Sep 17 00:00:00 2001 From: Sem4kok Date: Sat, 5 Apr 2025 20:32:12 +0300 Subject: [PATCH 4/7] fix(decorators): tests fixed --- project/decorators.py | 30 ++++++----------- tests/{test_basic.py => test_decorators.py} | 37 ++++++++------------- 2 files changed, 24 insertions(+), 43 deletions(-) rename tests/{test_basic.py => test_decorators.py} (81%) diff --git a/project/decorators.py b/project/decorators.py index 5f21348d..45e73c9d 100644 --- a/project/decorators.py +++ b/project/decorators.py @@ -1,8 +1,7 @@ import functools import copy from collections import deque -from typing import Callable, Any - +from typing import Any, Callable, Deque, Dict def curry_explicit(function: Callable, arity: int) -> Callable: if arity < 0: @@ -17,7 +16,6 @@ def curried(*args): return curried - def uncurry_explicit(function: Callable, arity: int) -> Callable: if arity < 0: raise ValueError("Arity cannot be negative") @@ -32,11 +30,10 @@ def uncurried(*args): return uncurried - def cache_results(size: int = 0): def decorator(func: Callable): - cache = {} - order = deque() + cache: Dict[Any, Any] = {} + order: Deque[Any] = deque() @functools.wraps(func) def wrapper(*args, **kwargs): @@ -44,11 +41,11 @@ def wrapper(*args, **kwargs): if key in cache: return cache[key] result = func(*args, **kwargs) - cache[key] = result # Сохраняем результат всегда - if size > 0: # Ограничиваем размер кэша только если size > 0 + cache[key] = result + if size > 0: if len(order) >= size: oldest_key = order.popleft() - cache.pop(oldest_key, None) + del cache[oldest_key] order.append(key) return result @@ -62,17 +59,13 @@ def __init__(self, func: Callable[[], Any]): def __call__(self): return self.func() - class Isolated: pass - def smart_args(func: Callable) -> Callable: defaults = func.__defaults__ or () - default_dict = {} param_names = func.__code__.co_varnames[:func.__code__.co_argcount] - for i, param in enumerate(param_names[-len(defaults):]): - default_dict[param] = defaults[i] + default_dict = dict(zip(param_names[-len(defaults):], defaults)) @functools.wraps(func) def wrapper(*args, **kwargs): @@ -82,24 +75,23 @@ def wrapper(*args, **kwargs): 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() # Вызываем каждый раз + new_kwargs[param] = default_value() elif isinstance(default_value, Isolated): - new_kwargs[param] = [] # Новый пустой список + new_kwargs[param] = [] else: new_kwargs[param] = default_value diff --git a/tests/test_basic.py b/tests/test_decorators.py similarity index 81% rename from tests/test_basic.py rename to tests/test_decorators.py index 31dfe21a..03908414 100644 --- a/tests/test_basic.py +++ b/tests/test_decorators.py @@ -4,22 +4,16 @@ import copy from typing import Callable, Any -from project.decorators import curry_explicit, uncurry_explicit, cache_results, smart_args +from project.decorators import ( + curry_explicit, + uncurry_explicit, + cache_results, + smart_args, + Evaluated, + Isolated, +) -class Evaluated: - def __init__(self, func: Callable[[], Any]): - self.func = func - - def __call__(self): - return self.func() - - -class Isolated: - pass - - -# Тесты для curry_explicit def test_curry_explicit_basic(): def add(a, b, c): return a + b + c @@ -41,7 +35,6 @@ def test_curry_explicit_too_many_args(): curried(1, 2, 3) -# Тесты для uncurry_explicit def test_uncurry_explicit_basic(): def add(a): return lambda b: a + b @@ -61,7 +54,6 @@ def test_uncurry_explicit_negative_arity(): uncurry_explicit(lambda x: x, -1) -# Тесты для cache_results--- def test_cache_results_basic(): call_count = 0 @@ -73,7 +65,7 @@ def add(a, b): assert add(1, 2) == 3 assert add(1, 2) == 3 - assert call_count == 1 # Функция вызвана только раз + assert call_count == 1 def test_cache_results_with_size(): @@ -90,10 +82,9 @@ def add(a, b): assert call_count == 1 assert add(2, 3) == 5 assert add(1, 2) == 3 - assert call_count == 3 # Кэш переполнен, первый результат удален + assert call_count == 3 -# Тесты для smart_args def test_smart_args_basic(): def func(a, b=1): return a + b @@ -102,7 +93,7 @@ def func(a, b=1): assert smart(2) == 3 assert smart(2, b=2) == 4 -#--- + def test_smart_args_evaluated(): call_count = 0 @@ -120,7 +111,7 @@ def func(a, b=Evaluated(get_value)): assert smart(3) == 8 assert call_count == 2 # Evaluated вызывается каждый раз -#--- + def test_smart_args_isolated(): def func(a, b=Isolated()): b.append(1) @@ -132,6 +123,4 @@ def func(a, b=Isolated()): result2 = smart(5, lst) assert result1 == (5, [0, 1]) assert result2 == (5, [0, 1]) - assert lst == [0] # Исходный список не изменился - - + assert lst == [0] # Исходный список не изменился \ No newline at end of file From 6dd1a5d23e7f010e39458f0ee8a94f611ed902fc Mon Sep 17 00:00:00 2001 From: Sem4kok Date: Sat, 5 Apr 2025 20:34:44 +0300 Subject: [PATCH 5/7] chore(style): code beautify --- project/decorators.py | 15 +++++++++++---- tests/test_decorators.py | 2 +- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/project/decorators.py b/project/decorators.py index 45e73c9d..a0905e05 100644 --- a/project/decorators.py +++ b/project/decorators.py @@ -3,6 +3,7 @@ from collections import deque from typing import Any, Callable, Deque, Dict + def curry_explicit(function: Callable, arity: int) -> Callable: if arity < 0: raise ValueError("Arity cannot be negative") @@ -16,6 +17,7 @@ def curried(*args): return curried + def uncurry_explicit(function: Callable, arity: int) -> Callable: if arity < 0: raise ValueError("Arity cannot be negative") @@ -30,6 +32,7 @@ def uncurried(*args): return uncurried + def cache_results(size: int = 0): def decorator(func: Callable): cache: Dict[Any, Any] = {} @@ -50,8 +53,10 @@ def wrapper(*args, **kwargs): return result return wrapper + return decorator + class Evaluated: def __init__(self, func: Callable[[], Any]): self.func = func @@ -59,18 +64,20 @@ def __init__(self, func: Callable[[], Any]): def __call__(self): return self.func() + class Isolated: pass + def smart_args(func: Callable) -> Callable: defaults = func.__defaults__ or () - param_names = func.__code__.co_varnames[:func.__code__.co_argcount] - default_dict = dict(zip(param_names[-len(defaults):], defaults)) + 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] + sig = func.__code__.co_varnames[: func.__code__.co_argcount] args_dict = dict(zip(sig, args)) for param in sig: @@ -97,4 +104,4 @@ def wrapper(*args, **kwargs): return func(**new_kwargs) - return wrapper \ No newline at end of file + return wrapper diff --git a/tests/test_decorators.py b/tests/test_decorators.py index 03908414..b328d7b4 100644 --- a/tests/test_decorators.py +++ b/tests/test_decorators.py @@ -123,4 +123,4 @@ def func(a, b=Isolated()): result2 = smart(5, lst) assert result1 == (5, [0, 1]) assert result2 == (5, [0, 1]) - assert lst == [0] # Исходный список не изменился \ No newline at end of file + assert lst == [0] # Исходный список не изменился From 8901de11b9c66cd73757c8fa559efd1ff2c228b6 Mon Sep 17 00:00:00 2001 From: Sem4kok Date: Sat, 5 Apr 2025 20:41:51 +0300 Subject: [PATCH 6/7] chore(style): doc --- project/decorators.py | 45 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/project/decorators.py b/project/decorators.py index a0905e05..2220a5da 100644 --- a/project/decorators.py +++ b/project/decorators.py @@ -5,6 +5,16 @@ def curry_explicit(function: Callable, arity: int) -> Callable: + """ + Каррирует функцию с явно заданной арностью. + Параметры: + function: исходная функция для каррирования + arity: количество аргументов для полного применения + Возвращает: + Каррированную функцию + Исключения: + ValueError при отрицательной arity или избыточном количестве аргументов + """ if arity < 0: raise ValueError("Arity cannot be negative") @@ -19,6 +29,16 @@ def curried(*args): def uncurry_explicit(function: Callable, arity: int) -> Callable: + """ + Обратное преобразование каррированной функции. + Параметры: + function: каррированная функция + arity: количество аргументов для полного применения + Возвращает: + Обычную функцию с фиксированным количеством аргументов + Исключения: + ValueError при отрицательной arity или неверном количестве аргументов + """ if arity < 0: raise ValueError("Arity cannot be negative") @@ -34,6 +54,14 @@ def uncurried(*args): def cache_results(size: int = 0): + """ + Декоратор для кэширования результатов вызовов функции. + Параметры: + size: максимальный размер кэша (0 = без ограничений) + Особенности: + - Использует LRU стратегию при size > 0 + - Ключ формируется из позиционных и именованных аргументов + """ def decorator(func: Callable): cache: Dict[Any, Any] = {} order: Deque[Any] = deque() @@ -58,6 +86,10 @@ def wrapper(*args, **kwargs): class Evaluated: + """ + Класс для отложенного вычисления значений по умолчанию. + При вызове возвращает результат обернутой функции. + """ def __init__(self, func: Callable[[], Any]): self.func = func @@ -66,10 +98,19 @@ def __call__(self): class Isolated: - pass + """ + Маркер для изолированных значений по умолчанию. + Гарантирует глубокое копирование аргументов при каждом вызове. + """ 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)) @@ -104,4 +145,4 @@ def wrapper(*args, **kwargs): return func(**new_kwargs) - return wrapper + return wrapper \ No newline at end of file From 3045ada62d5b8d307d1d905c28190a902c7c6464 Mon Sep 17 00:00:00 2001 From: Sem4kok Date: Sat, 5 Apr 2025 20:43:11 +0300 Subject: [PATCH 7/7] chore(style): doc --- project/decorators.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/project/decorators.py b/project/decorators.py index 2220a5da..68d48417 100644 --- a/project/decorators.py +++ b/project/decorators.py @@ -62,6 +62,7 @@ def cache_results(size: int = 0): - Использует LRU стратегию при size > 0 - Ключ формируется из позиционных и именованных аргументов """ + def decorator(func: Callable): cache: Dict[Any, Any] = {} order: Deque[Any] = deque() @@ -90,6 +91,7 @@ class Evaluated: Класс для отложенного вычисления значений по умолчанию. При вызове возвращает результат обернутой функции. """ + def __init__(self, func: Callable[[], Any]): self.func = func @@ -145,4 +147,4 @@ def wrapper(*args, **kwargs): return func(**new_kwargs) - return wrapper \ No newline at end of file + return wrapper