diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2246a57..6b76f23 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v6 with: - python-version: '3.9' + python-version: '3.10' - name: Get pip cache dir id: pip-cache diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b8a64fe..5bc3114 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,7 +9,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v5 @@ -35,7 +35,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.10", "3.11", "3.12", "3.13"] services: postgres: image: postgres:15 @@ -81,7 +81,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.10", "3.11", "3.12", "3.13"] services: mysql: image: mysql:8.0 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e57ca75..8b06b16 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,10 +4,10 @@ repos: rev: 25.9.0 hooks: - id: black - language_version: python3.9 + language_version: python3.10 args: - "--target-version" - - "py39" + - "py310" - repo: https://github.com/PyCQA/flake8 rev: "7.3.0" hooks: @@ -21,7 +21,7 @@ repos: rev: v3.20.0 hooks: - id: pyupgrade - args: [--py39-plus] + args: [--py310-plus] - repo: https://github.com/adamchainz/django-upgrade rev: 1.29.0 hooks: diff --git a/CHANGELOG.md b/CHANGELOG.md index cc735ec..c32e105 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Next Release +#### Improvements + +- Drop 'Python 3.9' support ([#773](https://github.com/jazzband/django-auditlog/pull/773)) + ## 3.3.0 (2025-09-18) #### Improvements diff --git a/auditlog/cid.py b/auditlog/cid.py index 8d2aa9f..3e2b78f 100644 --- a/auditlog/cid.py +++ b/auditlog/cid.py @@ -1,5 +1,4 @@ from contextvars import ContextVar -from typing import Optional from django.conf import settings from django.http import HttpRequest @@ -8,7 +7,7 @@ from django.utils.module_loading import import_string correlation_id = ContextVar("auditlog_correlation_id", default=None) -def set_cid(request: Optional[HttpRequest] = None) -> None: +def set_cid(request: HttpRequest | None = None) -> None: """ A function to read the cid from a request. If the header is not in the request, then we set it to `None`. @@ -40,11 +39,11 @@ def set_cid(request: Optional[HttpRequest] = None) -> None: correlation_id.set(cid) -def _get_cid() -> Optional[str]: +def _get_cid() -> str | None: return correlation_id.get() -def get_cid() -> Optional[str]: +def get_cid() -> str | None: """ Calls the cid getter function based on `settings.AUDITLOG_CID_GETTER` diff --git a/auditlog/diff.py b/auditlog/diff.py index fc98987..72d1a76 100644 --- a/auditlog/diff.py +++ b/auditlog/diff.py @@ -1,6 +1,6 @@ import json +from collections.abc import Callable from datetime import timezone -from typing import Callable, Optional from django.conf import settings from django.core.exceptions import ObjectDoesNotExist @@ -131,7 +131,7 @@ def is_primitive(obj) -> bool: return isinstance(obj, primitive_types) -def get_mask_function(mask_callable: Optional[str] = None) -> Callable[[str], str]: +def get_mask_function(mask_callable: str | None = None) -> Callable[[str], str]: """ Get the masking function to use based on the following priority: 1. Model-specific mask_callable if provided @@ -168,8 +168,8 @@ def mask_str(value: str) -> str: def model_instance_diff( - old: Optional[Model], - new: Optional[Model], + old: Model | None, + new: Model | None, fields_to_check=None, use_json_for_changes=False, ): diff --git a/auditlog/middleware.py b/auditlog/middleware.py index bd01da3..295448e 100644 --- a/auditlog/middleware.py +++ b/auditlog/middleware.py @@ -1,5 +1,3 @@ -from typing import Optional - from django.conf import settings from django.contrib.auth import get_user_model @@ -39,7 +37,7 @@ class AuditlogMiddleware: return remote_addr @staticmethod - def _get_remote_port(request) -> Optional[int]: + def _get_remote_port(request) -> int | None: remote_port = request.headers.get("X-Forwarded-Port", "") try: diff --git a/auditlog/models.py b/auditlog/models.py index 01b54a0..d73ecda 100644 --- a/auditlog/models.py +++ b/auditlog/models.py @@ -1,9 +1,10 @@ import ast import contextlib import json +from collections.abc import Callable from copy import deepcopy from datetime import timezone -from typing import Any, Callable, Union +from typing import Any from dateutil import parser from dateutil.tz import gettz @@ -534,7 +535,7 @@ class LogEntry(models.Model): return changes_display_dict def _get_changes_display_for_fk_field( - self, field: Union[models.ForeignKey, models.OneToOneField], value: Any + self, field: models.ForeignKey | models.OneToOneField, value: Any ) -> str: """ :return: A string representing a given FK value and the field to which it belongs diff --git a/auditlog/registry.py b/auditlog/registry.py index c8ca907..835acd1 100644 --- a/auditlog/registry.py +++ b/auditlog/registry.py @@ -1,7 +1,7 @@ import copy from collections import defaultdict -from collections.abc import Collection, Iterable -from typing import Any, Callable, Optional, Union +from collections.abc import Callable, Collection, Iterable +from typing import Any from django.apps import apps from django.db.models import ManyToManyField, Model @@ -38,7 +38,7 @@ class AuditlogModelRegistry: delete: bool = True, access: bool = True, m2m: bool = True, - custom: Optional[dict[ModelSignal, Callable]] = None, + custom: dict[ModelSignal, Callable] | None = None, ): from auditlog.receivers import log_access, log_create, log_delete, log_update @@ -62,14 +62,14 @@ class AuditlogModelRegistry: def register( self, model: ModelBase = None, - include_fields: Optional[list[str]] = None, - exclude_fields: Optional[list[str]] = None, - mapping_fields: Optional[dict[str, str]] = None, - mask_fields: Optional[list[str]] = None, - mask_callable: Optional[str] = None, - m2m_fields: Optional[Collection[str]] = None, + include_fields: list[str] | None = None, + exclude_fields: list[str] | None = None, + mapping_fields: dict[str, str] | None = None, + mask_fields: list[str] | None = None, + mask_callable: str | None = None, + m2m_fields: Collection[str] | None = None, serialize_data: bool = False, - serialize_kwargs: Optional[dict[str, Any]] = None, + serialize_kwargs: dict[str, Any] | None = None, serialize_auditlog_fields_only: bool = False, ): """ @@ -259,7 +259,7 @@ class AuditlogModelRegistry: ] return exclude_models - def _register_models(self, models: Iterable[Union[str, dict[str, Any]]]) -> None: + def _register_models(self, models: Iterable[str | dict[str, Any]]) -> None: models = copy.deepcopy(models) for model in models: if isinstance(model, str): diff --git a/docs/source/installation.rst b/docs/source/installation.rst index 2a2c3c2..ab4d468 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -11,10 +11,10 @@ The repository can be found at https://github.com/jazzband/django-auditlog/. **Requirements** -- Python 3.9 or higher +- Python 3.10 or higher - Django 4.2, 5.0, 5.1, and 5.2 -Auditlog is currently tested with Python 3.9+ and Django 4.2, 5.0, 5.1, and 5.2. The latest test report can be found +Auditlog is currently tested with Python 3.10+ and Django 4.2, 5.0, 5.1, and 5.2. The latest test report can be found at https://github.com/jazzband/django-auditlog/actions. Adding Auditlog to your Django application diff --git a/setup.py b/setup.py index 9147842..2f1cc5c 100644 --- a/setup.py +++ b/setup.py @@ -29,12 +29,11 @@ setup( description="Audit log app for Django", long_description=long_description, long_description_content_type="text/markdown", - python_requires=">=3.9", + python_requires=">=3.10", install_requires=["Django>=4.2", "python-dateutil>=2.7.0"], zip_safe=False, classifiers=[ "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", diff --git a/tox.ini b/tox.ini index e804268..a123535 100644 --- a/tox.ini +++ b/tox.ini @@ -1,13 +1,13 @@ [tox] envlist = - {py39,py310,py311}-django42 + {py310,py311}-django42 {py310,py311,py312}-django50 {py310,py311,py312,py313}-django51 {py310,py311,py312,py313}-django52 {py312,py313}-djangomain - py39-docs - py39-lint - py39-checkmigrations + py310-docs + py310-lint + py310-checkmigrations [testenv] setenv = @@ -42,19 +42,18 @@ basepython = py312: python3.12 py311: python3.11 py310: python3.10 - py39: python3.9 -[testenv:py39-docs] +[testenv:py310-docs] changedir = docs/source deps = -rdocs/requirements.txt commands = sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html -[testenv:py39-lint] +[testenv:py310-lint] deps = pre-commit commands = pre-commit run --all-files -[testenv:py39-checkmigrations] +[testenv:py310-checkmigrations] description = Check for missing migrations changedir = auditlog_tests deps = @@ -73,7 +72,6 @@ commands = [gh-actions] python = - 3.9: py39 3.10: py310 3.11: py311 3.12: py312