Skip to content

Commit

Permalink
feat(reattempt): add syn generator (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
sylvainmouquet authored Sep 27, 2024
1 parent ea04add commit 6c94ca9
Show file tree
Hide file tree
Showing 9 changed files with 474 additions and 140 deletions.
25 changes: 22 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
SHELL:=/bin/bash

SUPPORTED_COMMANDS := test
SUPPORTS_MAKE_ARGS := $(findstring $(firstword $(MAKECMDGOALS)), $(SUPPORTED_COMMANDS))
ifneq "$(SUPPORTS_MAKE_ARGS)" ""
COMMAND_ARGS := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS))
COMMAND_ARGS := $(subst :,\:,$(COMMAND_ARGS))
$(eval $(COMMAND_ARGS):;@:)
endif

# Git workflow commands
.PHONY: wip
wip:
Expand Down Expand Up @@ -42,12 +50,17 @@ install-local:
# Test command
.PHONY: test
test:
uv run pytest -v --log-cli-level=INFO
@echo "Modified arguments: $(new_args)"
@if [ -z "$(COMMAND_ARGS)" ]; then \
uv run pytest -v --log-cli-level=INFO; \
else \
uv run pytest -v --log-cli-level=INFO $(new_args); \
fi

# Lint command
.PHONY: lint
lint:
uv run ruff check
uv run ruff check --fix
uv run ruff format
uv run ruff format --check

Expand All @@ -59,7 +72,12 @@ update:
# Check for outdated dependencies
.PHONY: check-deps
check-deps:
uv pip list
.venv/bin/pip list --outdated

# Run type checking
.PHONY: type-check
type-check:
PYRIGHT_PYTHON_FORCE_VERSION=latest uv run pyright

# Display all available commands
.PHONY: help
Expand All @@ -74,4 +92,5 @@ help:
@echo " lint - Run linter"
@echo " update - Update dependencies"
@echo " check-deps - Check for outdated dependencies"
@echo " type-check - Run type checking"
@echo " help - Display this help message"
83 changes: 76 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ ReAttempt is a python decorator to retry a function when exceptions are raised.
from reattempt import reattempt

@reattempt(max_retries=5, min_time=0.1, max_time=2)
def wrong_function():
raise Exception("failure")
def simulate_network_failure():
raise Exception("Connection timeout")

if __name__ == "__main__":
wrong_function()
simulate_network_failure()

------------------------------------------------------- live log call -------------------------------------------------------
WARNING root:__init__.py:167 [RETRY] Attempt 1/5 failed, retrying in 0.17 seconds...
Expand Down Expand Up @@ -50,14 +50,83 @@ poetry add reattempt

```python
from reattempt import reattempt
import asyncio
import random

# List of flowers for our examples
flowers = ["Rose", "Tulip", "Sunflower", "Daisy", "Lily"]

# Synchronous function example
@reattempt
def plant_flower():
flower = random.choice(flowers)
print(f"Attempting to plant a {flower}")
if random.random() < 0.8: # 80% chance of failure
raise Exception(f"The {flower} didn't take root")
return f"{flower} planted successfully"

# Synchronous generator example
@reattempt
def grow_flowers():
for _ in range(3):
flower = random.choice(flowers)
print(f"Growing {flower}")
yield flower
if random.random() < 0.5: # 50% chance of failure at the end
raise Exception("The garden needs more fertilizer")

# Asynchronous function example
@reattempt
async def water_flower():
flower = random.choice(flowers)
print(f"Watering the {flower}")
await asyncio.sleep(0.1) # Simulating watering time
if random.random() < 0.6: # 60% chance of failure
raise Exception(f"The {flower} needs more water")
return f"{flower} is well-watered"

# Asynchronous generator function example
@reattempt
def hello_world():
print("Hello World")
raise Exception("failure")
async def harvest_flowers():
for _ in range(3):
flower = random.choice(flowers)
print(f"Harvesting {flower}")
yield flower
await asyncio.sleep(0.1) # Time between harvests
if random.random() < 0.4: # 40% chance of failure at the end
raise Exception("The garden needs more care")

async def tend_garden():
# Plant a flower (sync function)
try:
result = plant_flower()
print(result)
except Exception as e:
print(f"Planting error: {e}")

# Grow flowers (sync generator)
try:
for flower in grow_flowers():
print(f"Grown: {flower}")
except Exception as e:
print(f"Growing error: {e}")

# Water a flower (async function)
try:
result = await water_flower()
print(result)
except Exception as e:
print(f"Watering error: {e}")

# Harvest flowers (async generator function)
try:
async for flower in harvest_flowers():
print(f"Harvested: {flower}")
except Exception as e:
print(f"Harvesting error: {e}")

if __name__ == "__main__":
hello_world()
asyncio.run(tend_garden())
```


Expand Down
10 changes: 5 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "reattempt"
version = "1.0.7"
version = "1.0.10"
description = "Retrying a python function when exceptions are raised"
authors = [{name = "Sylvain Mouquet", email = "[email protected]"}]
readme = "README.md"
Expand Down Expand Up @@ -36,12 +36,12 @@ build-backend = "hatchling.build"

