diff --git a/.github/actions/test/action.yml b/.github/actions/test/action.yml index 73d6392..9f07876 100644 --- a/.github/actions/test/action.yml +++ b/.github/actions/test/action.yml @@ -10,17 +10,27 @@ runs: using: "composite" steps: - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ inputs.python-version }} - - name: Install dependencies + - name: Update pip shell: sh - run: | - python -m pip install --upgrade pip - if [[ ${{ inputs.django-version }} != 'main' ]]; then pip install --pre -q "Django>=${{ inputs.django-version }},<${{ inputs.django-version }}.99"; fi - if [[ ${{ inputs.django-version }} == 'main' ]]; then pip install https://github.com/django/django/archive/main.tar.gz; fi - pip install flake8 django-redis pymemcache + run: python -m pip install --upgrade pip + + - name: Install Django + shell: sh + run: python -m pip install "Django>=${{ inputs.django-version }},<${{ inputs.django-version }}.99" + if: ${{ inputs.django-version != 'main' }} + + - name: Install Django main + shell: sh + run: python -m pip install https://github.com/django/django/archive/main.tar.gz + if: ${{ inputs.django-version == 'main' }} + + - name: Install Django dependencies + shell: sh + run: pip install flake8 django-redis pymemcache - name: Test shell: sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bdd68fb..7b7011d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,8 +15,19 @@ jobs: strategy: fail-fast: false matrix: - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] - django: ['3.2', '4.0', '4.1', '4.2', 'main'] + python-version: + - '3.7' + - '3.8' + - '3.9' + - '3.10' + - '3.11' + django: + - '3.2' + - '4.0' + - '4.1' + - '4.2' + - '5.0' + - 'main' exclude: - python-version: '3.7' django: '4.0' @@ -24,15 +35,29 @@ jobs: django: '4.1' - python-version: '3.7' django: '4.2' + - python-version: '3.7' + django: '5.0' - python-version: '3.7' django: 'main' + - python-version: '3.8' + django: '5.0' + - python-version: '3.9' + django: '5.0' - python-version: '3.11' django: '3.2' - python-version: '3.11' django: '4.0' + - python-version: '3.12' + django: '3.2' + - python-version: '3.12' + django: '4.0' + - python-version: '3.12' + django: '4.1' + - python-version: '3.12' + django: '4.2' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: ./.github/actions/test with: @@ -43,9 +68,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - - uses: actions/setup-python@v2 + - uses: actions/setup-python@v4 with: python-version: '3.11' diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 1bfcf91..ee34d24 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -38,7 +38,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d07b84e..8ffdfdb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,16 +11,49 @@ jobs: strategy: fail-fast: true matrix: - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] - django: ['3.2', '4.0', '4.1'] + python-version: + - '3.7' + - '3.8' + - '3.9' + - '3.10' + - '3.11' + django: + - '3.2' + - '4.0' + - '4.1' + - '4.2' + - '5.0' + - 'main' exclude: - python-version: '3.7' django: '4.0' - python-version: '3.7' django: '4.1' + - python-version: '3.7' + django: '4.2' + - python-version: '3.7' + django: '5.0' + - python-version: '3.7' + django: 'main' + - python-version: '3.8' + django: '5.0' + - python-version: '3.9' + django: '5.0' + - python-version: '3.11' + django: '3.2' + - python-version: '3.11' + django: '4.0' + - python-version: '3.12' + django: '3.2' + - python-version: '3.12' + django: '4.0' + - python-version: '3.12' + django: '4.1' + - python-version: '3.12' + django: '4.2' steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: ./.github/actions/test with: @@ -32,10 +65,10 @@ jobs: needs: [test] steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.11 diff --git a/django_ratelimit/decorators.py b/django_ratelimit/decorators.py index 40c9541..0d50cea 100644 --- a/django_ratelimit/decorators.py +++ b/django_ratelimit/decorators.py @@ -1,4 +1,10 @@ from functools import wraps +import django +if django.VERSION >= (4, 1): + from asgiref.sync import iscoroutinefunction +else: + def iscoroutinefunction(func): + return False from django.conf import settings from django.utils.module_loading import import_string @@ -13,6 +19,23 @@ def ratelimit(group=None, key=None, rate=None, method=ALL, block=True): def decorator(fn): + # if iscoroutinefunction(fn): + # @wraps(fn) + # async def _async_wrapped(request, *args, **kw): + # old_limited = getattr(request, 'limited', False) + # ratelimited = is_ratelimited( + # request=request, group=group, fn=fn, key=key, rate=rate, + # method=method, increment=True) + # request.limited = ratelimited or old_limited + # if ratelimited and block: + # cls = getattr( + # settings, 'RATELIMIT_EXCEPTION_CLASS', Ratelimited) + # if isinstance(cls, str): + # cls = import_string(cls) + # raise cls() + # return await fn(request, *args, **kw) + # return _async_wrapped + @wraps(fn) def _wrapped(request, *args, **kw): old_limited = getattr(request, 'limited', False) @@ -23,7 +46,9 @@ def _wrapped(request, *args, **kw): if ratelimited and block: cls = getattr( settings, 'RATELIMIT_EXCEPTION_CLASS', Ratelimited) - raise (import_string(cls) if isinstance(cls, str) else cls)() + if isinstance(cls, str): + cls = import_string(cls) + raise cls() return fn(request, *args, **kw) return _wrapped return decorator diff --git a/django_ratelimit/tests.py b/django_ratelimit/tests.py index a58c89e..4561a1e 100644 --- a/django_ratelimit/tests.py +++ b/django_ratelimit/tests.py @@ -1,4 +1,8 @@ +import asyncio + +import django from functools import partial +from unittest import skipIf from django.core.cache import cache, InvalidCacheBackendError from django.core.exceptions import ImproperlyConfigured @@ -12,7 +16,10 @@ from django_ratelimit.core import (get_usage, is_ratelimited, _split_rate, _get_ip) - +if django.VERSION >= (4, 1): + from asgiref.sync import iscoroutinefunction + from django.test import AsyncRequestFactory + arf = AsyncRequestFactory() rf = RequestFactory() @@ -411,6 +418,26 @@ def view(request): req.META['REMOTE_ADDR'] = '2001:db9::1000' assert not view(req) + @skipIf( + django.VERSION < (4, 1), + reason="Async view support requires Django 4.1 or higher", + ) + async def test_decorate_async_function(self): + @ratelimit(key='ip', rate='1/m', block=False) + async def view(request): + await asyncio.sleep(0) + return request.limited + + req1 = arf.get('/') + req1.META['REMOTE_ADDR'] = '1.2.3.4' + + req2 = arf.get('/') + req2.META['REMOTE_ADDR'] = '1.2.3.4' + + assert iscoroutinefunction(view) + assert await view(req1) is False + assert await view(req2) is True + class FunctionsTests(TestCase): def setUp(self): diff --git a/docs/cookbook/429.rst b/docs/cookbook/429.rst index e79cb9a..19dcf3d 100644 --- a/docs/cookbook/429.rst +++ b/docs/cookbook/429.rst @@ -38,7 +38,7 @@ must define ``RATELIMIT_VIEW`` as a dotted-path to your error view: MIDDLEWARE = ( # ... toward the bottom ... - 'ratelimit.middleware.RatelimitMiddleware', + 'django_ratelimit.middleware.RatelimitMiddleware', # ... ) diff --git a/tox.ini b/tox.ini index 352f489..636583e 100644 --- a/tox.ini +++ b/tox.ini @@ -3,8 +3,9 @@ envlist = py37-django32, py38-django{32,40,41,42,main}, py39-django{32,40,41,42,main}, - py310-django{32,40,41,42,main}, - py311-django{41,42,main}, + py310-django{32,40,41,42,50,main}, + py311-django{41,42,50,main}, + py312-django{50,main}, pypy39-django{32,40,41,main}, [testenv] @@ -14,6 +15,7 @@ deps = django40: Django>=4.0,<4.1 django41: Django>=4.1,<4.2 django42: Django>=4.2,<4.3 + django50: Django>=5.0a1,<5.1 djangomain: https://github.com/django/django/archive/main.tar.gz pymemcache>=4.0,<5.0 django-redis>=5.2,<6.0