Drop 'Python 3.9' support (#773)

* Drop Python 3.9 support, set minimum version to 3.10

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Update CHANGELOG.md

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Fix lint error

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Youngkwang Yang 2025-10-18 00:51:53 +09:00 committed by GitHub
parent bd03eb6199
commit d417f30142
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 43 additions and 44 deletions

View file

@ -18,7 +18,7 @@ jobs:
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v6 uses: actions/setup-python@v6
with: with:
python-version: '3.9' python-version: '3.10'
- name: Get pip cache dir - name: Get pip cache dir
id: pip-cache id: pip-cache

View file

@ -9,7 +9,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] python-version: ["3.10", "3.11", "3.12", "3.13"]
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
@ -35,7 +35,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] python-version: ["3.10", "3.11", "3.12", "3.13"]
services: services:
postgres: postgres:
image: postgres:15 image: postgres:15
@ -81,7 +81,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] python-version: ["3.10", "3.11", "3.12", "3.13"]
services: services:
mysql: mysql:
image: mysql:8.0 image: mysql:8.0

View file

@ -4,10 +4,10 @@ repos:
rev: 25.9.0 rev: 25.9.0
hooks: hooks:
- id: black - id: black
language_version: python3.9 language_version: python3.10
args: args:
- "--target-version" - "--target-version"
- "py39" - "py310"
- repo: https://github.com/PyCQA/flake8 - repo: https://github.com/PyCQA/flake8
rev: "7.3.0" rev: "7.3.0"
hooks: hooks:
@ -21,7 +21,7 @@ repos:
rev: v3.20.0 rev: v3.20.0
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: [--py39-plus] args: [--py310-plus]
- repo: https://github.com/adamchainz/django-upgrade - repo: https://github.com/adamchainz/django-upgrade
rev: 1.29.0 rev: 1.29.0
hooks: hooks:

View file

