diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5b95a9e6e..a3c3b1d1d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,17 +11,15 @@ jobs: strategy: fail-fast: false matrix: - python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13-dev'] - django-version: ['4.2', '5.0', 'main'] + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] + django-version: ['4.2', '5.0', '5.1', 'main'] exclude: - # Exclude py3.8 and py3.9 for Django main and 5.0 - - python-version: '3.8' - django-version: '5.0' + # Exclude py3.9 for Django main and 5+ - python-version: '3.9' django-version: '5.0' - - python-version: '3.8' - django-version: 'main' + - python-version: '3.9' + django-version: '5.1' - python-version: '3.9' django-version: 'main' @@ -101,7 +99,7 @@ jobs: - name: Set up newest stable Python version uses: actions/setup-python@v5 with: - python-version: 3.11 + python-version: 3.13 cache: 'pip' # Invalidate the cache when this file updates, as the dependencies' versions # are pinned in the step below @@ -113,7 +111,7 @@ jobs: # Install this project in editable mode, so that its package metadata can be queried pip install -e . # Install the latest minor version of Django we support - pip install Django==5.0 + pip install Django==5.1 - name: Check translation files are updated run: python -m simple_history.tests.generated_file_checks.check_translations diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0e5536233..0af416b92 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,13 +1,13 @@ --- repos: - repo: https://github.com/PyCQA/bandit - rev: 1.7.9 + rev: 1.7.10 hooks: - id: bandit exclude: /.*tests/ - repo: https://github.com/psf/black-pre-commit-mirror - rev: 24.8.0 + rev: 24.10.0 hooks: - id: black language_version: python3.9 @@ -25,7 +25,7 @@ repos: - id: isort - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: - id: requirements-txt-fixer files: requirements/.*\.txt$ @@ -40,11 +40,11 @@ repos: - id: detect-private-key - repo: https://github.com/tox-dev/pyproject-fmt - rev: 2.2.3 + rev: v2.5.0 hooks: - id: pyproject-fmt - repo: https://github.com/abravalheri/validate-pyproject - rev: v0.19 + rev: v0.23 hooks: - id: validate-pyproject @@ -56,7 +56,7 @@ repos: - "--strict" - repo: https://github.com/asottile/pyupgrade - rev: v3.17.0 + rev: v3.19.0 hooks: - id: pyupgrade - args: [--py38-plus] + args: [--py39-plus] diff --git a/CHANGES.rst b/CHANGES.rst index 58ed7cfa9..6a01c92e7 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -10,6 +10,8 @@ Unreleased - Fixed issue with deferred fields causing DoesNotExist error (gh-678) - Added HistoricOneToOneField (gh-1394) - Updated all djangoproject.com links to reference the stable version (gh-1420) +- Dropped support for Python 3.8, which reached end-of-life on 2024-10-07 (gh-1421) +- Added support for Django 5.1 (gh-1388) 3.7.0 (2024-05-29) ------------------ diff --git a/README.rst b/README.rst index f054be6e8..6b044182c 100644 --- a/README.rst +++ b/README.rst @@ -45,9 +45,10 @@ This app supports the following combinations of Django and Python: ========== ======================== Django Python ========== ======================== -4.2 3.8, 3.9, 3.10, 3.11, 3.12, 3.13-dev -5.0 3.10, 3.11, 3.12, 3.13-dev -main 3.10, 3.11, 3.12, 3.13-dev +4.2 3.9, 3.10, 3.11, 3.12, 3.13 +5.0 3.10, 3.11, 3.12, 3.13 +5.1 3.10, 3.11, 3.12, 3.13 +main 3.10, 3.11, 3.12, 3.13 ========== ======================== Getting Help diff --git a/docs/index.rst b/docs/index.rst index 2276ba9f6..56ef8ea69 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -41,9 +41,10 @@ This app supports the following combinations of Django and Python: ========== ======================= Django Python ========== ======================= -4.2 3.8, 3.9, 3.10, 3.11, 3.12, 3.13-dev -5.0 3.10, 3.11, 3.12, 3.13-dev -main 3.10, 3.11, 3.12, 3.13-dev +4.2 3.9, 3.10, 3.11, 3.12, 3.13 +5.0 3.10, 3.11, 3.12, 3.13 +5.1 3.10, 3.11, 3.12, 3.13 +main 3.10, 3.11, 3.12, 3.13 ========== ======================= Contribute diff --git a/pyproject.toml b/pyproject.toml index 319078fce..f4c6ea811 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ maintainers = [ authors = [ { name = "Corey Bertram", email = "corey@qr7.com" }, ] -requires-python = ">=3.8" +requires-python = ">=3.9" classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Web Environment", @@ -26,13 +26,11 @@ classifiers = [ "License :: OSI Approved :: BSD License", "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", - # DEV: uncomment this when the `pyproject-fmt` pre-commit hook stops removing it - #"Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.13", ] dynamic = [ "readme", @@ -83,12 +81,12 @@ fragments = [ [tool.black] line-length = 88 target-version = [ - "py38", + "py39", ] [tool.isort] profile = "black" -py_version = "38" +py_version = "39" [tool.coverage.run] parallel = true diff --git a/simple_history/admin.py b/simple_history/admin.py index 6a55e2e1c..0c4b470c5 100644 --- a/simple_history/admin.py +++ b/simple_history/admin.py @@ -1,4 +1,5 @@ -from typing import Any, Sequence +from collections.abc import Sequence +from typing import Any from django import http from django.apps import apps as django_apps diff --git a/simple_history/models.py b/simple_history/models.py index 02b845784..b007efbfa 100644 --- a/simple_history/models.py +++ b/simple_history/models.py @@ -2,9 +2,10 @@ import importlib import uuid import warnings +from collections.abc import Iterable, Sequence from dataclasses import dataclass from functools import partial -from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Sequence, Type, Union +from typing import TYPE_CHECKING, Any, Union import django from django.apps import apps @@ -899,10 +900,14 @@ def related_manager_cls(self): class HistoricRelationModelManager(related_model._default_manager.__class__): def get_queryset(self): + cache_name = ( + # DEV: Remove this when support for Django 5.0 has been dropped + self.field.remote_field.get_cache_name() + if django.VERSION < (5, 1) + else self.field.remote_field.cache_name + ) try: - return self.instance._prefetched_objects_cache[ - self.field.remote_field.get_cache_name() - ] + return self.instance._prefetched_objects_cache[cache_name] except (AttributeError, KeyError): history = getattr( self.instance, SIMPLE_HISTORY_REVERSE_ATTR_NAME, None @@ -1088,7 +1093,7 @@ def _get_field_changes_for_diff( old_history: "HistoricalChanges", fields: Iterable[str], foreign_keys_are_objs: bool, - ) -> List["ModelChange"]: + ) -> list["ModelChange"]: """Helper method for ``diff_against()``.""" changes = [] @@ -1129,7 +1134,7 @@ def _get_m2m_field_changes_for_diff( old_history: "HistoricalChanges", m2m_fields: Iterable[str], foreign_keys_are_objs: bool, - ) -> List["ModelChange"]: + ) -> list["ModelChange"]: """Helper method for ``diff_against()``.""" changes = [] @@ -1198,7 +1203,7 @@ def get_value(obj, through_field): @dataclass(frozen=True) class DeletedObject: - model: Type[models.Model] + model: type[models.Model] pk: Any def __str__(self): @@ -1223,7 +1228,7 @@ def __str__(self): # The PK of the through model's related objects. # # - Any of the other possible values of a model field. -ModelChangeValue = Union[Any, DeletedObject, List[Dict[str, Union[Any, DeletedObject]]]] +ModelChangeValue = Union[Any, DeletedObject, list[dict[str, Union[Any, DeletedObject]]]] @dataclass(frozen=True) diff --git a/simple_history/template_utils.py b/simple_history/template_utils.py index ae02d02e9..722edf1b1 100644 --- a/simple_history/template_utils.py +++ b/simple_history/template_utils.py @@ -1,6 +1,6 @@ import dataclasses from os.path import commonprefix -from typing import Any, Dict, Final, List, Tuple, Type, Union +from typing import Any, Final, Union from django.db.models import ManyToManyField, Model from django.utils.html import conditional_escape @@ -40,7 +40,7 @@ class HistoricalRecordContextHelper: def __init__( self, - model: Type[Model], + model: type[Model], historical_record: HistoricalChanges, *, max_displayed_delta_change_chars=DEFAULT_MAX_DISPLAYED_DELTA_CHANGE_CHARS, @@ -50,7 +50,7 @@ def __init__( self.max_displayed_delta_change_chars = max_displayed_delta_change_chars - def context_for_delta_changes(self, delta: ModelDelta) -> List[Dict[str, Any]]: + def context_for_delta_changes(self, delta: ModelDelta) -> list[dict[str, Any]]: """ Return the template context for ``delta.changes``. By default, this is a list of dicts with the keys ``"field"``, @@ -119,7 +119,7 @@ def prepare_delta_change_value( def stringify_delta_change_values( self, change: ModelChange, old: Any, new: Any - ) -> Tuple[SafeString, SafeString]: + ) -> tuple[SafeString, SafeString]: """ Called by ``format_delta_change()`` after ``old`` and ``new`` have been prepared by ``prepare_delta_change_value()``. @@ -196,7 +196,7 @@ def __init__( ) assert self.min_diff_len >= 0 # nosec - def common_shorten_repr(self, *args: Any) -> Tuple[str, ...]: + def common_shorten_repr(self, *args: Any) -> tuple[str, ...]: """ Returns ``args`` with each element converted into a string representation. If any of the strings are longer than ``self.max_length``, they're all shortened diff --git a/simple_history/tests/tests/test_template_utils.py b/simple_history/tests/tests/test_template_utils.py index b094588f9..9a4e8b5c3 100644 --- a/simple_history/tests/tests/test_template_utils.py +++ b/simple_history/tests/tests/test_template_utils.py @@ -1,5 +1,4 @@ from datetime import datetime -from typing import Tuple from django.test import TestCase from django.utils.dateparse import parse_datetime @@ -225,7 +224,7 @@ def test_context_dict( ) def test__context_for_delta_changes__preserves_html_safe_strings(self): - def get_context_dict_old_and_new(old_value, new_value) -> Tuple[str, str]: + def get_context_dict_old_and_new(old_value, new_value) -> tuple[str, str]: # The field doesn't really matter, as long as it exists on the model # passed to `HistoricalRecordContextHelper` change = ModelChange("question", old_value, new_value) diff --git a/simple_history/tests/tests/utils.py b/simple_history/tests/tests/utils.py index ae6fe949c..16a7e6b7b 100644 --- a/simple_history/tests/tests/utils.py +++ b/simple_history/tests/tests/utils.py @@ -1,5 +1,4 @@ from enum import Enum -from typing import Type from django.conf import settings from django.db.models import Model @@ -15,7 +14,7 @@ class HistoricalTestCase(TestCase): - def assertRecordValues(self, record, klass: Type[Model], values_dict: dict): + def assertRecordValues(self, record, klass: type[Model], values_dict: dict): """ Fail if ``record`` doesn't contain the field values in ``values_dict``. ``record.history_object`` is also checked. diff --git a/tox.ini b/tox.ini index 6d845e47b..f088cf7f0 100644 --- a/tox.ini +++ b/tox.ini @@ -1,14 +1,12 @@ [tox] envlist = - py{38,39,310,311,312,313}-dj42-{sqlite3,postgres,mysql,mariadb}, - py{310,311,312,313}-dj50-{sqlite3,postgres,mysql,mariadb}, - py{310,311,312,313}-djmain-{sqlite3,postgres,mysql,mariadb}, + py{39,310,311,312,313}-dj42-{sqlite3,postgres,mysql,mariadb}, + py{310,311,312,313}-dj{50,51,main}-{sqlite3,postgres,mysql,mariadb}, docs, lint [gh-actions] python = - 3.8: py38 3.9: py39 3.10: py310 3.11: py311, docs, lint @@ -19,6 +17,7 @@ python = DJANGO = 4.2: dj42 5.0: dj50 + 5.1: dj51 main: djmain [flake8] @@ -32,6 +31,7 @@ deps = -rrequirements/test.txt dj42: Django>=4.2,<4.3 dj50: Django>=5.0,<5.1 + dj51: Django>=5.1,<5.2 djmain: https://github.com/django/django/tarball/main postgres: -rrequirements/postgres.txt mysql: -rrequirements/mysql.txt