From de04e0aeffada0beade393d2c8c19c00904fffc3 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Sat, 6 Jun 2026 11:25:43 +0100 Subject: [PATCH] Bump all the dependencies We also change our testing matrix so that we only test the upper and lower bounds of supported Python releases. This should ease pressure on CI somewhat. We also switch from the deprecated psycopg2 to psycopg (which actually corresponds to v3 now). Changes to the source were auto-generated with django-upgrade. Signed-off-by: Stephen Finucane --- .github/workflows/ci.yaml | 38 +++--- docker-compose-pg.yml | 2 +- manage.py | 5 - patchwork/admin.py | 73 ++++-------- patchwork/forms.py | 6 +- patchwork/settings/base.py | 2 +- patchwork/settings/production.example.py | 11 +- patchwork/tests/views/test_bundles.py | 16 ++- patchwork/tests/views/test_mail.py | 109 +++++------------ patchwork/tests/views/test_patch.py | 22 +--- patchwork/tests/views/test_user.py | 112 +++++------------- patchwork/views/xmlrpc.py | 4 +- .../django-5-0-support-923e45ec2dc93117.yaml | 2 +- .../django-5-2-support-32a602166789f7f8.yaml | 9 ++ .../django-6-0-support-1295530f992e1158.yaml | 5 + .../python-3-14-support-8dc97577ffc4c8a0.yaml | 9 ++ requirements-dev.txt | 10 +- requirements-prod.txt | 10 +- requirements-test.txt | 10 +- tox.ini | 23 ++-- 20 files changed, 184 insertions(+), 294 deletions(-) create mode 100644 releasenotes/notes/django-5-2-support-32a602166789f7f8.yaml create mode 100644 releasenotes/notes/django-6-0-support-1295530f992e1158.yaml create mode 100644 releasenotes/notes/python-3-14-support-8dc97577ffc4c8a0.yaml diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 796bf32a..e7c43e71 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -12,16 +12,16 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout source code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: - python-version: "3.13" + python-version: "3.14" cache: 'pip' - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: - go-version: "1.21" + go-version: "1.26" - name: Install dependencies run: python -m pip install tox - name: Run tox @@ -33,7 +33,7 @@ jobs: matrix: # NOTE: If you add a version here, don't forget to update the # '[gh-actions]' section in tox.ini - python: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python: ["3.11", "3.14"] db: [postgres, mysql, sqlite3] env: DATABASE_TYPE: "${{ matrix.db }}" @@ -44,7 +44,7 @@ jobs: MYSQL_ROOT_PASSWORD: root-${{ github.run_id }} services: postgres: - image: postgres:latest + image: postgres:17 env: POSTGRES_DB: ${{ env.DATABASE_NAME }} POSTGRES_PASSWORD: ${{ env.DATABASE_PASSWORD }} @@ -57,7 +57,7 @@ jobs: --health-timeout 5s --health-retries 5 mysql: - image: mysql:latest + image: mysql:8.4 env: MYSQL_DATABASE: ${{ env.DATABASE_NAME }} MYSQL_USER: ${{ env.DATABASE_USER }} @@ -72,9 +72,9 @@ jobs: --health-retries 5 steps: - name: Checkout source code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python }} cache: 'pip' @@ -104,20 +104,22 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout source code - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 + - name: Fetch all branches + run: git fetch --all - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: - python-version: "3.13" + python-version: "3.14" cache: 'pip' - name: Install dependencies run: python -m pip install tox - name: Build docs (via tox) run: tox -e docs - name: Archive build results - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: html-docs-build path: docs/_build/html @@ -132,13 +134,13 @@ jobs: COMPOSE_FILE: ${{ matrix.db == 'mysql' && 'docker-compose.yml' || (matrix.db == 'postgres' && 'docker-compose-pg.yml') || 'docker-compose-sqlite3.yml' }} steps: - name: Checkout source code - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: - python-version: "3.13" + python-version: "3.14" cache: 'pip' - name: Build docker-compose service run: | @@ -151,7 +153,7 @@ jobs: { echo patchwork; echo patchwork; } | \ docker compose run -T --rm web \ python manage.py changepassword patchwork - # FIXME(stephenfin): Re-enable this once dbbackup supports Django 4.0 + # FIXME(stephenfin): Re-enable this once we've update configuration accordingly # - name: Test dbbackup/dbrestore # run: | # docker compose run -T --rm web python manage.py dbbackup diff --git a/docker-compose-pg.yml b/docker-compose-pg.yml index 25cf8ff7..3e277e5c 100644 --- a/docker-compose-pg.yml +++ b/docker-compose-pg.yml @@ -1,7 +1,7 @@ --- services: db: - image: postgres:latest + image: postgres:17 volumes: - ./tools/docker/db/postdata:/var/lib/postgresql/data environment: diff --git a/manage.py b/manage.py index a2f4bd33..faddbd02 100755 --- a/manage.py +++ b/manage.py @@ -7,11 +7,6 @@ if __name__ == '__main__': 'DJANGO_SETTINGS_MODULE', 'patchwork.settings.production' ) - import django - - if django.VERSION < (3, 2): - raise Exception('Patchwork requires Django 3.2 or greater') - from django.core.management import execute_from_command_line execute_from_command_line(sys.argv) diff --git a/patchwork/admin.py b/patchwork/admin.py index d1c389a1..d3bdae1b 100644 --- a/patchwork/admin.py +++ b/patchwork/admin.py @@ -44,6 +44,7 @@ class DelegationRuleInline(admin.TabularInline): fields = ('path', 'user', 'priority') +@admin.register(Project) class ProjectAdmin(admin.ModelAdmin): list_display = ('name', 'linkname', 'listid', 'listemail') inlines = [ @@ -51,31 +52,26 @@ class ProjectAdmin(admin.ModelAdmin): ] -admin.site.register(Project, ProjectAdmin) - - +@admin.register(Person) class PersonAdmin(admin.ModelAdmin): list_display = ('__str__', 'has_account') search_fields = ('name', 'email') + @admin.display( + description='Account', + boolean=True, + ordering='user', + ) def has_account(self, person): return bool(person.user) - has_account.boolean = True - has_account.admin_order_field = 'user' - has_account.short_description = 'Account' - - -admin.site.register(Person, PersonAdmin) - +@admin.register(State) class StateAdmin(admin.ModelAdmin): list_display = ('name', 'action_required') -admin.site.register(State, StateAdmin) - - +@admin.register(Cover) class CoverAdmin(admin.ModelAdmin): list_display = ('name', 'submitter', 'project', 'date') list_filter = ('project',) @@ -83,9 +79,7 @@ class CoverAdmin(admin.ModelAdmin): date_hierarchy = 'date' -admin.site.register(Cover, CoverAdmin) - - +@admin.register(Patch) class PatchAdmin(admin.ModelAdmin): list_display = ( 'name', @@ -101,40 +95,35 @@ class PatchAdmin(admin.ModelAdmin): search_fields = ('name', 'submitter__name', 'submitter__email') date_hierarchy = 'date' + @admin.display( + description='Pull', + boolean=True, + ordering='pull_url', + ) def is_pull_request(self, patch): return bool(patch.pull_url) - is_pull_request.boolean = True - is_pull_request.admin_order_field = 'pull_url' - is_pull_request.short_description = 'Pull' - - -admin.site.register(Patch, PatchAdmin) - +@admin.register(CoverComment) class CoverCommentAdmin(admin.ModelAdmin): list_display = ('cover', 'submitter', 'date') search_fields = ('cover__name', 'submitter__name', 'submitter__email') date_hierarchy = 'date' -admin.site.register(CoverComment, CoverCommentAdmin) - - +@admin.register(PatchComment) class PatchCommentAdmin(admin.ModelAdmin): list_display = ('patch', 'submitter', 'date') search_fields = ('patch__name', 'submitter__name', 'submitter__email') date_hierarchy = 'date' -admin.site.register(PatchComment, PatchCommentAdmin) - - class PatchInline(admin.StackedInline): model = Patch extra = 0 +@admin.register(Series) class SeriesAdmin(admin.ModelAdmin): list_display = ( 'name', @@ -154,11 +143,10 @@ class SeriesAdmin(admin.ModelAdmin): filter_horizontal = ('dependencies',) inlines = (PatchInline,) + @admin.display(boolean=True) def received_all(self, series): return series.received_all - received_all.boolean = True - def get_queryset(self, request): qs = super(SeriesAdmin, self).get_queryset(request) return qs.prefetch_related( @@ -171,16 +159,12 @@ class SeriesAdmin(admin.ModelAdmin): ) -admin.site.register(Series, SeriesAdmin) - - +@admin.register(SeriesReference) class SeriesReferenceAdmin(admin.ModelAdmin): model = SeriesReference -admin.site.register(SeriesReference, SeriesReferenceAdmin) - - +@admin.register(Check) class CheckAdmin(admin.ModelAdmin): list_display = ( 'patch', @@ -195,27 +179,18 @@ class CheckAdmin(admin.ModelAdmin): date_hierarchy = 'date' -admin.site.register(Check, CheckAdmin) - - +@admin.register(Bundle) class BundleAdmin(admin.ModelAdmin): list_display = ('name', 'owner', 'project', 'public') list_filter = ('public', 'project') search_fields = ('name', 'owner') -admin.site.register(Bundle, BundleAdmin) - - +@admin.register(Tag) class TagAdmin(admin.ModelAdmin): list_display = ('name',) -admin.site.register(Tag, TagAdmin) - - +@admin.register(PatchRelation) class PatchRelationAdmin(admin.ModelAdmin): model = PatchRelation - - -admin.site.register(PatchRelation, PatchRelationAdmin) diff --git a/patchwork/forms.py b/patchwork/forms.py index cf77bdcc..efedfa97 100644 --- a/patchwork/forms.py +++ b/patchwork/forms.py @@ -3,7 +3,6 @@ # # SPDX-License-Identifier: GPL-2.0-or-later -import django from django.contrib.auth.models import User from django import forms from django.forms import renderers @@ -179,10 +178,7 @@ class OptionalModelChoiceField(forms.ModelChoiceField): choices.append(self.no_change_choice) return choices - if django.VERSION >= (5, 0): - choices = property(_get_choices, forms.ChoiceField.choices.fset) - else: - choices = property(_get_choices, forms.ChoiceField._set_choices) + choices = property(_get_choices, forms.ChoiceField.choices.fset) def is_no_change(self, value): return value == self.no_change_choice[0] diff --git a/patchwork/settings/base.py b/patchwork/settings/base.py index dccab6c3..2b557fc1 100644 --- a/patchwork/settings/base.py +++ b/patchwork/settings/base.py @@ -119,7 +119,7 @@ STATICFILES_DIRS = [ if os.getenv('DATABASE_TYPE') == 'postgres': DATABASES = { 'default': { - 'ENGINE': 'django.db.backends.postgresql_psycopg2', + 'ENGINE': 'django.db.backends.postgresql', 'HOST': os.environ.get('DATABASE_HOST', 'localhost'), 'PORT': os.environ.get('DATABASE_PORT', ''), 'NAME': os.environ.get('DATABASE_NAME', 'patchwork'), diff --git a/patchwork/settings/production.example.py b/patchwork/settings/production.example.py index 8955a204..65cb1455 100644 --- a/patchwork/settings/production.example.py +++ b/patchwork/settings/production.example.py @@ -56,6 +56,11 @@ ADMINS = ( STATIC_ROOT = os.environ.get('STATIC_ROOT', '/srv/patchwork/htdocs/static') -STATICFILES_STORAGE = ( - 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage' -) +STORAGES = { + 'default': { + 'BACKEND': 'django.core.files.storage.FileSystemStorage', + }, + 'staticfiles': { + 'BACKEND': 'django.contrib.staticfiles.storage.ManifestStaticFilesStorage', + }, +} diff --git a/patchwork/tests/views/test_bundles.py b/patchwork/tests/views/test_bundles.py index 1d317c30..722abd88 100644 --- a/patchwork/tests/views/test_bundles.py +++ b/patchwork/tests/views/test_bundles.py @@ -331,14 +331,18 @@ class BundlePrivateViewMboxTest(BundlePrivateViewTest): # Check we can view as owner auth_string = _get_auth_string(self.user) - response = self.client.get(self.url, HTTP_AUTHORIZATION=auth_string) + response = self.client.get( + self.url, headers={'authorization': auth_string} + ) self.assertEqual(response.status_code, 200) self.assertContains(response, self.patches[0].name) # Check we can't view as another user auth_string = _get_auth_string(self.other_user) - response = self.client.get(self.url, HTTP_AUTHORIZATION=auth_string) + response = self.client.get( + self.url, headers={'authorization': auth_string} + ) self.assertEqual(response.status_code, 404) def test_private_bundle_mbox_token_auth(self): @@ -353,14 +357,18 @@ class BundlePrivateViewMboxTest(BundlePrivateViewTest): # Check we can view as owner auth_string = _get_auth_string(self.user) - response = self.client.get(self.url, HTTP_AUTHORIZATION=auth_string) + response = self.client.get( + self.url, headers={'authorization': auth_string} + ) self.assertEqual(response.status_code, 200) self.assertContains(response, self.patches[0].name) # Check we can't view as another user auth_string = _get_auth_string(self.other_user) - response = self.client.get(self.url, HTTP_AUTHORIZATION=auth_string) + response = self.client.get( + self.url, headers={'authorization': auth_string} + ) self.assertEqual(response.status_code, 404) diff --git a/patchwork/tests/views/test_mail.py b/patchwork/tests/views/test_mail.py index 798b83f4..b8545dc7 100644 --- a/patchwork/tests/views/test_mail.py +++ b/patchwork/tests/views/test_mail.py @@ -5,7 +5,6 @@ import re -import django from django.core import mail from django.test import TestCase from django.urls import reverse @@ -34,37 +33,21 @@ class MailSettingsTest(TestCase): response = self.client.post(reverse('mail-settings'), {'email': ''}) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, 'patchwork/mail.html') - if django.VERSION >= (4, 1): - self.assertFormError( - response.context['form'], - 'email', - 'This field is required.', - ) - else: - self.assertFormError( - response, - 'form', - 'email', - 'This field is required.', - ) + self.assertFormError( + response.context['form'], + 'email', + 'This field is required.', + ) def test_post_invalid(self): response = self.client.post(reverse('mail-settings'), {'email': 'foo'}) self.assertEqual(response.status_code, 200) self.assertTemplateUsed(response, 'patchwork/mail.html') - if django.VERSION >= (4, 1): - self.assertFormError( - response.context['form'], - 'email', - error_strings['email'], - ) - else: - self.assertFormError( - response, - 'form', - 'email', - error_strings['email'], - ) + self.assertFormError( + response.context['form'], + 'email', + error_strings['email'], + ) def test_post_optin(self): email = 'foo@example.com' @@ -114,19 +97,11 @@ class OptoutRequestTest(TestCase): def test_post_empty(self): response = self.client.post(reverse('mail-optout'), {'email': ''}) self.assertEqual(response.status_code, 200) - if django.VERSION >= (4, 1): - self.assertFormError( - response.context['form'], - 'email', - 'This field is required.', - ) - else: - self.assertFormError( - response, - 'form', - 'email', - 'This field is required.', - ) + self.assertFormError( + response.context['form'], + 'email', + 'This field is required.', + ) self.assertTrue(response.context['error']) self.assertNotIn('confirmation', response.context) self.assertEqual(len(mail.outbox), 0) @@ -134,19 +109,11 @@ class OptoutRequestTest(TestCase): def test_post_non_email(self): response = self.client.post(reverse('mail-optout'), {'email': 'foo'}) self.assertEqual(response.status_code, 200) - if django.VERSION >= (4, 1): - self.assertFormError( - response.context['form'], - 'email', - error_strings['email'], - ) - else: - self.assertFormError( - response, - 'form', - 'email', - error_strings['email'], - ) + self.assertFormError( + response.context['form'], + 'email', + error_strings['email'], + ) self.assertTrue(response.context['error']) self.assertNotIn('confirmation', response.context) self.assertEqual(len(mail.outbox), 0) @@ -216,19 +183,11 @@ class OptinRequestTest(TestCase): def test_post_empty(self): response = self.client.post(reverse('mail-optin'), {'email': ''}) self.assertEqual(response.status_code, 200) - if django.VERSION >= (4, 1): - self.assertFormError( - response.context['form'], - 'email', - 'This field is required.', - ) - else: - self.assertFormError( - response, - 'form', - 'email', - 'This field is required.', - ) + self.assertFormError( + response.context['form'], + 'email', + 'This field is required.', + ) self.assertTrue(response.context['error']) self.assertNotIn('confirmation', response.context) self.assertEqual(len(mail.outbox), 0) @@ -236,19 +195,11 @@ class OptinRequestTest(TestCase): def test_post_non_email(self): response = self.client.post(reverse('mail-optin'), {'email': 'foo'}) self.assertEqual(response.status_code, 200) - if django.VERSION >= (4, 1): - self.assertFormError( - response.context['form'], - 'email', - error_strings['email'], - ) - else: - self.assertFormError( - response, - 'form', - 'email', - error_strings['email'], - ) + self.assertFormError( + response.context['form'], + 'email', + error_strings['email'], + ) self.assertTrue(response.context['error']) self.assertNotIn('confirmation', response.context) self.assertEqual(len(mail.outbox), 0) diff --git a/patchwork/tests/views/test_patch.py b/patchwork/tests/views/test_patch.py index 3de558f0..133bae36 100644 --- a/patchwork/tests/views/test_patch.py +++ b/patchwork/tests/views/test_patch.py @@ -8,7 +8,6 @@ from datetime import timedelta import re import unittest -import django from django.conf import settings from django.test import TestCase from django.urls import reverse @@ -460,21 +459,12 @@ class PatchUpdateTest(TestCase): new_states = [Patch.objects.get(pk=p.pk).state for p in self.patches] self.assertEqual(new_states, orig_states) - if django.VERSION >= (4, 1): - self.assertFormError( - response.context['patch_form'], - 'state', - 'Select a valid choice. That choice is not one ' - 'of the available choices.', - ) - else: - self.assertFormError( - response, - 'patch_form', - 'state', - 'Select a valid choice. That choice is not one ' - 'of the available choices.', - ) + self.assertFormError( + response.context['patch_form'], + 'state', + 'Select a valid choice. That choice is not one ' + 'of the available choices.', + ) def _test_delegate_change(self, delegate_str): data = self.base_data.copy() diff --git a/patchwork/tests/views/test_user.py b/patchwork/tests/views/test_user.py index 3c3b0c5d..5fe4a82f 100644 --- a/patchwork/tests/views/test_user.py +++ b/patchwork/tests/views/test_user.py @@ -6,7 +6,6 @@ import string import secrets -import django from django.contrib.auth.models import User from django.core import mail from django.test.client import Client @@ -80,38 +79,22 @@ class RegistrationTest(TestCase): del data[field] response = self.client.post('/register/', data) self.assertEqual(response.status_code, 200) - if django.VERSION >= (4, 1): - self.assertFormError( - response.context['form'], - field, - self.required_error, - ) - else: - self.assertFormError( - response, - 'form', - field, - self.required_error, - ) + self.assertFormError( + response.context['form'], + field, + self.required_error, + ) def test_invalid_username(self): data = self.default_data.copy() data['username'] = 'invalid user' response = self.client.post('/register/', data) self.assertEqual(response.status_code, 200) - if django.VERSION >= (4, 1): - self.assertFormError( - response.context['form'], - 'username', - self.invalid_error, - ) - else: - self.assertFormError( - response, - 'form', - 'username', - self.invalid_error, - ) + self.assertFormError( + response.context['form'], + 'username', + self.invalid_error, + ) def test_existing_username(self): user = create_user() @@ -119,19 +102,11 @@ class RegistrationTest(TestCase): data['username'] = user.username response = self.client.post('/register/', data) self.assertEqual(response.status_code, 200) - if django.VERSION >= (4, 1): - self.assertFormError( - response.context['form'], - 'username', - 'This username is already taken. Please choose another.', - ) - else: - self.assertFormError( - response, - 'form', - 'username', - 'This username is already taken. Please choose another.', - ) + self.assertFormError( + response.context['form'], + 'username', + 'This username is already taken. Please choose another.', + ) def test_existing_email(self): user = create_user() @@ -139,21 +114,12 @@ class RegistrationTest(TestCase): data['email'] = user.email response = self.client.post('/register/', data) self.assertEqual(response.status_code, 200) - if django.VERSION >= (4, 1): - self.assertFormError( - response.context['form'], - 'email', - 'This email address is already in use for the account ' - '"%s".\n' % user.username, - ) - else: - self.assertFormError( - response, - 'form', - 'email', - 'This email address is already in use for the account ' - '"%s".\n' % user.username, - ) + self.assertFormError( + response.context['form'], + 'email', + 'This email address is already in use for the account ' + '"%s".\n' % user.username, + ) def test_valid_registration(self): response = self.client.post('/register/', self.default_data) @@ -304,37 +270,21 @@ class UserLinkTest(_UserTestCase): response = self.client.post(reverse('user-link'), {'email': ''}) self.assertEqual(response.status_code, 200) self.assertTrue(response.context['linkform']) - if django.VERSION >= (4, 1): - self.assertFormError( - response.context['linkform'], - 'email', - 'This field is required.', - ) - else: - self.assertFormError( - response, - 'linkform', - 'email', - 'This field is required.', - ) + self.assertFormError( + response.context['linkform'], + 'email', + 'This field is required.', + ) def test_user_person_request_invalid(self): response = self.client.post(reverse('user-link'), {'email': 'foo'}) self.assertEqual(response.status_code, 200) self.assertTrue(response.context['linkform']) - if django.VERSION >= (4, 1): - self.assertFormError( - response.context['linkform'], - 'email', - error_strings['email'], - ) - else: - self.assertFormError( - response, - 'linkform', - 'email', - error_strings['email'], - ) + self.assertFormError( + response.context['linkform'], + 'email', + error_strings['email'], + ) def test_user_person_request_valid(self): response = self.client.post( diff --git a/patchwork/views/xmlrpc.py b/patchwork/views/xmlrpc.py index a936798e..8137720a 100644 --- a/patchwork/views/xmlrpc.py +++ b/patchwork/views/xmlrpc.py @@ -49,8 +49,8 @@ class PatchworkXMLRPCDispatcher(SimpleXMLRPCDispatcher, XMLRPCDocGenerator): def _user_for_request(self, request): auth_header = None - if 'HTTP_AUTHORIZATION' in request.META: - auth_header = request.META.get('HTTP_AUTHORIZATION') + if 'authorization' in request.headers: + auth_header = request.headers.get('authorization') elif 'Authorization' in request.META: auth_header = request.META.get('Authorization') diff --git a/releasenotes/notes/django-5-0-support-923e45ec2dc93117.yaml b/releasenotes/notes/django-5-0-support-923e45ec2dc93117.yaml index 2e7aa35e..09bfcc51 100644 --- a/releasenotes/notes/django-5-0-support-923e45ec2dc93117.yaml +++ b/releasenotes/notes/django-5-0-support-923e45ec2dc93117.yaml @@ -5,5 +5,5 @@ features: now supported. upgrade: - | - Django 3.2 and 4.1 are no longer supported. Bother releases are no longer + Django 3.2 and 4.1 are no longer supported. Both releases are no longer supported upstream and most distributions provide a newer version. diff --git a/releasenotes/notes/django-5-2-support-32a602166789f7f8.yaml b/releasenotes/notes/django-5-2-support-32a602166789f7f8.yaml new file mode 100644 index 00000000..d6adec8d --- /dev/null +++ b/releasenotes/notes/django-5-2-support-32a602166789f7f8.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + `Django 5.2 `_ is + now supported. +upgrade: + - | + Django 4.2, 5.0 and 5.1 are no longer supported. These releases are no + longer supported upstream and most distributions provide a newer version. diff --git a/releasenotes/notes/django-6-0-support-1295530f992e1158.yaml b/releasenotes/notes/django-6-0-support-1295530f992e1158.yaml new file mode 100644 index 00000000..4d131e60 --- /dev/null +++ b/releasenotes/notes/django-6-0-support-1295530f992e1158.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + `Django 6.0 `_ is + now supported. diff --git a/releasenotes/notes/python-3-14-support-8dc97577ffc4c8a0.yaml b/releasenotes/notes/python-3-14-support-8dc97577ffc4c8a0.yaml new file mode 100644 index 00000000..3cf353ec --- /dev/null +++ b/releasenotes/notes/python-3-14-support-8dc97577ffc4c8a0.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + `Python 3.14 `_ is + now supported. +upgrade: + - | + Python 3.9 and 3.10 are no longer supported. These releases are no longer + supported upstream and most distributions provide a newer version. diff --git a/requirements-dev.txt b/requirements-dev.txt index 90bc4c31..a1d0e03e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,6 +1,6 @@ -Django~=5.1.0 -djangorestframework~=3.15.2 -django-filter~=24.2.0 -django-debug-toolbar~=5.0.1 -django-dbbackup~=4.2.0 +Django~=6.0.0 +djangorestframework~=3.17.1 +django-filter~=25.2.0 +django-debug-toolbar~=6.3.0 +django-dbbackup~=5.3.0 -r requirements-test.txt diff --git a/requirements-prod.txt b/requirements-prod.txt index 4bd824a7..97973113 100644 --- a/requirements-prod.txt +++ b/requirements-prod.txt @@ -1,5 +1,5 @@ -Django~=5.1.0 -djangorestframework~=3.15.0 -django-filter~=24.3.0 -psycopg2~=2.9.10 -sqlparse~=0.5.1 +Django~=6.0.0 +djangorestframework~=3.17.1 +django-filter~=25.2.0 +psycopg~=3.3.4 +sqlparse~=0.5.5 diff --git a/requirements-test.txt b/requirements-test.txt index 1371abd3..8c88ae0f 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,8 +1,8 @@ -mysqlclient~=2.2.5 -psycopg2-binary~=2.9.9 -sqlparse~=0.5.1 +mysqlclient~=2.2.8 +psycopg~=3.3.4 +sqlparse~=0.5.5 python-dateutil~=2.9.0 -tblib~=3.0.0 +tblib~=3.2.2 openapi-core~=0.19.2 jsonschema-path~=0.3.3 -termcolor~=2.5.0 +termcolor~=3.3.0 diff --git a/tox.ini b/tox.ini index f1385fda..5fe0e753 100644 --- a/tox.ini +++ b/tox.ini @@ -1,23 +1,19 @@ [tox] -minversion = 3.2 -envlist = pep8,docs,py{39,310,311}-django42,py{310,311,312}-django{50,51},py313-django51 +minversion = 4.6 +envlist = pep8,docs,py{311,312,313,314}-django52,py{312,313,314}-django60 [testenv] skip_install = true deps = -r{toxinidir}/requirements-test.txt - django42: django~=4.2.0 - django42: djangorestframework~=3.15.0 - django42: django-filter~=24.3.0 - django50: django~=5.0.0 - django50: djangorestframework~=3.15.0 - django50: django-filter~=24.3.0 - django51: django~=5.1.0 - django51: djangorestframework~=3.15.0 - django51: django-filter~=24.3.0 + django52: django~=5.2.0 + django52: djangorestframework~=3.16.0 + django52: django-filter~=25.1.0 + django60: django~=6.0.0 + django60: djangorestframework~=3.16.0 + django60: django-filter~=25.2.0 setenv = DJANGO_SETTINGS_MODULE = patchwork.settings.dev - PYTHONDONTWRITEBYTECODE = 1 PYTHONDEVMODE = 1 PYTHONWARNINGS = once passenv = @@ -74,8 +70,7 @@ commands = [gh-actions] python = - 3.9: py39 - 3.10: py310 3.11: py311 3.12: py312 3.13: py313 + 3.14: py314 -- 2.47.3