diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 35b92f1..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,107 +0,0 @@ -version: 2.0 - -common: &common - working_directory: ~/repo - steps: - - checkout - - restore_cache: - keys: - - v2-deps-{{ .Environment.CIRCLE_JOB }}-{{ checksum "setup.py" }}-{{ checksum "tox.ini" }} - - v2-deps- - - run: - name: install dependencies - command: pip install --user tox - - run: - name: run tox - command: ~/.local/bin/tox - - run: - name: upload coverage report - command: | - if [[ "$UPLOAD_COVERAGE" != 0 ]]; then - bash <(curl -s https://codecov.io/bash) - fi - - save_cache: - paths: - - .tox - - ~/.cache/pip - - ~/.local - - ./eggs - key: v2-deps-{{ .Environment.CIRCLE_JOB }}-{{ checksum "setup.py" }}-{{ checksum "tox.ini" }} - -jobs: - lint: - <<: *common - docker: - - image: circleci/python:3.8 - environment: - - TOXENV=checkqa - - UPLOAD_COVERAGE=0 - py36dj22: - <<: *common - docker: - - image: circleci/python:3.6 - environment: - - TOXENV=py36-dj22 - py36dj30: - <<: *common - docker: - - image: circleci/python:3.6 - environment: - - TOXENV=py36-dj30 - py36djmaster: - <<: *common - docker: - - image: circleci/python:3.6 - environment: - - TOXENV=py36-djmaster - py37dj22: - <<: *common - docker: - - image: circleci/python:3.7 - environment: - - TOXENV=py37-dj22 - py37dj30: - <<: *common - docker: - - image: circleci/python:3.7 - environment: - - TOXENV=py37-dj30 - py37djmaster: - <<: *common - docker: - - image: circleci/python:3.7 - environment: - - TOXENV=py37-djmaster - py38dj22: - <<: *common - docker: - - image: circleci/python:3.8 - environment: - - TOXENV=py38-dj22 - py38dj30: - <<: *common - docker: - - image: circleci/python:3.8 - environment: - - TOXENV=py38-dj30 - py38djmaster: - <<: *common - docker: - - image: circleci/python:3.8 - environment: - - TOXENV=py38-djmaster - -workflows: - version: 2 - test: - jobs: - - lint - - py36dj22 - - py36dj30 - - py36djmaster - - py37dj22 - - py37dj30 - - py37djmaster - - py38dj22 - - py38dj30 - - py38djmaster diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..4f9f801 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,33 @@ +name: Publish + +on: + release: + types: [published] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.13" + - run: pip install build + - run: python -m build + - uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + + publish: + needs: build + runs-on: ubuntu-latest + environment: pypi + permissions: + id-token: write + steps: + - uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + - uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..6762dac --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,57 @@ +name: Test + +on: + push: + branches: [master] + pull_request: + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.13" + - run: pip install tox + - run: tox -e ruff + + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - run: pip install tox tox-gh-actions + - run: tox + + test-django-main: + runs-on: ubuntu-latest + continue-on-error: true + strategy: + fail-fast: false + matrix: + include: + - python-version: "3.12" + tox-env: py312-djmain + - python-version: "3.13" + tox-env: py313-djmain + - python-version: "3.14" + tox-env: py314-djmain + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - run: pip install tox + - run: tox -e ${{ matrix.tox-env }} diff --git a/README.md b/README.md index a7bcf71..4ade591 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,8 @@ [![](https://img.shields.io/pypi/v/django-authlink.svg)](https://pypi.python.org/pypi/django-authlink/) [![](https://img.shields.io/badge/license-MIT-blue.svg)](https://pypi.python.org/pypi/django-authlink/) -[![CircleCI](https://circleci.com/gh/lukeburden/django-authlink.svg?style=svg)](https://circleci.com/gh/lukeburden/django-authlink) -[![Codecov](https://codecov.io/gh/lukeburden/django-authlink/branch/master/graph/badge.svg)](https://codecov.io/gh/lukeburden/django-authlink) -[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) +[![Test](https://github.com/lukeburden/django-authlink/actions/workflows/test.yml/badge.svg)](https://github.com/lukeburden/django-authlink/actions/workflows/test.yml) +[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) ## django-authlink @@ -102,38 +101,33 @@ Default: 60 Allows increasing or decreasing the period of validity for an authlink. -### Contribute +### Supported versions -`django-authlink` supports a variety of Python and Django versions. It's best if you test each one of these before committing. Our [Circle CI Integration](https://circleci.com) will test these when you push but knowing before you commit prevents from having to do a lot of extra commits to get the build to pass. +`django-authlink` supports the Python and Django versions currently supported upstream: -#### Environment Setup +- Python 3.10 through 3.14 +- Django 5.2 (LTS) and 6.0 -In order to easily test on all these Pythons and run the exact same thing that CI will execute you'll want to setup [pyenv](https://github.com/yyuu/pyenv) and install the Python versions outlined in [tox.ini](tox.ini). +Django's `main` branch is also tested in CI, but failures there do not fail the build. -If you are on Mac OS X, it's recommended you use [brew](http://brew.sh/). After installing `brew` run: +### Contribute -```bash -brew install pyenv pyenv-virtualenv pyenv-virtualenvwrapper -``` +Tests run via [tox](https://tox.wiki) across all supported Python/Django combinations, and +[GitHub Actions](https://github.com/lukeburden/django-authlink/actions) runs the same +environments on push and pull request. -Next, install the various python versions we want to test against and create a virtualenv specifically for `django-authlink`: +To run the tests locally against the Python versions you have installed: ```bash -pyenv install 3.6.10 -pyenv install 3.7.6 -pyenv install 3.8.1 -pyenv virtualenv 3.8.1 authlink -pyenv activate authlink -pip install detox -pyenv shell authlink 3.6.10 3.7.6 +pip install tox +tox ``` -Now ensure the `authlink` virtualenv is activated, make the other python versions also on our path, and run the tests! - +To run a single environment, or lint/format checks with [Ruff](https://docs.astral.sh/ruff/): ```bash -pyenv shell authlink 3.6.10 3.7.6 -detox +tox -e py313-dj60 +tox -e ruff ``` -This will execute the test environments in parallel as defined in the `tox.ini`. +Releases are published to PyPI automatically when a GitHub release is created. diff --git a/authlink/adapter.py b/authlink/adapter.py index 99e6378..877fbb5 100644 --- a/authlink/adapter.py +++ b/authlink/adapter.py @@ -8,14 +8,14 @@ from django.contrib.auth import login, logout from django.http import HttpResponseForbidden from django.utils import timezone -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ -from ipware.ip import get_ip, get_real_ip +from ipware import get_client_ip from .models import AuthLink -class DefaultAuthLinkAdapter(object): +class DefaultAuthLinkAdapter: """ Most application logic should live here, such that it becomes easily overridable. @@ -35,15 +35,15 @@ def create(self, **kwargs): return authlink def calculate_expiry(self, created): - return created + datetime.timedelta( - seconds=getattr(settings, "AUTHLINK_TTL_SECONDS", 60) - ) + return created + datetime.timedelta(seconds=getattr(settings, "AUTHLINK_TTL_SECONDS", 60)) def extract_ipaddress(self, request): - ipaddress = get_real_ip(request) - if not ipaddress and settings.DEBUG: - ipaddress = get_ip(request) - return ipaddress + # only trust non-routable addresses when in DEBUG, mirroring the + # behaviour of the legacy ipware get_real_ip/get_ip functions + client_ip, is_routable = get_client_ip(request) + if is_routable or settings.DEBUG: + return client_ip + return None def add_message(self, request, level, message): messages.add_message(request, level, message) @@ -98,7 +98,7 @@ def in_url_whitelist(self, url): def get_whitelist_failure_response(self, request): return HttpResponseForbidden( - _("That URL is not whitelisted for your " "authentication method.") + _("That URL is not whitelisted for your authentication method.") ) diff --git a/authlink/api/rest_framework/serializers.py b/authlink/api/rest_framework/serializers.py index 2617aef..3b45eea 100644 --- a/authlink/api/rest_framework/serializers.py +++ b/authlink/api/rest_framework/serializers.py @@ -1,4 +1,4 @@ -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from rest_framework import serializers diff --git a/authlink/middleware.py b/authlink/middleware.py index 29eb6a0..e1b040f 100644 --- a/authlink/middleware.py +++ b/authlink/middleware.py @@ -3,10 +3,11 @@ from authlink.adapter import get_adapter + adapter = get_adapter() -class AuthLinkWhitelistMiddleware(object): +class AuthLinkWhitelistMiddleware: """ Only allow access to whitelisted URLs for sessions that are established using the authlink authentication mechanism. diff --git a/authlink/migrations/0001_initial.py b/authlink/migrations/0001_initial.py index 3356bc7..8f036b2 100644 --- a/authlink/migrations/0001_initial.py +++ b/authlink/migrations/0001_initial.py @@ -5,7 +5,6 @@ class Migration(migrations.Migration): - dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)] operations = [ @@ -32,9 +31,7 @@ class Migration(migrations.Migration): ("used", models.DateTimeField(null=True, blank=True)), ( "user", - models.ForeignKey( - to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE - ), + models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE), ), ], ) diff --git a/authlink/utils.py b/authlink/utils.py index e53125f..6ebbaa9 100644 --- a/authlink/utils.py +++ b/authlink/utils.py @@ -16,6 +16,4 @@ def get_timezone_now(): def generate_authlink_key(): - return get_random_string( - getattr(settings, "AUTHLINK_KEY_LENGTH", 64), VALID_KEY_CHARS - ) + return get_random_string(getattr(settings, "AUTHLINK_KEY_LENGTH", 64), VALID_KEY_CHARS) diff --git a/authlink/views.py b/authlink/views.py index 2073e20..8ef5d42 100644 --- a/authlink/views.py +++ b/authlink/views.py @@ -2,7 +2,7 @@ from django.db import transaction from django.http import HttpResponseRedirect from django.shortcuts import get_object_or_404 -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from django.views.generic import View from .adapter import get_adapter diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..06236c2 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,78 @@ +[build-system] +requires = ["setuptools>=61"] +build-backend = "setuptools.build_meta" + +[project] +name = "django-authlink" +version = "2.0.0" +description = "Provides magic-link authentication for Django web apps" +readme = "README.md" +license = { text = "MIT" } +authors = [{ name = "Luke Burden", email = "lukeburden@gmail.com" }] +requires-python = ">=3.10" +dependencies = [ + "django>=5.2", + "django-ipware>=3", + "djangorestframework>=3.16", +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Web Environment", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Framework :: Django", + "Framework :: Django :: 5.2", + "Framework :: Django :: 6.0", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", +] + +[project.urls] +Homepage = "https://github.com/lukeburden/django-authlink" + +[tool.setuptools.packages.find] +include = ["authlink*"] + +[tool.ruff] +line-length = 100 +target-version = "py310" + +[tool.ruff.format] +quote-style = "double" + +[tool.ruff.lint] +select = ["E", "F", "I", "W", "UP", "C90", "Q"] +ignore = ["E501"] + +[tool.ruff.lint.mccabe] +max-complexity = 10 + +[tool.ruff.lint.isort] +known-first-party = ["authlink"] +force-sort-within-sections = true +lines-after-imports = 2 +section-order = [ + "future", + "standard-library", + "django", + "third-party", + "first-party", + "local-folder", +] + +[tool.ruff.lint.isort.sections] +django = ["django"] + +[tool.coverage.run] +source = ["authlink"] +branch = true +data_file = ".coverage" + +[tool.coverage.report] +show_missing = true diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 9774edd..0000000 --- a/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -Django>=2 -django-ipware>=2 -djangorestframework>=3 diff --git a/setup.py b/setup.py deleted file mode 100755 index dd98dbd..0000000 --- a/setup.py +++ /dev/null @@ -1,41 +0,0 @@ -from setuptools import find_packages, setup - -name = "django-authlink" -description = "Provides magic-link authentication for Django web apps" -author = "Luke Burden" -author_email = "lukeburden@gmail.com" -url = "https://github.com/lukeburden/django-authlink" - -with open("README.md", "r") as fh: - long_description = fh.read() - -install_requires = [ - "django>=2,<4", - "django-ipware>=2", - "djangorestframework>=3", -] - -setup( - name=name, - author=author, - author_email=author_email, - description=description, - long_description=long_description, - long_description_content_type="text/markdown", - version="1.0.0", - license="MIT", - url=url, - packages=find_packages(exclude=["tests",]), - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Environment :: Web Environment", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Framework :: Django", - ], - install_requires=install_requires, - zip_safe=False, -) diff --git a/tests/manage.py b/tests/manage.py index 7f2d1a2..aa4f37c 100755 --- a/tests/manage.py +++ b/tests/manage.py @@ -1,6 +1,7 @@ #!/usr/bin/env python + from django.core import management -import os + if __name__ == "__main__": management.execute_from_command_line() diff --git a/tests/settings.py b/tests/settings.py index 8d16ae8..2451d64 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -1,10 +1,9 @@ import os import sys -import django # allow tests to find the `authlink` dir -sys.path.append(os.path.dirname(os.path.abspath(__file__)) + '/..' ) +sys.path.append(os.path.dirname(os.path.abspath(__file__)) + "/..") SECRET_KEY = "sosecreteh" @@ -49,4 +48,3 @@ "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", ) - diff --git a/tests/test_adapter.py b/tests/test_adapter.py index 09960da..94a0d72 100644 --- a/tests/test_adapter.py +++ b/tests/test_adapter.py @@ -1,22 +1,19 @@ -from django.db import IntegrityError -from django.contrib.auth import get_user_model -from django.contrib.auth import SESSION_KEY -from django.contrib.auth import BACKEND_SESSION_KEY -from django.contrib.auth.models import AnonymousUser +import datetime +from importlib import import_module + from django.conf import settings -from django.test import TestCase -from django.test import RequestFactory +from django.contrib.auth import BACKEND_SESSION_KEY, SESSION_KEY, get_user_model +from django.contrib.auth.models import AnonymousUser +from django.db import IntegrityError +from django.test import RequestFactory, TestCase from django.test.utils import override_settings from django.utils import timezone -from authlink.adapter import get_adapter -from authlink.adapter import DefaultAuthLinkAdapter + +from authlink.adapter import DefaultAuthLinkAdapter, get_adapter from authlink.models import AuthLink from .utils import mock_now -import datetime -from importlib import import_module - class TestAdapter(DefaultAuthLinkAdapter): pass @@ -56,15 +53,14 @@ def test_create_ok(self): self.assertEqual(authlink.user, self.user) self.assertEqual( authlink.expires, - authlink.created - + datetime.timedelta(seconds=settings.AUTHLINK_TTL_SECONDS), + authlink.created + datetime.timedelta(seconds=settings.AUTHLINK_TTL_SECONDS), ) def test_create_ipaddress_missing(self): request = self.factory.get("/some/url") request.user = self.user with self.assertRaises(IntegrityError): - authlink = self.adapter.create(**{"url": "/some/url", "request": request}) + self.adapter.create(**{"url": "/some/url", "request": request}) def test_create_user_not_authenticated(self): request = self.factory.get("/some/url") @@ -72,14 +68,12 @@ def test_create_user_not_authenticated(self): request.user = AnonymousUser() request.META = {"REMOTE_ADDR": "177.139.233.133"} with self.assertRaises(RuntimeError): - authlink = self.adapter.create(**{"url": "/some/url", "request": request}) + self.adapter.create(**{"url": "/some/url", "request": request}) def test_calculate_expiry(self): now = timezone.now() expires = self.adapter.calculate_expiry(now) - self.assertEqual( - expires, now + datetime.timedelta(seconds=settings.AUTHLINK_TTL_SECONDS) - ) + self.assertEqual(expires, now + datetime.timedelta(seconds=settings.AUTHLINK_TTL_SECONDS)) @override_settings(AUTHLINK_TTL_SECONDS=62) @mock_now @@ -188,9 +182,7 @@ def test_get_full_url(self): @override_settings(AUTHLINK_URL_TEMPLATE="/some/url/{key}") def test_get_full_url_configurable(self): - self.assertIn( - "/some/url/%s" % self.authlink.key, self.adapter.get_full_url(self.authlink) - ) + self.assertIn(f"/some/url/{self.authlink.key}", self.adapter.get_full_url(self.authlink)) def test_in_url_whitelist_default_deny(self): self.assertFalse(self.adapter.in_url_whitelist("/nothing/will/work")) diff --git a/tests/test_api.py b/tests/test_api.py index 01c5f5b..0339de3 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,10 +1,12 @@ from django.contrib.auth import get_user_model -from django.urls import reverse from django.test import TestCase from django.test.utils import override_settings -from authlink.models import AuthLink +from django.urls import reverse + from rest_framework.test import APIClient +from authlink.models import AuthLink + @override_settings(AUTHLINK_URL_WHITELIST=[r"^/very/specific/url/$"]) class APITestCase(TestCase): diff --git a/tests/test_middleware.py b/tests/test_middleware.py index 1654ae7..7bb7fcb 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -1,18 +1,16 @@ +import datetime + from django.conf import settings -from django.contrib.auth import get_user_model -from django.contrib.auth import SESSION_KEY -from django.contrib.auth import BACKEND_SESSION_KEY +from django.contrib.auth import SESSION_KEY, get_user_model from django.core.exceptions import ImproperlyConfigured -from django.urls import reverse -from django.test import TestCase -from django.test import Client +from django.test import Client, TestCase from django.test.utils import override_settings +from django.urls import reverse from django.utils import timezone -from .utils import mock_now from authlink.models import AuthLink -import datetime +from .utils import mock_now TEST_URL = "/very/specific/url/" @@ -50,8 +48,7 @@ def test_middleware_not_active(self): self.assertEqual(response.status_code, 200) @override_settings( - MIDDLEWARE=settings.MIDDLEWARE - + ("authlink.middleware.AuthLinkWhitelistMiddleware",), + MIDDLEWARE=settings.MIDDLEWARE + ("authlink.middleware.AuthLinkWhitelistMiddleware",), AUTHLINK_URL_WHITELIST=(r"/authenticatedview/",), ) def test_middleware_active_url_whitelisted(self): @@ -66,8 +63,7 @@ def test_middleware_active_url_whitelisted(self): self.assertEqual(response.status_code, 200) @override_settings( - MIDDLEWARE=settings.MIDDLEWARE - + ("authlink.middleware.AuthLinkWhitelistMiddleware",), + MIDDLEWARE=settings.MIDDLEWARE + ("authlink.middleware.AuthLinkWhitelistMiddleware",), AUTHLINK_URL_WHITELIST=[], ) def test_middleware_active_url_not_whitelisted(self): @@ -85,9 +81,7 @@ def test_middleware_active_url_not_whitelisted(self): "That URL is not whitelisted for your authentication method.", ) - @override_settings( - MIDDLEWARE=("authlink.middleware.AuthLinkWhitelistMiddleware",) - ) + @override_settings(MIDDLEWARE=("authlink.middleware.AuthLinkWhitelistMiddleware",)) def test_middleware_after_session_middleware(self): with self.assertRaises(ImproperlyConfigured) as ic: response = self.client.get( diff --git a/tests/test_views.py b/tests/test_views.py index 48860c4..d8e2835 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -1,17 +1,14 @@ - +import datetime from django.conf import settings -from django.contrib.auth import get_user_model -from django.contrib.auth import SESSION_KEY +from django.contrib.auth import SESSION_KEY, get_user_model +from django.test import Client, TestCase from django.urls import reverse -from django.test import TestCase -from django.test import Client from django.utils import timezone -from .utils import mock_now from authlink.models import AuthLink -import datetime +from .utils import mock_now TEST_URL = "/very/specific/url/" diff --git a/tests/urls.py b/tests/urls.py index f29380a..d8747f9 100644 --- a/tests/urls.py +++ b/tests/urls.py @@ -1,7 +1,6 @@ -from django.conf.urls import url - from django.contrib.auth.decorators import login_required from django.http import HttpResponse +from django.urls import re_path from django.views.generic import View from authlink.api.rest_framework.views import AuthLinkCreateView @@ -18,9 +17,9 @@ def get(self, request, *args, **kwargs): urlpatterns = [ - url(r"^api/authlink/$", AuthLinkCreateView.as_view(), name="authlink_generate"), - url(r"^authlink/(?P[\w]+)$", AuthLinkView.as_view(), name="authlink_use"), - url( + re_path(r"^api/authlink/$", AuthLinkCreateView.as_view(), name="authlink_generate"), + re_path(r"^authlink/(?P[\w]+)$", AuthLinkView.as_view(), name="authlink_use"), + re_path( r"^authenticatedview/$", login_required(AuthenticatedView.as_view()), name="authenticated_view", diff --git a/tests/utils.py b/tests/utils.py index 4e8f3af..df66ee0 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,14 +1,14 @@ -import mock import datetime +from unittest import mock + from django.utils import timezone + current_timezone = timezone.get_current_timezone() mock_now = mock.patch( "django.utils.timezone.now", mock.Mock( - side_effect=lambda: current_timezone.localize( - datetime.datetime(2015, 10, 0o7, 14, 23, 0) - ) + side_effect=lambda: datetime.datetime(2015, 10, 7, 14, 23, 0, tzinfo=current_timezone) ), ) diff --git a/tox.ini b/tox.ini index d1f7665..a7f4ac0 100644 --- a/tox.ini +++ b/tox.ini @@ -1,75 +1,39 @@ -[flake8] -ignore = E265,E501,W391 -max-line-length = 100 -max-complexity = 10 -exclude = docs/* -inline-quotes = double - -[isort] -multi_line_output=3 -known_django=django -known_first_party=authlink -sections=FUTURE,STDLIB,DJANGO,THIRDPARTY,FIRSTPARTY,LOCALFOLDER -include_trailing_comma=True -force_grid_wrap=0 -use_parentheses=True -line_length=88 -force_sort_within_sections=True - -[coverage:run] -source = authlink -omit = authlink/tests/* -branch = true -data_file = .coverage - -[coverage:report] -omit = authlink/tests/* -show_missing = True - [tox] -envlist = - checkqa - py{36,37,38}-dj{22,30,master} +env_list = + ruff + py{310,311,312,313,314}-dj52 + py{312,313,314}-dj60 + +[gh-actions] +python = + 3.10: py310 + 3.11: py311 + 3.12: py312 + 3.13: py313 + 3.14: py314 [testenv] +package = editable passenv = - CI CIRCLECI CIRCLE_* - CONSTANTS_DATABASE_ENGINE - CONSTANTS_DATABASE_HOST - CONSTANTS_DATABASE_NAME - CONSTANTS_DATABASE_USER - -# we assume use of semantic versioning on deps + CI deps = - coverage - codecov - mock - django-ipware>=2 - # adding these envs with unbounded DRF for some variation - dj22: Django~=2.2.8 - dj22: djangorestframework>=3.10 - dj30: Django==3.0.* - dj30: djangorestframework>=3.10 - djmaster: https://github.com/django/django/tarball/master - djmaster: https://github.com/encode/django-rest-framework/tarball/master - -usedevelop = True - + coverage[toml] + dj52: Django>=5.2,<5.3 + dj52: djangorestframework>=3.16 + dj60: Django>=6.0,<6.1 + dj60: djangorestframework>=3.17 + djmain: https://github.com/django/django/tarball/main + djmain: https://github.com/encode/django-rest-framework/tarball/master setenv = DJANGO_SETTINGS_MODULE=settings - commands = - coverage run tests/manage.py test {posargs} + coverage run tests/manage.py test tests {posargs} coverage report -m --skip-covered -[testenv:checkqa] -commands = - flake8 authlink - isort -rc --check-only --diff authlink - black authlink --check - +[testenv:ruff] +skip_install = true deps = - flake8==3.7.9 - flake8-quotes==2.1.1 - isort==4.3.21 - black==19.10b0 + ruff == 0.9.6 +commands = + ruff check authlink tests + ruff format --check authlink tests