[tool.uv]
dev-dependencies = [
"aiohttp>=3.10.6",
"pip>=24.2",
"pytest>=8.3.3",
"pytest-asyncio>=0.24.0",
"pytest-mock>=3.14.0",
"pytest>=8.3.3",
"ruff>=0.6.7",
"reattempt>=0.0.1",
"aiohttp>=3.10.6",
"ruff>=0.6.7"
]

[tool.uv.sources]
Expand Down
49 changes: 48 additions & 1 deletion reattempt/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,10 +186,57 @@ async def retry_async_gen_func(*args, **kwargs):
if capture_exception and not should_retry:
raise capture_exception

@functools.wraps(func)
def retry_sync_gen_func(*args, **kwargs):
wait_time: float = min_time
attempt: int = 0
should_retry: bool = True
capture_exception: Exception | None = None

while should_retry and attempt < max_retries:
wait_time = random.uniform(wait_time, max_time)

try:
item = None
for item in func(*args, **kwargs):
yield item
break
except Exception as e:
capture_exception = e

if not item: # the instantiation has failed
logging.exception(e)

if attempt + 1 == max_retries:
logging.warning(
f"[RETRY] Attempt {attempt + 1}/{max_retries} failed, stopping"
)
else:
logging.warning(
f"[RETRY] Attempt {attempt + 1}/{max_retries} failed, retrying in {format(wait_time, '.2f')} seconds..."
)

attempt = attempt + 1
time.sleep(wait_time)
else:
should_retry = False

if attempt == max_retries:
logging.error("[RETRY] Max retries reached")

if capture_exception:
raise capture_exception

# for exceptions out of the scope of the gen
if capture_exception and not should_retry:
raise capture_exception

if inspect.iscoroutinefunction(func):
return retry_async_func
if inspect.isasyncgenfunction(func):
elif inspect.isasyncgenfunction(func):
return retry_async_gen_func
elif inspect.isgeneratorfunction(func):
return retry_sync_gen_func
return retry_sync_func

if func:
Expand Down
83 changes: 83 additions & 0 deletions test/test_flowers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import pytest
from reattempt import reattempt
import asyncio
import random
import logging

# List of flowers for our examples
flowers = ["Rose", "Tulip", "Sunflower", "Daisy", "Lily"]


# Synchronous function example
@reattempt
def plant_flower():
flower = random.choice(flowers)
logging.info(f"Attempting to plant a {flower}")
if random.random() < 0.8: # 80% chance of failure
raise Exception(f"The {flower} didn't take root")
return f"{flower} planted successfully"


# Synchronous generator example
@reattempt
def grow_flowers():
for _ in range(3):
flower = random.choice(flowers)
print(f"Growing {flower}")
yield flower
if random.random() < 0.5: # 50% chance of failure at the end
raise Exception("The garden needs more fertilizer")


# Asynchronous function example
@reattempt
async def water_flower():
flower = random.choice(flowers)
logging.info(f"Watering the {flower}")
await asyncio.sleep(0.1) # Simulating watering time
if random.random() < 0.6: # 60% chance of failure
raise Exception(f"The {flower} needs more water")
return f"{flower} is well-watered"


# Asynchronous generator function example
@reattempt
async def harvest_flowers():
for _ in range(3):
flower = random.choice(flowers)
logging.info(f"Harvesting {flower}")
yield flower
await asyncio.sleep(0.1) # Time between harvests
if random.random() < 0.4: # 40% chance of failure at the end
raise Exception("The garden needs more care")


@pytest.mark.asyncio
async def test_tend_garden(disable_logging_exception):
# Plant a flower (sync function)
try:
result = plant_flower()
logging.info(result)
except Exception as e:
logging.info(f"Planting error: {e}")

# Grow flowers (sync generator)
try:
for flower in grow_flowers():
print(f"Grown: {flower}")
except Exception as e:
print(f"Growing error: {e}")

# Water a flower (async function)
try:
result = await water_flower()
logging.info(result)
except Exception as e:
logging.info(f"Watering error: {e}")

# Harvest flowers (async generator function)
try:
async for flower in harvest_flowers():
logging.info(f"Harvested: {flower}")
except Exception as e:
logging.info(f"Harvesting error: {e}")
6 changes: 3 additions & 3 deletions test/test_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

@reattempt(max_retries=MAX_ATTEMPTS, min_time=MIN_TIME, max_time=MAX_TIME)
async def async_aiohttp_call(status: int):
async_aiohttp_call.counter += 1
async_aiohttp_call.counter += 1 # type: ignore

async with aiohttp.ClientSession() as session:
async with session.get(
Expand All @@ -17,15 +17,15 @@ async def async_aiohttp_call(status: int):


@pytest.mark.asyncio
async def test_retry_http_200():
async def test_retry_http_200(disable_logging_exception):
async_aiohttp_call.counter = 0 # type: ignore

await async_aiohttp_call(200)
assert async_aiohttp_call.counter == 1 # type: ignore


@pytest.mark.asyncio
async def test_retry_http_500():
async def test_retry_http_500(disable_logging_exception):
async_aiohttp_call.counter = 0 # type: ignore

try:
Expand Down
Loading

0 comments on commit 6c94ca9

Please sign in to comment.