@ -2,6 +2,10 @@
## Next Release ## Next Release
#### Improvements
- Drop 'Python 3.9' support ([#773](https://github.com/jazzband/django-auditlog/pull/773))
## 3.3.0 (2025-09-18) ## 3.3.0 (2025-09-18)
#### Improvements #### Improvements

View file

@ -1,5 +1,4 @@
from contextvars import ContextVar from contextvars import ContextVar
from typing import Optional
from django.conf import settings from django.conf import settings
from django.http import HttpRequest 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) 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. A function to read the cid from a request.
If the header is not in the request, then we set it to `None`. 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) correlation_id.set(cid)
def _get_cid() -> Optional[str]: def _get_cid() -> str | None:
return correlation_id.get() 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` Calls the cid getter function based on `settings.AUDITLOG_CID_GETTER`

View file

@ -1,6 +1,6 @@
import json import json
from collections.abc import Callable
from datetime import timezone from datetime import timezone
from typing import Callable, Optional
from django.conf import settings from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
@ -131,7 +131,7 @@ def is_primitive(obj) -> bool:
return isinstance(obj, primitive_types) 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: Get the masking function to use based on the following priority:
1. Model-specific mask_callable if provided 1. Model-specific mask_callable if provided
@ -168,8 +168,8 @@ def mask_str(value: str) -> str:
def model_instance_diff( def model_instance_diff(
old: Optional[Model], old: Model | None,
new: Optional[Model], new: Model | None,
fields_to_check=None, fields_to_check=None,
use_json_for_changes=False, use_json_for_changes=False,
): ):

View file

@ -1,5 +1,3 @@
from typing import Optional
from django.conf import settings from django.conf import settings
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
@ -39,7 +37,7 @@ class AuditlogMiddleware:
return remote_addr return remote_addr
@staticmethod @staticmethod
def _get_remote_port(request) -> Optional[int]: def _get_remote_port(request) -> int | None:
remote_port = request.headers.get("X-Forwarded-Port", "") remote_port = request.headers.get("X-Forwarded-Port", "")
try: try:

View file

@ -1,9 +1,10 @@
import ast import ast
import contextlib import contextlib
import json import json
from collections.abc import Callable
from copy import deepcopy from copy import deepcopy
from datetime import timezone from datetime import timezone
from typing import Any, Callable, Union from typing import Any
from dateutil import parser from dateutil import parser
from dateutil.tz import gettz from dateutil.tz import gettz
@ -534,7 +535,7 @@ class LogEntry(models.Model):
return changes_display_dict return changes_display_dict
def _get_changes_display_for_fk_field( 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: ) -> str:
""" """
:return: A string representing a given FK value and the field to which it belongs :return: A string representing a given FK value and the field to which it belongs

View file

@ -1,7 +1,7 @@
import copy import copy
from collections import defaultdict from collections import defaultdict
from collections.abc import Collection, Iterable from collections.abc import Callable, Collection, Iterable
from typing import Any, Callable, Optional, Union from typing import Any
from django.apps import apps from django.apps import apps
from django.db.models import ManyToManyField, Model from django.db.models import ManyToManyField, Model
@ -38,7 +38,7 @@ class AuditlogModelRegistry:
delete: bool = True, delete: bool = True,
access: bool = True, access: bool = True,
m2m: 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 from auditlog.receivers import log_access, log_create, log_delete, log_update
@ -62,14 +62,14 @@ class AuditlogModelRegistry:
def register( def register(
self, self,
model: ModelBase = None, model: ModelBase = None,
include_fields: Optional[list[str]] = None, include_fields: list[str] | None = None,
exclude_fields: Optional[list[str]] = None, exclude_fields: list[str] | None = None,
mapping_fields: Optional[dict[str, str]] = None, mapping_fields: dict[str, str] | None = None,
mask_fields: Optional[list[str]] = None, mask_fields: list[str] | None = None,
mask_callable: Optional[str] = None, mask_callable: str | None = None,
m2m_fields: Optional[Collection[str]] = None, m2m_fields: Collection[str] | None = None,
serialize_data: bool = False, serialize_data: bool = False,
serialize_kwargs: Optional[dict[str, Any]] = None, serialize_kwargs: dict[str, Any] | None = None,
serialize_auditlog_fields_only: bool = False, serialize_auditlog_fields_only: bool = False,
): ):
""" """
@ -259,7 +259,7 @@ class AuditlogModelRegistry:
] ]
return exclude_models 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) models = copy.deepcopy(models)
for model in models: for model in models:
if isinstance(model, str): if isinstance(model, str):

View file

@ -11,10 +11,10 @@ The repository can be found at https://github.com/jazzband/django-auditlog/.
**Requirements** **Requirements**
- Python 3.9 or higher - Python 3.10 or higher
- Django 4.2, 5.0, 5.1, and 5.2 - 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. at https://github.com/jazzband/django-auditlog/actions.
Adding Auditlog to your Django application Adding Auditlog to your Django application

View file

@ -29,12 +29,11 @@ setup(
description="Audit log app for Django", description="Audit log app for Django",
long_description=long_description, long_description=long_description,
long_description_content_type="text/markdown", long_description_content_type="text/markdown",
python_requires=">=3.9", python_requires=">=3.10",
install_requires=["Django>=4.2", "python-dateutil>=2.7.0"], install_requires=["Django>=4.2", "python-dateutil>=2.7.0"],
zip_safe=False, zip_safe=False,
classifiers=[ classifiers=[
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.12",

16
tox.ini
View file

@ -1,13 +1,13 @@
[tox] [tox]
envlist = envlist =
{py39,py310,py311}-django42 {py310,py311}-django42
{py310,py311,py312}-django50 {py310,py311,py312}-django50
{py310,py311,py312,py313}-django51 {py310,py311,py312,py313}-django51
{py310,py311,py312,py313}-django52 {py310,py311,py312,py313}-django52
{py312,py313}-djangomain {py312,py313}-djangomain
py39-docs py310-docs
py39-lint py310-lint
py39-checkmigrations py310-checkmigrations
[testenv] [testenv]
setenv = setenv =
@ -42,19 +42,18 @@ basepython =
py312: python3.12 py312: python3.12
py311: python3.11 py311: python3.11
py310: python3.10 py310: python3.10
py39: python3.9
[testenv:py39-docs] [testenv:py310-docs]
changedir = docs/source changedir = docs/source
deps = -rdocs/requirements.txt deps = -rdocs/requirements.txt
commands = sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html commands = sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html
[testenv:py39-lint] [testenv:py310-lint]
deps = pre-commit deps = pre-commit
commands = commands =
pre-commit run --all-files pre-commit run --all-files
[testenv:py39-checkmigrations] [testenv:py310-checkmigrations]
description = Check for missing migrations description = Check for missing migrations
changedir = auditlog_tests changedir = auditlog_tests
deps = deps =
@ -73,7 +72,6 @@ commands =
[gh-actions] [gh-actions]
python = python =
3.9: py39
3.10: py310 3.10: py310
3.11: py311 3.11: py311
3.12: py312 3.12: py312