From 867aca6bb6cd6e6ddeaa772a2dbd1cf334a9584f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 13 Aug 2026 04:30:11 +0000 Subject: [PATCH 1/2] Bump ruff from 0.15.21 to 0.16.2 in /django Bumps [ruff](https://github.com/astral-sh/ruff) from 0.15.21 to 0.16.2. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.15.21...0.16.2) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.16.2 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- django/uv.lock | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/django/uv.lock b/django/uv.lock index 39f92269..ccfbfce6 100644 --- a/django/uv.lock +++ b/django/uv.lock @@ -993,27 +993,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.21" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/36/6f65aa9989acdec45d417192d8f4e7921931d8a6cf87ac74bce3eed98a8e/ruff-0.15.21.tar.gz", hash = "sha256:d0cfc841c572283c36548f82664a54ce6565567f1b0d5b4cf2caac693d8b7500", size = 4769401, upload-time = "2026-07-09T20:01:34.005Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/c6/ede15cac6839f3dbce52565c8f5164a8210e669c7bc4decb03e5bdf47d0d/ruff-0.15.21-py3-none-linux_armv6l.whl", hash = "sha256:63ea0e965e5d73c90e95b2434beeafc70820536717f561b32ab6e777cb9bdf5d", size = 10854342, upload-time = "2026-07-09T20:00:53.998Z" }, - { url = "https://files.pythonhosted.org/packages/28/9d/d825b07ee7ea9e2d61df92a860033c94e06e7300d50a1c2653aac27d24fe/ruff-0.15.21-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0f212c5d7d54c01bbfe6dcab02b724a39300f3e34ed7acbe995ccb320a2c58bd", size = 11139539, upload-time = "2026-07-09T20:00:57.809Z" }, - { url = "https://files.pythonhosted.org/packages/f5/de/3b107712e642f063c7a9e0887c427b22cb44097de5aab36c05f2e280670c/ruff-0.15.21-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e6312e41bc96791299614995ea3a977c5857c3b5662b1ecef6755b02b87cb646", size = 10595437, upload-time = "2026-07-09T20:01:00.006Z" }, - { url = "https://files.pythonhosted.org/packages/9a/6f/b4523cc90ba239ede441447a19d0c968846a3012e5a0b0c5b62831a3d5e3/ruff-0.15.21-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01d65b4831c6b2a4ba8ee6faa84049d44d982b7a706e622c4094c509e51673be", size = 10990053, upload-time = "2026-07-09T20:01:02.187Z" }, - { url = "https://files.pythonhosted.org/packages/92/cc/c6a9872a5375f0628875481cf2f66b13d7d865bf3ca2e57f91c7e762d976/ruff-0.15.21-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2c5a913a589120ce67933d5d05fd6ddbcc2481c6a054980ee767f7414c72b4fd", size = 10666096, upload-time = "2026-07-09T20:01:04.299Z" }, - { url = "https://files.pythonhosted.org/packages/ab/97/c621f7a17e097f1790fa3af6374138823b330b2d03fc38337945daca212c/ruff-0.15.21-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef04b681d02ad4dc9620f00f83ac5c22f652d0e9a9cfe431d219b16ad5ccc41", size = 11537011, upload-time = "2026-07-09T20:01:06.771Z" }, - { url = "https://files.pythonhosted.org/packages/ea/51/d928727e476e25ccc57c6f449ffd80241a651a973ad949d39cfb2a771d28/ruff-0.15.21-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16d090c0740916594157e75b80d666eab8e78083b39b3b0e1d698f4670a17b86", size = 12347101, upload-time = "2026-07-09T20:01:08.859Z" }, - { url = "https://files.pythonhosted.org/packages/1e/88/8cd62026802b16018ad06931d87997cf795ba2a6239ab659606c87d96bf0/ruff-0.15.21-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a10e74757dd65004d779b73e2f3c5210156d9980b41224d50d2ebcf1db51e67", size = 11572001, upload-time = "2026-07-09T20:01:11.092Z" }, - { url = "https://files.pythonhosted.org/packages/b2/97/f63084cf55444fc110e8cb985ebfcc592af47f597d44453d778cb81bc156/ruff-0.15.21-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bab0905d2f29e0d9fbc3c373ed23db0095edaa3f71f1f4f519ec15134d9e85c8", size = 11549239, upload-time = "2026-07-09T20:01:13.27Z" }, - { url = "https://files.pythonhosted.org/packages/9d/77/f107da4a2874b7715914b03f09ba9c54424de3ff8a1cc5d015d3ee2ce0ac/ruff-0.15.21-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:00eca240af5789fec6fe7df74c088cc1f9644ed83027113468efba7c92b94075", size = 11535340, upload-time = "2026-07-09T20:01:15.206Z" }, - { url = "https://files.pythonhosted.org/packages/d5/e9/601deb322d3303a7bf212b0100ead6f2ee3f6a044d89c30f2f92bf83c731/ruff-0.15.21-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:262ab31557a75141325e32d3357f3597645a7f084e732b6b054dde428ecd9341", size = 10964048, upload-time = "2026-07-09T20:01:17.723Z" }, - { url = "https://files.pythonhosted.org/packages/ea/2e/0f2176d1e99c15192caea19c8c3a0a955246b4cb4de795042eeb616345cd/ruff-0.15.21-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:659c4e7a4212f83306045ec7c5e5a356d16d9a6ef4ae0c7a4d872914fc655d9d", size = 10667055, upload-time = "2026-07-09T20:01:19.73Z" }, - { url = "https://files.pythonhosted.org/packages/48/60/abd74a02e0c4214f12a68becfd30af7165cfdcb0e661ecdc60bbb949c09a/ruff-0.15.21-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9e866eab611a5f959d36df2d10e446973a3610bc42b0c15b31dc27977d59c233", size = 11242043, upload-time = "2026-07-09T20:01:21.947Z" }, - { url = "https://files.pythonhosted.org/packages/b2/c6/583075d8ccabb4b229345edcaf1545eb3d8d6be90f686a479d7e94088bbf/ruff-0.15.21-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e89bc93c0d3803ba870b55c29671bad9dc6d94bb1eb181b056b52eb05b52854f", size = 11648064, upload-time = "2026-07-09T20:01:24.023Z" }, - { url = "https://files.pythonhosted.org/packages/3a/3c/37d0ecb729a7cc2d393ea7dce316fc585680f35d93b8d62139d7d0a3700c/ruff-0.15.21-py3-none-win32.whl", hash = "sha256:01f8d5be84823c172b389e123174f781f9daf86d6c58719d603f941932195cdd", size = 10896555, upload-time = "2026-07-09T20:01:26.941Z" }, - { url = "https://files.pythonhosted.org/packages/c0/b8/e43466b2a6067ce91e669068f6e28d6c719a920f014b070d5c8731725de3/ruff-0.15.21-py3-none-win_amd64.whl", hash = "sha256:d4b8d9a2f0f12b816b50447f6eccb9f4bb01a6b82c86b50fb3b5354b458dc6d3", size = 12038772, upload-time = "2026-07-09T20:01:29.497Z" }, - { url = "https://files.pythonhosted.org/packages/dd/75/e90ab9aeece218a9fc5a5bc3ec97d0ee6bb3c4ff95869463c1de58e29a1c/ruff-0.15.21-py3-none-win_arm64.whl", hash = "sha256:6e83115d4b9377c1cbc13abf0e051f069fab0ef815ea0504a8a008cee24dd0a8", size = 11375265, upload-time = "2026-07-09T20:01:31.772Z" }, +version = "0.16.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/73/e1/4508a569211b35599016e84ba65c1a992b7a4004b4b6c4bea02a851cba1b/ruff-0.16.2.tar.gz", hash = "sha256:c3d7828d12e8927a6fc65fe38e2c2541b9e762d360a1786d752cb1b8883b3c9c", size = 4885811, upload-time = "2026-08-07T13:31:01.432Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/57/db19951540f98859c956b50bdb4d31089b4d91e9f15e2968e7d5193806d5/ruff-0.16.2-py3-none-linux_armv6l.whl", hash = "sha256:3c8de4cf2181f01d57946d87d777aa52916976fc09942aed89938fab5e013318", size = 10847925, upload-time = "2026-08-07T13:30:14.468Z" }, + { url = "https://files.pythonhosted.org/packages/13/5a/995fe85a8470d3e391ac0f7fa8054bb454eaf33ee138196d6172ed1079c0/ruff-0.16.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9a48cc05c6fbc811ca81b5d7ba95375affea6582d1b8024e455e41afbbf55344", size = 11072662, upload-time = "2026-08-07T13:30:18.143Z" }, + { url = "https://files.pythonhosted.org/packages/32/53/370d767c61c71a971a4ace36703a7ecd8c393956349a7325d7fab2b56827/ruff-0.16.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a2c0d14fcbb26c91f0f867a6dc9bd71bbc30b1b6151829c884f23faeab2e5700", size = 10566771, upload-time = "2026-08-07T13:30:20.899Z" }, + { url = "https://files.pythonhosted.org/packages/85/d6/9d96948caf5a632be62d62202d5ec914d6856f204fd79eb036e5915e79ea/ruff-0.16.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:335c621622c4650330be50842561c6586ac6971bb8ab5407fe34dcc9efb16bbe", size = 10975825, upload-time = "2026-08-07T13:30:23.517Z" }, + { url = "https://files.pythonhosted.org/packages/3b/92/ea87129b3414acb0b5770563779c51804d37ac67675c7ba35447ddb14773/ruff-0.16.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20e66910f2c37cc753f9ef6580c914a621b80c4fa3549d3e3521e29d0f5bfc3f", size = 10649437, upload-time = "2026-08-07T13:30:26.097Z" }, + { url = "https://files.pythonhosted.org/packages/ac/43/f8f291dcd4af5bb7872b74fdfa41a7cd7c856ca1d4069670971cf1b9f5cb/ruff-0.16.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7e36fbfba65510548156902bcf1350a979a958ce0347ce0f90d73894036b39f", size = 11446761, upload-time = "2026-08-07T13:30:28.752Z" }, + { url = "https://files.pythonhosted.org/packages/71/4a/ef991fb2fcf516ab71f0808adcdd8da5e18c8cde447f4ceaf5f47a5132a5/ruff-0.16.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f0eab35f80df8f134aae5d1630e751901321d317cc8e50dc39e36fa3ed34cd12", size = 12336364, upload-time = "2026-08-07T13:30:31.468Z" }, + { url = "https://files.pythonhosted.org/packages/f3/24/f615e74f307e6ca0e56a482872477b856c70d530aa356abfb6dfe5ca8a80/ruff-0.16.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40ea8c0594feb894e89c8c61ab9c103d38b0ea72dfde6c594107147ca31b1140", size = 11630720, upload-time = "2026-08-07T13:30:34.426Z" }, + { url = "https://files.pythonhosted.org/packages/c5/d3/8ef50149e8412a77f7ab409efdef0e2b23803707a3863da4fc64cb23d459/ruff-0.16.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab3d62dde0b19facdd632008cc4827fc28ada7736c6bd35ab6f1050f0bfed53f", size = 11466130, upload-time = "2026-08-07T13:30:36.958Z" }, + { url = "https://files.pythonhosted.org/packages/dd/a7/a19334985c4dea8c381981fa252cd854c7ee52dc4b1686dc16f4a911c702/ruff-0.16.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e43e1f5b8388da9eca1b9e88328d47a5cec794633ccf6f7484ac2dd15eee92c0", size = 11523634, upload-time = "2026-08-07T13:30:39.822Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6c/96d192b0e742412ceda08c0a50f9669b253dde9fd6a60ea1a10c9fa79a63/ruff-0.16.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c24788a980581e1d7ea3a0cbe4344c4fbeb0a6a9b1f4713aa46bb104f8294690", size = 10949807, upload-time = "2026-08-07T13:30:42.745Z" }, + { url = "https://files.pythonhosted.org/packages/fa/51/e26599ceca11e79ee255c7df515995561edf87e9ca1893284e44d98f5a86/ruff-0.16.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:81806b08329130005dd4a8a8394a0c9da8c6f4cafb16ba438d2a2ee6a18bedf1", size = 10646891, upload-time = "2026-08-07T13:30:45.522Z" }, + { url = "https://files.pythonhosted.org/packages/68/01/800c4b1f97bc8d7c6029e06b1f20473a3cf1e13c4933d8f3342add83fc55/ruff-0.16.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:4ce4e02bad779bef557f541a1b31f20d6abeae1cc05ed1b1ac019d4ffd1044c8", size = 11162063, upload-time = "2026-08-07T13:30:48.131Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d0/1477ea50fc5a0d4b0b71d1d63d50770bdd794d90b43e37a7618e63ec9894/ruff-0.16.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e0422abdf70070255fc4073ce9dfc814cc03db577013761ddd09bc1e4a9a4fbd", size = 11556038, upload-time = "2026-08-07T13:30:50.686Z" }, + { url = "https://files.pythonhosted.org/packages/b8/76/a7776f32048d991e16d4fa8ff91790b877342d3596cc3ed04acdbf1aaedc/ruff-0.16.2-py3-none-win32.whl", hash = "sha256:bf3a63d78fb39f4bf5ac8ae52051c5520505301abe19ba4e204c453b3f09bb0b", size = 10872850, upload-time = "2026-08-07T13:30:53.471Z" }, + { url = "https://files.pythonhosted.org/packages/00/0d/929c800d920e61397d82a01b60bffc68da3052c17d31de59efaad2e4ed75/ruff-0.16.2-py3-none-win_amd64.whl", hash = "sha256:bcabe2f6d0fc7819f1431793005af4e4de7371927d037345bf941252b195b9fa", size = 12023338, upload-time = "2026-08-07T13:30:56.193Z" }, + { url = "https://files.pythonhosted.org/packages/5b/6c/93e26c22c5f78ff87363e07da49c84955affbeb1098bd1936bf3b3f293bf/ruff-0.16.2-py3-none-win_arm64.whl", hash = "sha256:d614e95cedf38a2053fd351c55b103ba30d017d61688fdbfd40ee0412852a99f", size = 11374065, upload-time = "2026-08-07T13:30:58.775Z" }, ] [[package]] From 66a24f592b903d9b13f3e76ea66587415a2304ca Mon Sep 17 00:00:00 2001 From: Samuel Monroe Date: Thu, 20 Aug 2026 10:25:49 +0200 Subject: [PATCH 2/2] chore(django): adopt ruff 0.16's default rule set MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ruff 0.16 widened the rules enabled when no `select` is configured, and also began formatting Python code blocks embedded in Markdown. This repo has never carried a ruff config, so the bump surfaced 167 lint errors and 6 unformatted docs files. Rather than pin the old defaults, adapt the code. Automatic fixes (138): - UP006/UP035/UP045: PEP 585/604 built-in generics — `List[x]` -> `list[x]`, `Optional[x]` -> `x | None`, and `Callable`/`Iterable`/`Mapping` moved from `typing` to `collections.abc`. - UP017: `datetime.timezone.utc` -> `datetime.UTC`. - I001: import-block ordering. - RUF022/RUF100: `__all__` ordering and unused noqa directives. Manual fixes (29): - RUF012: annotated genuinely-shared class attributes with `ClassVar` (serializer `Meta` options, migration `dependencies`/`operations`, the `audited_models` registry and its test stubs). DRF views are the exception — `APIView` declares `permission_classes` as an *instance* variable, so `ClassVar` there fails mypy with "Cannot override instance variable with class variable". Those use an immutable tuple instead, which satisfies RUF012 without fighting the base class. - SIM117/SIM102: collapsed nested `with` and `if` statements. - B017: `assertRaises(Exception)` -> `assertRaises(IntegrityError)` in the two one-to-one uniqueness tests; verified that is what is actually raised. - B018: dropped a no-op `WefaLogEntry.objects.all().__class__` statement. - C408: `dict(...)` -> literal. FURB162: redundant zero-offset tz replace. - DTZ005: docs `datetime.now()` -> `datetime.now(tz=UTC)`. - BLE001: the blind catch around `django.setup()` in docs/source/conf.py is deliberate (docs must build without Django), so it carries an explicit `noqa: BLE001` with the reason; RUF100 will flag it if it stops applying. Markdown reformatting normalises the doc snippets to double quotes, matching the codebase. Inline comments are preserved, including the `nside_wefa.common` app-ordering note. Note for future work: the migration files are Django-generated, so a newly generated migration will need the same `ClassVar` annotations (or a per-file ignore) to pass lint. Verified: ruff check, ruff format --check, mypy, bandit, pysentry, `makemigrations --check`, and 325 passing tests. Co-Authored-By: Claude Opus 5 (1M context) --- django/CONTRIBUTE.md | 42 ++++++------- django/README.md | 2 +- django/demo/urls.py | 2 +- django/docs/source/conf.py | 6 +- django/nside_wefa/audit/README.md | 10 +++- django/nside_wefa/audit/__init__.py | 10 ++-- django/nside_wefa/audit/admin.py | 12 ++-- django/nside_wefa/audit/api.py | 20 +++---- django/nside_wefa/audit/builtin/__init__.py | 3 +- django/nside_wefa/audit/builtin/locale.py | 9 ++- django/nside_wefa/audit/checks.py | 16 ++--- django/nside_wefa/audit/immutability.py | 3 +- .../management/commands/wefa_audit_export.py | 3 +- .../management/commands/wefa_audit_verify.py | 21 ++++--- .../audit/migrations/0001_initial.py | 6 +- django/nside_wefa/audit/registration.py | 10 ++-- django/nside_wefa/audit/serializers.py | 10 ++-- django/nside_wefa/audit/tests/test_api.py | 22 ++++--- django/nside_wefa/audit/tests/test_checks.py | 4 +- .../audit/tests/test_immutability.py | 2 +- django/nside_wefa/audit/tests/test_models.py | 20 +++---- .../audit/tests/test_registration_paths.py | 11 ++-- .../audit/tests/test_settings_translation.py | 4 +- django/nside_wefa/audit/views/__init__.py | 2 +- django/nside_wefa/audit/views/_filters.py | 5 +- .../audit/views/audit_events_view.py | 4 +- .../nside_wefa/audit/views/my_audit_view.py | 2 +- django/nside_wefa/authentication/README.md | 58 +++++++++--------- django/nside_wefa/authentication/checks.py | 3 +- .../authentication/tests/test_apps.py | 3 +- .../authentication/tests/test_checks.py | 29 ++++----- .../authentication/tests/test_urls.py | 17 ++++-- .../utils/test_settings_initialization.py | 1 + django/nside_wefa/authentication/urls.py | 2 +- .../utils/settings_initialization.py | 6 +- django/nside_wefa/common/settings.py | 4 +- django/nside_wefa/common/tests/test_checks.py | 25 ++++---- django/nside_wefa/legal_consent/README.md | 14 ++--- django/nside_wefa/legal_consent/checks.py | 5 +- .../legal_consent/migrations/0001_initial.py | 6 +- .../legal_consent/models/legal_consent.py | 4 +- .../nside_wefa/legal_consent/serializers.py | 10 ++-- .../test_legal_consent_migration.py | 3 +- .../tests/models/test_legal_consent.py | 21 +++---- .../test_legal_consent_serializer.py | 4 +- .../legal_consent/tests/test_checks.py | 59 ++++++++++--------- .../tests/views/test_gdpr_agreement_view.py | 6 +- .../tests/views/test_privacy_notice_view.py | 2 +- .../tests/views/test_terms_of_use_view.py | 2 +- django/nside_wefa/legal_consent/urls.py | 3 +- .../legal_consent/views/__init__.py | 4 +- .../legal_consent/views/legal_consent_view.py | 10 ++-- .../views/privacy_notice_view.py | 6 +- .../legal_consent/views/terms_of_use_view.py | 6 +- .../nside_wefa/legal_consent/views/utils.py | 3 +- django/nside_wefa/locale/README.md | 6 +- django/nside_wefa/locale/checks.py | 3 +- .../locale/migrations/0001_initial.py | 6 +- .../nside_wefa/locale/models/user_locale.py | 4 +- django/nside_wefa/locale/serializers.py | 8 +-- .../locale/tests/models/test_user_locale.py | 3 +- django/nside_wefa/locale/views/__init__.py | 4 +- .../locale/views/available_locales_view.py | 10 ++-- .../locale/views/user_locale_view.py | 10 ++-- django/nside_wefa/utils/checks.py | 39 ++++++------ 65 files changed, 349 insertions(+), 321 deletions(-) diff --git a/django/CONTRIBUTE.md b/django/CONTRIBUTE.md index 198a9556..7a5348d4 100644 --- a/django/CONTRIBUTE.md +++ b/django/CONTRIBUTE.md @@ -76,30 +76,30 @@ To test the library in a separate Django project: ```python # settings.py INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'rest_framework', - 'rest_framework.authtoken', # Required for Token authentication - 'rest_framework_simplejwt', # Required for JWT authentication - 'nside_wefa.common', # Must come before other nside_wefa apps - 'nside_wefa.authentication', # Add the Authentication app - 'nside_wefa.legal_consent', # Add the LegalConsent app + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "rest_framework", + "rest_framework.authtoken", # Required for Token authentication + "rest_framework_simplejwt", # Required for JWT authentication + "nside_wefa.common", # Must come before other nside_wefa apps + "nside_wefa.authentication", # Add the Authentication app + "nside_wefa.legal_consent", # Add the LegalConsent app ] - + # Configuration NSIDE_WEFA = { - 'APP_NAME': 'My App', - 'AUTHENTICATION': { - 'TYPES': ['TOKEN', 'JWT'], + "APP_NAME": "My App", + "AUTHENTICATION": { + "TYPES": ["TOKEN", "JWT"], + }, + "LEGAL_CONSENT": { + "VERSION": 1, + "EXPIRY_LIMIT": 365, }, - 'LEGAL_CONSENT': { - 'VERSION': 1, - 'EXPIRY_LIMIT': 365, - } } ``` @@ -214,6 +214,7 @@ from typing import Optional from django.contrib.auth.models import User from nside_wefa.legal_consent.models import LegalConsent + def create_user_agreement(user: User, version: Optional[int] = None) -> LegalConsent: """Create a legal consent for the given user.""" # Implementation here @@ -301,6 +302,7 @@ Create an `apps.py` file: ```python from django.apps import AppConfig + class YourLibraryConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" name = "nside_wefa.your_library" diff --git a/django/README.md b/django/README.md index 985f8b6e..3ff18982 100644 --- a/django/README.md +++ b/django/README.md @@ -71,7 +71,7 @@ Wraps `django-auditlog` to give every product an append-only audit store with fo "rest_framework", "rest_framework.authtoken", # For token auth "rest_framework_simplejwt", # For JWT auth - "auditlog", # Required by nside_wefa.audit + "auditlog", # Required by nside_wefa.audit "nside_wefa.common", "nside_wefa.authentication", "nside_wefa.legal_consent", diff --git a/django/demo/urls.py b/django/demo/urls.py index 217abb37..08686729 100644 --- a/django/demo/urls.py +++ b/django/demo/urls.py @@ -16,7 +16,7 @@ """ from django.contrib import admin -from django.urls import path, include +from django.urls import include, path urlpatterns = [ path("admin/", admin.site.urls), diff --git a/django/docs/source/conf.py b/django/docs/source/conf.py index 43bdece5..e32bd821 100644 --- a/django/docs/source/conf.py +++ b/django/docs/source/conf.py @@ -10,7 +10,7 @@ import os import sys -from datetime import datetime +from datetime import UTC, datetime from pathlib import Path # -- Path setup -------------------------------------------------------------- @@ -22,7 +22,7 @@ # -- Project information ----------------------------------------------------- project = "N-SIDE WeFa" organization = "N-SIDE" -current_year = datetime.now().year +current_year = datetime.now(tz=UTC).year copyright = f"{current_year}, {organization}" # -- Django setup ------------------------------------------------------------ @@ -32,7 +32,7 @@ import django # type: ignore django.setup() -except Exception as exc: # pragma: no cover - docs build environment only +except Exception as exc: # noqa: BLE001 - pragma: no cover; docs must build even if Django setup fails # Don't fail import-time; Sphinx will still build non-autodoc pages. # The CI sets DJANGO_SETTINGS_MODULE and has Django installed. print(f"[sphinx conf] Warning: Django setup failed: {exc}") diff --git a/django/nside_wefa/audit/README.md b/django/nside_wefa/audit/README.md index f9e89bf3..fc7e1004 100644 --- a/django/nside_wefa/audit/README.md +++ b/django/nside_wefa/audit/README.md @@ -76,11 +76,13 @@ The recommended primary path. Call at the bottom of `models.py`: from django.db import models from nside_wefa.audit import register + class Order(models.Model): status = models.CharField(max_length=32) - total = models.DecimalField(max_digits=10, decimal_places=2) + total = models.DecimalField(max_digits=10, decimal_places=2) secret_token = models.CharField(max_length=64) + register( Order, include_fields=["status", "total"], @@ -93,10 +95,11 @@ register( ```python from nside_wefa.audit import audited + @audited(include_fields=["status", "total"]) class Order(models.Model): status = models.CharField(max_length=32) - total = models.DecimalField(max_digits=10, decimal_places=2) + total = models.DecimalField(max_digits=10, decimal_places=2) ``` ### Path C — `AuditAppConfigMixin` @@ -107,10 +110,11 @@ Keep models clean by declaring registrations on your `AppConfig`: from django.apps import AppConfig from nside_wefa.audit import AuditAppConfigMixin + class OrdersConfig(AuditAppConfigMixin, AppConfig): name = "orders" audited_models = { - "Order": {"include_fields": ["status", "total"]}, + "Order": {"include_fields": ["status", "total"]}, "Customer": {"exclude_fields": ["last_login_ip"]}, } ``` diff --git a/django/nside_wefa/audit/__init__.py b/django/nside_wefa/audit/__init__.py index a6a4cc05..9a39ee83 100644 --- a/django/nside_wefa/audit/__init__.py +++ b/django/nside_wefa/audit/__init__.py @@ -39,12 +39,12 @@ def __getattr__(name: str): __all__ = [ - "register", - "audited", "AuditAppConfigMixin", + "AuditEventImmutableError", + "AuditWriteError", + "Outcome", + "audited", "log", + "register", "set_actor", - "Outcome", - "AuditWriteError", - "AuditEventImmutableError", ] diff --git a/django/nside_wefa/audit/admin.py b/django/nside_wefa/audit/admin.py index bd21beed..7c1c4bfe 100644 --- a/django/nside_wefa/audit/admin.py +++ b/django/nside_wefa/audit/admin.py @@ -6,7 +6,7 @@ audit events specifically are read-only. """ -from typing import Any, Optional +from typing import Any from auditlog.models import LogEntry from django.contrib import admin @@ -66,17 +66,17 @@ class LogEntryAdmin(admin.ModelAdmin): "serialized_data", ) - def has_add_permission(self, request: HttpRequest) -> bool: # noqa: D401 + def has_add_permission(self, request: HttpRequest) -> bool: return False def has_change_permission( - self, request: HttpRequest, obj: Optional[Any] = None - ) -> bool: # noqa: D401 + self, request: HttpRequest, obj: Any | None = None + ) -> bool: return False def has_delete_permission( - self, request: HttpRequest, obj: Optional[Any] = None - ) -> bool: # noqa: D401 + self, request: HttpRequest, obj: Any | None = None + ) -> bool: return False @admin.display(description="action") diff --git a/django/nside_wefa/audit/api.py b/django/nside_wefa/audit/api.py index 1728405f..24038666 100644 --- a/django/nside_wefa/audit/api.py +++ b/django/nside_wefa/audit/api.py @@ -19,7 +19,7 @@ import enum import logging -from typing import Any, Dict, Optional +from typing import Any from auditlog import get_logentry_model from auditlog.context import set_actor as _auditlog_set_actor @@ -63,11 +63,11 @@ def log( action: str, *, actor: Any = _UNSET, - target: Optional[models.Model] = None, - changes: Optional[Dict[str, Any]] = None, - metadata: Optional[Dict[str, Any]] = None, + target: models.Model | None = None, + changes: dict[str, Any] | None = None, + metadata: dict[str, Any] | None = None, outcome: Outcome = Outcome.SUCCESS, -) -> Optional[AbstractLogEntry]: +) -> AbstractLogEntry | None: """Write an audit event and return the resulting log entry. Returns an instance of whichever model auditlog has been told to use — @@ -98,7 +98,7 @@ def log( redacted_changes = _redact(changes, redact_fields) if changes else None redacted_metadata = _redact(metadata, redact_fields) if metadata else {} - additional_data: Dict[str, Any] = { + additional_data: dict[str, Any] = { "outcome": outcome.value, "action": action, } @@ -110,7 +110,7 @@ def log( # base, so it's safe to read off the active class. log_model = get_logentry_model() - create_kwargs: Dict[str, Any] = { + create_kwargs: dict[str, Any] = { "action": log_model.Action.UPDATE, "additional_data": additional_data, } @@ -149,7 +149,7 @@ def log( try: return log_model.objects.create(**create_kwargs) - except Exception as exc: # noqa: BLE001 — by design; see RAISE_ON_FAILURE + except Exception as exc: if raise_on_failure: raise AuditWriteError( f"Failed to write audit event {action!r}: {exc}" @@ -182,7 +182,7 @@ def _resolve_actor(actor: Any) -> Any: return actor -def _redact(payload: Dict[str, Any], redact_fields: list) -> Dict[str, Any]: +def _redact(payload: dict[str, Any], redact_fields: list) -> dict[str, Any]: """Return a shallow copy of ``payload`` with sensitive values masked. Keys are matched case-insensitively. Nested dicts are walked recursively @@ -192,7 +192,7 @@ def _redact(payload: Dict[str, Any], redact_fields: list) -> Dict[str, Any]: if not isinstance(payload, dict): return payload redact_lower = {f.lower() for f in redact_fields} - out: Dict[str, Any] = {} + out: dict[str, Any] = {} for key, value in payload.items(): if isinstance(key, str) and key.lower() in redact_lower: out[key] = "[REDACTED]" diff --git a/django/nside_wefa/audit/builtin/__init__.py b/django/nside_wefa/audit/builtin/__init__.py index 9cea352e..cfbd949a 100644 --- a/django/nside_wefa/audit/builtin/__init__.py +++ b/django/nside_wefa/audit/builtin/__init__.py @@ -13,7 +13,6 @@ import importlib import logging -from typing import Tuple from django.apps import apps as django_apps @@ -22,7 +21,7 @@ logger = logging.getLogger("nside_wefa.audit") # Source name → (Django app label that must be installed, importable submodule). -KNOWN_SOURCES: dict[str, Tuple[str, str]] = { +KNOWN_SOURCES: dict[str, tuple[str, str]] = { "auth": ("auth", "nside_wefa.audit.builtin.auth"), "legal_consent": ( "nside_wefa.legal_consent", diff --git a/django/nside_wefa/audit/builtin/locale.py b/django/nside_wefa/audit/builtin/locale.py index f3d01dc5..977f2e6f 100644 --- a/django/nside_wefa/audit/builtin/locale.py +++ b/django/nside_wefa/audit/builtin/locale.py @@ -40,11 +40,10 @@ def _snapshot_code(sender: Any, instance: Any, **kwargs: Any) -> None: def _on_locale_saved(sender: Any, instance: Any, created: bool, **kwargs: Any) -> None: - if created: - # New row with an empty code: not interesting. - if instance.code is None: - setattr(instance, _SNAPSHOT_ATTR, instance.code) - return + # New row with an empty code: not interesting. + if created and instance.code is None: + setattr(instance, _SNAPSHOT_ATTR, instance.code) + return previous = getattr(instance, _SNAPSHOT_ATTR, None) current = instance.code diff --git a/django/nside_wefa/audit/checks.py b/django/nside_wefa/audit/checks.py index 9be77621..3485a6c6 100644 --- a/django/nside_wefa/audit/checks.py +++ b/django/nside_wefa/audit/checks.py @@ -14,7 +14,7 @@ unknown source name is an error. """ -from typing import Any, List +from typing import Any from django.core.checks import Error, register @@ -34,21 +34,21 @@ @register() -def wefa_apps_dependencies_check(app_configs, **kwargs) -> List[Error]: +def wefa_apps_dependencies_check(app_configs, **kwargs) -> list[Error]: """Validate INSTALLED_APPS order. ``nside_wefa.common`` and ``auditlog`` are independent prerequisites of ``nside_wefa.audit`` — their relative order does not matter, but each must precede ``nside_wefa.audit``. """ - errors: List[Error] = [] + errors: list[Error] = [] errors.extend(check_apps_dependencies_order([CommonConfig.name, AuditConfig.name])) errors.extend(check_apps_dependencies_order(["auditlog", AuditConfig.name])) return errors @register() -def audit_settings_check(app_configs, **kwargs) -> List[Error]: +def audit_settings_check(app_configs, **kwargs) -> list[Error]: """Validate the ``NSIDE_WEFA.AUDIT`` settings section. ``NSIDE_WEFA.AUDIT`` itself is optional — a missing or empty section is @@ -93,16 +93,16 @@ def audit_settings_check(app_configs, **kwargs) -> List[Error]: ), } - errors: List[Error] = [] + errors: list[Error] = [] for key, validator in validators.items(): if key in section: errors.extend(validator(section[key])) return errors -def _validate_exclude_models(value: Any) -> List[Error]: +def _validate_exclude_models(value: Any) -> list[Error]: """``EXCLUDE_MODELS`` must be a list of resolvable ``"app.Model"`` labels.""" - errors: List[Error] = [] + errors: list[Error] = [] if not isinstance(value, list): return [ Error( @@ -120,7 +120,7 @@ def _validate_exclude_models(value: Any) -> List[Error]: def _validate_non_empty_string(setting_path: str): """Inline helper for plain non-empty string settings.""" - def _validator(value: Any) -> List[Error]: + def _validator(value: Any) -> list[Error]: if not isinstance(value, str) or not value: return [ Error( diff --git a/django/nside_wefa/audit/immutability.py b/django/nside_wefa/audit/immutability.py index 0b43e1b4..f835314c 100644 --- a/django/nside_wefa/audit/immutability.py +++ b/django/nside_wefa/audit/immutability.py @@ -11,8 +11,9 @@ DELETE`` at the database level; this is documented in the README. """ +from collections.abc import Iterator from contextlib import contextmanager -from typing import Any, Iterator +from typing import Any from django.db.models.signals import pre_delete, pre_save diff --git a/django/nside_wefa/audit/management/commands/wefa_audit_export.py b/django/nside_wefa/audit/management/commands/wefa_audit_export.py index ca485318..1514a27d 100644 --- a/django/nside_wefa/audit/management/commands/wefa_audit_export.py +++ b/django/nside_wefa/audit/management/commands/wefa_audit_export.py @@ -7,7 +7,8 @@ import csv import json -from typing import Any, Iterable +from collections.abc import Iterable +from typing import Any from auditlog import get_logentry_model from auditlog.models import AbstractLogEntry diff --git a/django/nside_wefa/audit/management/commands/wefa_audit_verify.py b/django/nside_wefa/audit/management/commands/wefa_audit_verify.py index e6314a53..4c236004 100644 --- a/django/nside_wefa/audit/management/commands/wefa_audit_verify.py +++ b/django/nside_wefa/audit/management/commands/wefa_audit_verify.py @@ -24,7 +24,7 @@ from django.core.management.base import BaseCommand, CommandError -from nside_wefa.audit.models import WefaLogEntry, ZERO_HASH, compute_event_hash +from nside_wefa.audit.models import ZERO_HASH, WefaLogEntry, compute_event_hash from nside_wefa.common.settings import get_section @@ -98,17 +98,16 @@ def handle(self, *args: Any, **options: Any) -> None: for entry in queryset.iterator(): is_anchor = checked == 0 and prev_hash is None - if is_anchor and options["strict_head"]: - if entry.prev_hash != ZERO_HASH: - self.stderr.write( - self.style.ERROR( - f"--strict-head: anchor row id={entry.id} has " - f"non-zero prev_hash ({entry.prev_hash!r}). " - f"The chain origin appears to have been purged " - f"or tampered with." - ) + if is_anchor and options["strict_head"] and entry.prev_hash != ZERO_HASH: + self.stderr.write( + self.style.ERROR( + f"--strict-head: anchor row id={entry.id} has " + f"non-zero prev_hash ({entry.prev_hash!r}). " + f"The chain origin appears to have been purged " + f"or tampered with." ) - sys.exit(1) + ) + sys.exit(1) # Chain-link check skipped on the anchor row — its predecessor is # either outside the window (--from) or has been purged. diff --git a/django/nside_wefa/audit/migrations/0001_initial.py b/django/nside_wefa/audit/migrations/0001_initial.py index 0aa3290f..754af942 100644 --- a/django/nside_wefa/audit/migrations/0001_initial.py +++ b/django/nside_wefa/audit/migrations/0001_initial.py @@ -1,5 +1,7 @@ # Generated by Django 6.0.4 on 2026-04-26 08:05 +from typing import ClassVar + import django.db.models.deletion import django.utils.timezone from django.conf import settings @@ -9,12 +11,12 @@ class Migration(migrations.Migration): initial = True - dependencies = [ + dependencies: ClassVar[list[tuple[str, str]]] = [ ("contenttypes", "0002_remove_content_type_name"), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] - operations = [ + operations: ClassVar[list[migrations.operations.base.Operation]] = [ migrations.CreateModel( name="WefaLogEntry", fields=[ diff --git a/django/nside_wefa/audit/registration.py b/django/nside_wefa/audit/registration.py index df42d8a9..4ef6a934 100644 --- a/django/nside_wefa/audit/registration.py +++ b/django/nside_wefa/audit/registration.py @@ -16,7 +16,7 @@ """ import logging -from typing import Any, Dict, Set, Type +from typing import Any, ClassVar from django.db import models @@ -24,10 +24,10 @@ # Set of model classes registered through one of the WeFa entry points. # Exposed for tests; not part of the public contract. -_wefa_registered: Set[Type[models.Model]] = set() +_wefa_registered: set[type[models.Model]] = set() -def register(model: Type[models.Model], **kwargs: Any) -> Type[models.Model]: +def register(model: type[models.Model], **kwargs: Any) -> type[models.Model]: """Register a model for auditing. Thin wrapper around :func:`auditlog.registry.auditlog.register` that @@ -55,7 +55,7 @@ class Order(models.Model): ... """ - def _decorator(cls: Type[models.Model]) -> Type[models.Model]: + def _decorator(cls: type[models.Model]) -> type[models.Model]: register(cls, **kwargs) return cls @@ -82,7 +82,7 @@ class ConsumerAppConfig(AuditAppConfigMixin, AppConfig): entry does not block the rest of the app from booting. """ - audited_models: Dict[str, Dict[str, Any]] = {} + audited_models: ClassVar[dict[str, dict[str, Any]]] = {} def ready(self) -> None: # type: ignore[override] # Cooperative super() call so the mixin composes with other mixins on diff --git a/django/nside_wefa/audit/serializers.py b/django/nside_wefa/audit/serializers.py index e5aeea03..a3943728 100644 --- a/django/nside_wefa/audit/serializers.py +++ b/django/nside_wefa/audit/serializers.py @@ -7,7 +7,7 @@ used only by the staff detail endpoint when tamper-evidence is enabled. """ -from typing import Any, Optional +from typing import Any, ClassVar from auditlog.models import LogEntry from drf_spectacular.utils import extend_schema_field, extend_schema_serializer @@ -43,7 +43,7 @@ class AuditEventSerializer(serializers.ModelSerializer): class Meta: model = LogEntry - fields = [ + fields: ClassVar[list[str]] = [ "id", "timestamp", "action", @@ -65,17 +65,17 @@ def get_action(self, obj: LogEntry) -> str: return (obj.additional_data or {}).get("action") or obj.get_action_display() @extend_schema_field(serializers.CharField(allow_null=True)) - def get_actor(self, obj: LogEntry) -> Optional[str]: + def get_actor(self, obj: LogEntry) -> str | None: if obj.actor is not None: return str(obj.actor) return obj.actor_email or None @extend_schema_field(serializers.CharField(allow_null=True)) - def get_target(self, obj: LogEntry) -> Optional[str]: + def get_target(self, obj: LogEntry) -> str | None: return obj.object_repr or None @extend_schema_field(serializers.CharField(allow_null=True)) - def get_target_type(self, obj: LogEntry) -> Optional[str]: + def get_target_type(self, obj: LogEntry) -> str | None: if obj.content_type_id is None or obj.content_type is None: return None return f"{obj.content_type.app_label}.{obj.content_type.model}" diff --git a/django/nside_wefa/audit/tests/test_api.py b/django/nside_wefa/audit/tests/test_api.py index 2bedb7c6..0a01b6ab 100644 --- a/django/nside_wefa/audit/tests/test_api.py +++ b/django/nside_wefa/audit/tests/test_api.py @@ -8,7 +8,7 @@ from django.test import TestCase, override_settings from nside_wefa import audit -from nside_wefa.audit.api import _redact, AuditWriteError, Outcome +from nside_wefa.audit.api import AuditWriteError, Outcome, _redact class LogTargetResolutionTest(TestCase): @@ -110,11 +110,13 @@ def setUp(self): self.user = User.objects.create_user(username="u") def test_default_returns_none_and_warns(self): - with mock.patch.object( - LogEntry.objects, "create", side_effect=RuntimeError("boom") + with ( + mock.patch.object( + LogEntry.objects, "create", side_effect=RuntimeError("boom") + ), + self.assertLogs("nside_wefa.audit", level="WARNING") as logs, ): - with self.assertLogs("nside_wefa.audit", level="WARNING") as logs: - result = audit.log("demo.fail", actor=self.user) + result = audit.log("demo.fail", actor=self.user) self.assertIsNone(result) self.assertTrue(any("failed to persist" in msg for msg in logs.output)) @@ -125,11 +127,13 @@ def test_default_returns_none_and_warns(self): } ) def test_raise_on_failure_reraises_as_audit_write_error(self): - with mock.patch.object( - LogEntry.objects, "create", side_effect=RuntimeError("boom") + with ( + mock.patch.object( + LogEntry.objects, "create", side_effect=RuntimeError("boom") + ), + self.assertRaises(AuditWriteError), ): - with self.assertRaises(AuditWriteError): - audit.log("demo.fail", actor=self.user) + audit.log("demo.fail", actor=self.user) class SetActorReExportTest(TestCase): diff --git a/django/nside_wefa/audit/tests/test_checks.py b/django/nside_wefa/audit/tests/test_checks.py index c46cc7bb..63d3f02c 100644 --- a/django/nside_wefa/audit/tests/test_checks.py +++ b/django/nside_wefa/audit/tests/test_checks.py @@ -1,6 +1,6 @@ """Tests for the audit system checks.""" -from typing import Any, List +from typing import Any from django.core.checks import Error from django.test import TestCase, override_settings @@ -72,7 +72,7 @@ def test_auditlog_must_be_present(self): class AuditSettingsCheckTest(TestCase): """Validates every key under ``NSIDE_WEFA.AUDIT``.""" - def _run(self, audit_section: Any) -> List[Error]: + def _run(self, audit_section: Any) -> list[Error]: with override_settings(NSIDE_WEFA={"APP_NAME": "T", "AUDIT": audit_section}): return audit_settings_check(None) diff --git a/django/nside_wefa/audit/tests/test_immutability.py b/django/nside_wefa/audit/tests/test_immutability.py index 64c9c718..051771d8 100644 --- a/django/nside_wefa/audit/tests/test_immutability.py +++ b/django/nside_wefa/audit/tests/test_immutability.py @@ -6,7 +6,7 @@ from django.test import TestCase from nside_wefa import audit -from nside_wefa.audit.immutability import allow_purge, AuditEventImmutableError +from nside_wefa.audit.immutability import AuditEventImmutableError, allow_purge class LogEntryImmutabilityTest(TestCase): diff --git a/django/nside_wefa/audit/tests/test_models.py b/django/nside_wefa/audit/tests/test_models.py index e5930857..41ff38fc 100644 --- a/django/nside_wefa/audit/tests/test_models.py +++ b/django/nside_wefa/audit/tests/test_models.py @@ -1,34 +1,32 @@ """Tests for the tamper-evident WefaLogEntry hash chain.""" +from auditlog.models import LogEntry from django.contrib.contenttypes.models import ContentType from django.test import TestCase -from auditlog.models import LogEntry - from nside_wefa.audit.models import ( HASH_LENGTH, - WefaLogEntry, ZERO_HASH, + WefaLogEntry, compute_event_hash, ) def _make_entry(**overrides): """Helper that creates a minimal WefaLogEntry.""" - kwargs = dict( - action=LogEntry.Action.UPDATE, - content_type=ContentType.objects.get_for_model(LogEntry), - object_pk="0", - object_repr="test", - additional_data={}, - ) + kwargs = { + "action": LogEntry.Action.UPDATE, + "content_type": ContentType.objects.get_for_model(LogEntry), + "object_pk": "0", + "object_repr": "test", + "additional_data": {}, + } kwargs.update(overrides) return WefaLogEntry.objects.create(**kwargs) class HashChainTest(TestCase): def test_first_event_uses_zero_prev_hash(self): - WefaLogEntry.objects.all().__class__ # touch the model entry = _make_entry(additional_data={"action": "first"}) self.assertEqual(entry.prev_hash, ZERO_HASH) self.assertEqual(len(entry.hash), HASH_LENGTH) diff --git a/django/nside_wefa/audit/tests/test_registration_paths.py b/django/nside_wefa/audit/tests/test_registration_paths.py index 6905457e..63830edd 100644 --- a/django/nside_wefa/audit/tests/test_registration_paths.py +++ b/django/nside_wefa/audit/tests/test_registration_paths.py @@ -5,11 +5,12 @@ under Path D — using a different model keeps the registries from interfering. """ -from django.contrib.auth.models import Group, Permission, User -from django.test import TestCase +from typing import Any, ClassVar from auditlog.models import LogEntry from auditlog.registry import auditlog as _auditlog_registry +from django.contrib.auth.models import Group, Permission, User +from django.test import TestCase from nside_wefa import audit from nside_wefa.audit.registration import ( @@ -83,7 +84,9 @@ def test_mixin_walks_audited_models_and_registers(self): class _StubAppConfig(AuditAppConfigMixin): label = "auth" - audited_models = {"Permission": {"include_fields": ["name"]}} + audited_models: ClassVar[dict[str, dict[str, Any]]] = { + "Permission": {"include_fields": ["name"]} + } def get_model(self, name: str): seen.append(name) @@ -100,7 +103,7 @@ def get_model(self, name: str): def test_mixin_skips_unresolvable_models(self): class _StubAppConfig(AuditAppConfigMixin): label = "auth" - audited_models = {"NoSuchModel": {}} + audited_models: ClassVar[dict[str, dict[str, Any]]] = {"NoSuchModel": {}} def get_model(self, name: str): raise LookupError(name) diff --git a/django/nside_wefa/audit/tests/test_settings_translation.py b/django/nside_wefa/audit/tests/test_settings_translation.py index eaf7209c..598410a9 100644 --- a/django/nside_wefa/audit/tests/test_settings_translation.py +++ b/django/nside_wefa/audit/tests/test_settings_translation.py @@ -1,5 +1,7 @@ """Tests for the NSIDE_WEFA.AUDIT → AUDITLOG_* translation layer.""" +from typing import ClassVar + from django.test import TestCase, override_settings from nside_wefa.audit.settings_translation import ( @@ -17,7 +19,7 @@ class _RestoreAuditlogSettings: exit so subsequent tests aren't affected. """ - AUDITLOG_KEYS = [ + AUDITLOG_KEYS: ClassVar[list[str]] = [ "AUDITLOG_INCLUDE_TRACKING_MODELS", "AUDITLOG_INCLUDE_ALL_MODELS", "AUDITLOG_EXCLUDE_TRACKING_MODELS", diff --git a/django/nside_wefa/audit/views/__init__.py b/django/nside_wefa/audit/views/__init__.py index 841b4711..48730af1 100644 --- a/django/nside_wefa/audit/views/__init__.py +++ b/django/nside_wefa/audit/views/__init__.py @@ -4,7 +4,7 @@ from .my_audit_view import MyAuditEventListView __all__ = [ - "AuditEventListView", "AuditEventDetailView", + "AuditEventListView", "MyAuditEventListView", ] diff --git a/django/nside_wefa/audit/views/_filters.py b/django/nside_wefa/audit/views/_filters.py index 1dc302a0..c9c5ae17 100644 --- a/django/nside_wefa/audit/views/_filters.py +++ b/django/nside_wefa/audit/views/_filters.py @@ -6,7 +6,8 @@ shared filterset. """ -from typing import Any, Iterable, Optional, Tuple +from collections.abc import Iterable +from typing import Any from django.contrib.contenttypes.models import ContentType from django.db.models import QuerySet @@ -15,7 +16,7 @@ def apply_filters( queryset: QuerySet, query_params: Any, allowed: Iterable[str] -) -> Tuple[QuerySet, Optional[str]]: +) -> tuple[QuerySet, str | None]: """Apply audit-event filters to ``queryset``. Returns ``(queryset, error_message)``. ``error_message`` is non-None when diff --git a/django/nside_wefa/audit/views/audit_events_view.py b/django/nside_wefa/audit/views/audit_events_view.py index 851a2636..5b12279e 100644 --- a/django/nside_wefa/audit/views/audit_events_view.py +++ b/django/nside_wefa/audit/views/audit_events_view.py @@ -50,7 +50,7 @@ class AuditEventListView(APIView): Supports the filters listed in :data:`LIST_FILTERS`. """ - permission_classes = [IsAdminUser] + permission_classes = (IsAdminUser,) @extend_schema( operation_id="audit_events_list", @@ -85,7 +85,7 @@ def get(self, request: Request) -> Response: class AuditEventDetailView(APIView): """``GET /audit/events//`` — single audit event (staff only).""" - permission_classes = [IsAdminUser] + permission_classes = (IsAdminUser,) @extend_schema( operation_id="audit_events_detail", diff --git a/django/nside_wefa/audit/views/my_audit_view.py b/django/nside_wefa/audit/views/my_audit_view.py index 76ce8357..87361ff1 100644 --- a/django/nside_wefa/audit/views/my_audit_view.py +++ b/django/nside_wefa/audit/views/my_audit_view.py @@ -26,7 +26,7 @@ class MyAuditEventListView(APIView): """List audit events the authenticated user is the actor of.""" - permission_classes = [IsAuthenticated] + permission_classes = (IsAuthenticated,) @extend_schema( operation_id="audit_me_list", diff --git a/django/nside_wefa/authentication/README.md b/django/nside_wefa/authentication/README.md index 73c2d89b..40981a8f 100644 --- a/django/nside_wefa/authentication/README.md +++ b/django/nside_wefa/authentication/README.md @@ -22,11 +22,11 @@ The Authentication app simplifies Django REST Framework authentication setup by ```python INSTALLED_APPS = [ # ... other apps - 'rest_framework', - 'rest_framework.authtoken', # Required for Token authentication - 'rest_framework_simplejwt', # Required for JWT authentication - 'nside_wefa.common', # Must come before authentication - 'nside_wefa.authentication', + "rest_framework", + "rest_framework.authtoken", # Required for Token authentication + "rest_framework_simplejwt", # Required for JWT authentication + "nside_wefa.common", # Must come before authentication + "nside_wefa.authentication", ] ``` @@ -43,7 +43,7 @@ from django.urls import path, include urlpatterns = [ # ... other URL patterns - path('auth/', include('nside_wefa.authentication.urls')), + path("auth/", include("nside_wefa.authentication.urls")), # ... other URL patterns ] ``` @@ -127,13 +127,13 @@ The app updates `settings.REST_FRAMEWORK` with: ```python REST_FRAMEWORK = { - 'DEFAULT_AUTHENTICATION_CLASSES': [ + "DEFAULT_AUTHENTICATION_CLASSES": [ # Dynamically added based on AUTHENTICATION.TYPES: # 'rest_framework.authentication.TokenAuthentication', # if "TOKEN" enabled # 'rest_framework_simplejwt.authentication.JWTAuthentication', # if "JWT" enabled ], - 'DEFAULT_PERMISSION_CLASSES': [ - 'rest_framework.permissions.IsAuthenticated', + "DEFAULT_PERMISSION_CLASSES": [ + "rest_framework.permissions.IsAuthenticated", ], } ``` @@ -162,7 +162,7 @@ urlpatterns += [ path("token-auth/", obtain_auth_token, name="api-auth"), ] -# If JWT authentication is enabled +# If JWT authentication is enabled urlpatterns += [ path("token/", TokenObtainPairView.as_view(), name="token_obtain_pair"), path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"), @@ -177,17 +177,17 @@ urlpatterns += [ ```python import requests -response = requests.post('http://your-domain/auth/token-auth/', { - 'username': 'your_username', - 'password': 'your_password' -}) -token = response.json()['token'] +response = requests.post( + "http://your-domain/auth/token-auth/", + {"username": "your_username", "password": "your_password"}, +) +token = response.json()["token"] ``` 2. **Use the token in API requests:** ```python -headers = {'Authorization': f'Token {token}'} -response = requests.get('http://your-domain/api/protected-endpoint/', headers=headers) +headers = {"Authorization": f"Token {token}"} +response = requests.get("http://your-domain/api/protected-endpoint/", headers=headers) ``` ### Using JWT Authentication @@ -196,27 +196,27 @@ response = requests.get('http://your-domain/api/protected-endpoint/', headers=he ```python import requests -response = requests.post('http://your-domain/auth/token/', { - 'username': 'your_username', - 'password': 'your_password' -}) +response = requests.post( + "http://your-domain/auth/token/", + {"username": "your_username", "password": "your_password"}, +) tokens = response.json() -access_token = tokens['access'] -refresh_token = tokens['refresh'] +access_token = tokens["access"] +refresh_token = tokens["refresh"] ``` 2. **Use the access token:** ```python -headers = {'Authorization': f'Bearer {access_token}'} -response = requests.get('http://your-domain/api/protected-endpoint/', headers=headers) +headers = {"Authorization": f"Bearer {access_token}"} +response = requests.get("http://your-domain/api/protected-endpoint/", headers=headers) ``` 3. **Refresh the token:** ```python -refresh_response = requests.post('http://your-domain/auth/token/refresh/', { - 'refresh': refresh_token -}) -new_access_token = refresh_response.json()['access'] +refresh_response = requests.post( + "http://your-domain/auth/token/refresh/", {"refresh": refresh_token} +) +new_access_token = refresh_response.json()["access"] ``` ## Testing diff --git a/django/nside_wefa/authentication/checks.py b/django/nside_wefa/authentication/checks.py index 960f5772..726d0248 100644 --- a/django/nside_wefa/authentication/checks.py +++ b/django/nside_wefa/authentication/checks.py @@ -17,10 +17,11 @@ """ from typing import Any + from django.core.checks import Error, register -from nside_wefa.authentication.constants import AUTHENTICATION_TYPES from nside_wefa.authentication.apps import AuthenticationConfig +from nside_wefa.authentication.constants import AUTHENTICATION_TYPES from nside_wefa.common.apps import CommonConfig from nside_wefa.utils.checks import ( check_apps_dependencies_order, diff --git a/django/nside_wefa/authentication/tests/test_apps.py b/django/nside_wefa/authentication/tests/test_apps.py index b6975a05..3b812278 100644 --- a/django/nside_wefa/authentication/tests/test_apps.py +++ b/django/nside_wefa/authentication/tests/test_apps.py @@ -1,6 +1,7 @@ from unittest.mock import patch -from django.test import TestCase + from django.apps import apps +from django.test import TestCase from nside_wefa.authentication.apps import AuthenticationConfig diff --git a/django/nside_wefa/authentication/tests/test_checks.py b/django/nside_wefa/authentication/tests/test_checks.py index e7f1bfba..874cf99f 100644 --- a/django/nside_wefa/authentication/tests/test_checks.py +++ b/django/nside_wefa/authentication/tests/test_checks.py @@ -1,10 +1,11 @@ from unittest.mock import patch + from django.core.checks import Error from django.test import TestCase, override_settings from nside_wefa.authentication.checks import ( - wefa_apps_dependencies_check, authentication_settings_check, + wefa_apps_dependencies_check, ) @@ -82,19 +83,19 @@ def test_authentication_settings_check_missing_nside_wefa(self): """Test that missing NSIDE_WEFA setting raises an error.""" from django.conf import settings - with patch.object(settings, "NSIDE_WEFA", None): - with patch("django.conf.settings.NSIDE_WEFA", None, create=True): - with patch( - "nside_wefa.authentication.checks.getattr", return_value=None - ): - errors = authentication_settings_check(None) - - self.assertEqual(len(errors), 1) - self.assertIsInstance(errors[0], Error) - self.assertEqual( - errors[0].msg, - "NSIDE_WEFA.AUTHENTICATION is not defined in settings.py", - ) + with ( + patch.object(settings, "NSIDE_WEFA", None), + patch("django.conf.settings.NSIDE_WEFA", None, create=True), + patch("nside_wefa.authentication.checks.getattr", return_value=None), + ): + errors = authentication_settings_check(None) + + self.assertEqual(len(errors), 1) + self.assertIsInstance(errors[0], Error) + self.assertEqual( + errors[0].msg, + "NSIDE_WEFA.AUTHENTICATION is not defined in settings.py", + ) def test_authentication_settings_check_missing_authentication_key(self): """Test that missing AUTHENTICATION key in NSIDE_WEFA raises an error.""" diff --git a/django/nside_wefa/authentication/tests/test_urls.py b/django/nside_wefa/authentication/tests/test_urls.py index a60e3ca3..1d528057 100644 --- a/django/nside_wefa/authentication/tests/test_urls.py +++ b/django/nside_wefa/authentication/tests/test_urls.py @@ -14,7 +14,8 @@ def test_url_generation_logic_with_token_authentication(self): """Test the logic that generates URLs based on authentication types.""" from django.urls import path from rest_framework.authtoken.views import obtain_auth_token - from nside_wefa.authentication.constants import AUTH_TYPE_TOKEN, AUTH_TYPE_JWT + + from nside_wefa.authentication.constants import AUTH_TYPE_JWT, AUTH_TYPE_TOKEN # Simulate the URL generation logic authentication_types = ["TOKEN"] @@ -59,7 +60,8 @@ def test_url_generation_logic_with_jwt_authentication(self): from django.urls import path from rest_framework.authtoken.views import obtain_auth_token from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView - from nside_wefa.authentication.constants import AUTH_TYPE_TOKEN, AUTH_TYPE_JWT + + from nside_wefa.authentication.constants import AUTH_TYPE_JWT, AUTH_TYPE_TOKEN # Simulate the URL generation logic authentication_types = ["JWT"] @@ -99,7 +101,8 @@ def test_url_generation_logic_with_both_authentication_types(self): from django.urls import path from rest_framework.authtoken.views import obtain_auth_token from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView - from nside_wefa.authentication.constants import AUTH_TYPE_TOKEN, AUTH_TYPE_JWT + + from nside_wefa.authentication.constants import AUTH_TYPE_JWT, AUTH_TYPE_TOKEN # Simulate the URL generation logic authentication_types = ["TOKEN", "JWT"] @@ -137,7 +140,8 @@ def test_url_generation_logic_with_both_authentication_types(self): def test_url_generation_logic_with_no_authentication_types(self): """Test the logic when no authentication types are specified.""" from django.urls import path - from nside_wefa.authentication.constants import AUTH_TYPE_TOKEN, AUTH_TYPE_JWT + + from nside_wefa.authentication.constants import AUTH_TYPE_JWT, AUTH_TYPE_TOKEN # Simulate the URL generation logic authentication_types = [] @@ -179,7 +183,8 @@ def test_url_generation_logic_with_no_authentication_types(self): def test_url_generation_logic_with_unknown_authentication_type(self): """Test the logic when unknown authentication types are specified.""" from django.urls import path - from nside_wefa.authentication.constants import AUTH_TYPE_TOKEN, AUTH_TYPE_JWT + + from nside_wefa.authentication.constants import AUTH_TYPE_JWT, AUTH_TYPE_TOKEN # Simulate the URL generation logic authentication_types = ["UNKNOWN_TYPE"] @@ -224,7 +229,7 @@ class AuthenticationUrlsIntegrationTest(TestCase): def test_constants_are_properly_imported(self): """Test that authentication constants are properly imported in urls module.""" - from nside_wefa.authentication.constants import AUTH_TYPE_TOKEN, AUTH_TYPE_JWT + from nside_wefa.authentication.constants import AUTH_TYPE_JWT, AUTH_TYPE_TOKEN # Test that the constants used in urls.py match the ones from constants.py self.assertEqual(AUTH_TYPE_TOKEN, "TOKEN") diff --git a/django/nside_wefa/authentication/tests/utils/test_settings_initialization.py b/django/nside_wefa/authentication/tests/utils/test_settings_initialization.py index 06e0b7b6..e11d38a5 100644 --- a/django/nside_wefa/authentication/tests/utils/test_settings_initialization.py +++ b/django/nside_wefa/authentication/tests/utils/test_settings_initialization.py @@ -1,4 +1,5 @@ from unittest.mock import patch + from django.test import TestCase, override_settings from nside_wefa.authentication.utils.settings_initialization import initialize_settings diff --git a/django/nside_wefa/authentication/urls.py b/django/nside_wefa/authentication/urls.py index a065b90d..8f8a339f 100644 --- a/django/nside_wefa/authentication/urls.py +++ b/django/nside_wefa/authentication/urls.py @@ -12,7 +12,7 @@ from rest_framework.authtoken.views import obtain_auth_token from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView -from nside_wefa.authentication.constants import AUTH_TYPE_TOKEN, AUTH_TYPE_JWT +from nside_wefa.authentication.constants import AUTH_TYPE_JWT, AUTH_TYPE_TOKEN from nside_wefa.authentication.utils.utils import get_authentication_types app_name = "authentication" diff --git a/django/nside_wefa/authentication/utils/settings_initialization.py b/django/nside_wefa/authentication/utils/settings_initialization.py index a4a8e78c..89ce36eb 100644 --- a/django/nside_wefa/authentication/utils/settings_initialization.py +++ b/django/nside_wefa/authentication/utils/settings_initialization.py @@ -6,11 +6,11 @@ classes accordingly. """ -from django.conf import settings - -from nside_wefa.authentication.constants import AUTH_TYPE_TOKEN, AUTH_TYPE_JWT import logging +from django.conf import settings + +from nside_wefa.authentication.constants import AUTH_TYPE_JWT, AUTH_TYPE_TOKEN from nside_wefa.authentication.utils.utils import get_authentication_types logger = logging.getLogger(__name__) diff --git a/django/nside_wefa/common/settings.py b/django/nside_wefa/common/settings.py index c27bdc1d..57097beb 100644 --- a/django/nside_wefa/common/settings.py +++ b/django/nside_wefa/common/settings.py @@ -11,11 +11,11 @@ check`` time, not lazily at first-read time. """ -from typing import Any, Mapping +from collections.abc import Mapping +from typing import Any from django.conf import settings - _SENTINEL: Any = object() diff --git a/django/nside_wefa/common/tests/test_checks.py b/django/nside_wefa/common/tests/test_checks.py index 6230813b..55435165 100644 --- a/django/nside_wefa/common/tests/test_checks.py +++ b/django/nside_wefa/common/tests/test_checks.py @@ -1,6 +1,7 @@ +from unittest.mock import patch + from django.core.checks import Error from django.test import TestCase, override_settings -from unittest.mock import patch from nside_wefa.common.checks import common_settings_check @@ -13,17 +14,17 @@ def test_common_settings_check_missing_setting(self): from django.conf import settings # Mock settings to not have NSIDE_WEFA - with patch.object(settings, "NSIDE_WEFA", None): - with patch("django.conf.settings.NSIDE_WEFA", None, create=True): - # Also ensure getattr returns None for missing attribute - with patch("nside_wefa.common.checks.getattr", return_value=None): - errors = common_settings_check(None) - - self.assertEqual(len(errors), 1) - self.assertIsInstance(errors[0], Error) - self.assertEqual( - errors[0].msg, "NSIDE_WEFA is not defined in settings.py" - ) + # Also ensure getattr returns None for missing attribute + with ( + patch.object(settings, "NSIDE_WEFA", None), + patch("django.conf.settings.NSIDE_WEFA", None, create=True), + patch("nside_wefa.common.checks.getattr", return_value=None), + ): + errors = common_settings_check(None) + + self.assertEqual(len(errors), 1) + self.assertIsInstance(errors[0], Error) + self.assertEqual(errors[0].msg, "NSIDE_WEFA is not defined in settings.py") def test_common_settings_check_missing_app_name_key(self): """Test that missing APP_NAME key in NSIDE_WEFA raises an error.""" diff --git a/django/nside_wefa/legal_consent/README.md b/django/nside_wefa/legal_consent/README.md index 2966e678..8cd62beb 100644 --- a/django/nside_wefa/legal_consent/README.md +++ b/django/nside_wefa/legal_consent/README.md @@ -23,8 +23,8 @@ The Legal Consent app automatically creates and manages consent records for user ```python INSTALLED_APPS = [ # ... other apps - 'nside_wefa.common', - 'nside_wefa.legal_consent', + "nside_wefa.common", + "nside_wefa.legal_consent", ] ``` @@ -42,7 +42,7 @@ from django.urls import path, include urlpatterns = [ # ... other URL patterns - path('legal-consent/', include('nside_wefa.legal_consent.urls')), + path("legal-consent/", include("nside_wefa.legal_consent.urls")), # ... other URL patterns ] ``` @@ -125,9 +125,9 @@ Configure the Legal Consent app in your Django settings: NSIDE_WEFA = { "APP_NAME": "Your Application Name", # Used in template variables "LEGAL_CONSENT": { - "VERSION": 1, # Current legal documents version - "EXPIRY_LIMIT": 365, # Agreement expiry in days - "TEMPLATES": "/path/to/templates", # Optional: Custom template directory + "VERSION": 1, # Current legal documents version + "EXPIRY_LIMIT": 365, # Agreement expiry in days + "TEMPLATES": "/path/to/templates", # Optional: Custom template directory }, } ``` @@ -175,7 +175,7 @@ from django.contrib.auth.models import User from nside_wefa.legal_consent.models.legal_consent import LegalConsent # Get user's legal consent -user = User.objects.get(username='example_user') +user = User.objects.get(username="example_user") legal_consent = user.legalconsent # Update legal consent version diff --git a/django/nside_wefa/legal_consent/checks.py b/django/nside_wefa/legal_consent/checks.py index 00547381..d63349ed 100644 --- a/django/nside_wefa/legal_consent/checks.py +++ b/django/nside_wefa/legal_consent/checks.py @@ -12,16 +12,17 @@ - Django system check framework: https://docs.djangoproject.com/en/stable/topics/checks/ """ -from typing import Any from pathlib import Path +from typing import Any + from django.conf import settings from django.core.checks import Error, register from nside_wefa.common.apps import CommonConfig from nside_wefa.legal_consent.apps import LegalConsentConfig from nside_wefa.utils.checks import ( - check_nside_wefa_settings, check_apps_dependencies_order, + check_nside_wefa_settings, ) diff --git a/django/nside_wefa/legal_consent/migrations/0001_initial.py b/django/nside_wefa/legal_consent/migrations/0001_initial.py index 4961c4e6..79b78f06 100644 --- a/django/nside_wefa/legal_consent/migrations/0001_initial.py +++ b/django/nside_wefa/legal_consent/migrations/0001_initial.py @@ -1,5 +1,7 @@ # Generated by Django 5.2.6 on 2025-09-23 11:53 +from typing import ClassVar + import django.db.models.deletion from django.conf import settings from django.db import migrations, models @@ -23,11 +25,11 @@ def reverse_create_legal_consents(apps, schema_editor): class Migration(migrations.Migration): initial = True - dependencies = [ + dependencies: ClassVar[list[tuple[str, str]]] = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] - operations = [ + operations: ClassVar[list[migrations.operations.base.Operation]] = [ migrations.CreateModel( name="LegalConsent", fields=[ diff --git a/django/nside_wefa/legal_consent/models/legal_consent.py b/django/nside_wefa/legal_consent/models/legal_consent.py index 8d8028ca..74e21364 100644 --- a/django/nside_wefa/legal_consent/models/legal_consent.py +++ b/django/nside_wefa/legal_consent/models/legal_consent.py @@ -86,7 +86,7 @@ def renew(self) -> None: configuration = _LegalConsentConfiguration() self.version = configuration.version - self.accepted_at = datetime.datetime.now(tz=datetime.timezone.utc) + self.accepted_at = datetime.datetime.now(tz=datetime.UTC) self.save() def is_valid(self) -> bool: @@ -108,7 +108,7 @@ def is_valid(self) -> bool: days=configuration.expiry_limit ) return ( - expiry_date > datetime.datetime.now(tz=datetime.timezone.utc) + expiry_date > datetime.datetime.now(tz=datetime.UTC) and self.version == configuration.version ) diff --git a/django/nside_wefa/legal_consent/serializers.py b/django/nside_wefa/legal_consent/serializers.py index 98f79b70..b376c939 100644 --- a/django/nside_wefa/legal_consent/serializers.py +++ b/django/nside_wefa/legal_consent/serializers.py @@ -6,8 +6,10 @@ for clean API responses and OpenAPI schema generation. """ -from rest_framework import serializers +from typing import Any, ClassVar + from drf_spectacular.utils import extend_schema_field, extend_schema_serializer +from rest_framework import serializers from nside_wefa.legal_consent.models.legal_consent import LegalConsent @@ -33,9 +35,9 @@ class LegalConsentSerializer(serializers.ModelSerializer): class Meta: model = LegalConsent - fields = ["version", "accepted_at", "valid"] - read_only_fields = ["valid"] - extra_kwargs = { + fields: ClassVar[list[str]] = ["version", "accepted_at", "valid"] + read_only_fields: ClassVar[list[str]] = ["valid"] + extra_kwargs: ClassVar[dict[str, dict[str, Any]]] = { "version": { "help_text": "Version of the legal documents the user consented to", "allow_null": True, diff --git a/django/nside_wefa/legal_consent/tests/migrations/test_legal_consent_migration.py b/django/nside_wefa/legal_consent/tests/migrations/test_legal_consent_migration.py index a6afb31f..a9219b85 100644 --- a/django/nside_wefa/legal_consent/tests/migrations/test_legal_consent_migration.py +++ b/django/nside_wefa/legal_consent/tests/migrations/test_legal_consent_migration.py @@ -1,6 +1,7 @@ import importlib -from django.contrib.auth.models import User + from django.apps import apps +from django.contrib.auth.models import User from django.db import connection from django.test import TransactionTestCase diff --git a/django/nside_wefa/legal_consent/tests/models/test_legal_consent.py b/django/nside_wefa/legal_consent/tests/models/test_legal_consent.py index 49026913..2e1cb5db 100644 --- a/django/nside_wefa/legal_consent/tests/models/test_legal_consent.py +++ b/django/nside_wefa/legal_consent/tests/models/test_legal_consent.py @@ -3,6 +3,7 @@ from unittest.mock import patch from django.contrib.auth.models import User +from django.db import IntegrityError from django.test import TestCase, override_settings from django.utils import timezone @@ -48,7 +49,7 @@ def test_legal_consent_one_to_one_relationship(self): self.assertEqual(self.user.legalconsent, legal_consent) # Verify only one agreement can exist per user - with self.assertRaises(Exception): + with self.assertRaises(IntegrityError): LegalConsent.objects.create(user=self.user) def test_legal_consent_cascade_deletion(self): @@ -312,9 +313,7 @@ def test_renew_with_valid_configuration(self) -> None: with patch( "nside_wefa.legal_consent.models.legal_consent.datetime" ) as mock_datetime: - mock_now = datetime.datetime( - 2025, 9, 12, 16, 32, tzinfo=datetime.timezone.utc - ) + mock_now = datetime.datetime(2025, 9, 12, 16, 32, tzinfo=datetime.UTC) mock_datetime.datetime.now.return_value = mock_now mock_datetime.timezone = datetime.timezone mock_datetime.timedelta = datetime.timedelta @@ -342,9 +341,7 @@ def test_renew_with_different_configuration_values(self) -> None: with patch( "nside_wefa.legal_consent.models.legal_consent.datetime" ) as mock_datetime: - mock_now = datetime.datetime( - 2025, 9, 12, 16, 32, tzinfo=datetime.timezone.utc - ) + mock_now = datetime.datetime(2025, 9, 12, 16, 32, tzinfo=datetime.UTC) mock_datetime.datetime.now.return_value = mock_now mock_datetime.timezone = datetime.timezone mock_datetime.timedelta = datetime.timedelta @@ -364,9 +361,7 @@ def test_renew_with_minimal_expiration(self) -> None: with patch( "nside_wefa.legal_consent.models.legal_consent.datetime" ) as mock_datetime: - mock_now = datetime.datetime( - 2025, 9, 12, 16, 32, tzinfo=datetime.timezone.utc - ) + mock_now = datetime.datetime(2025, 9, 12, 16, 32, tzinfo=datetime.UTC) mock_datetime.datetime.now.return_value = mock_now mock_datetime.timezone = datetime.timezone mock_datetime.timedelta = datetime.timedelta @@ -428,9 +423,7 @@ def test_renew_timezone_handling(self) -> None: with patch( "nside_wefa.legal_consent.models.legal_consent.datetime" ) as mock_datetime: - mock_now = datetime.datetime( - 2025, 9, 12, 16, 32, 15, tzinfo=datetime.timezone.utc - ) + mock_now = datetime.datetime(2025, 9, 12, 16, 32, 15, tzinfo=datetime.UTC) mock_datetime.datetime.now.return_value = mock_now mock_datetime.timezone = datetime.timezone mock_datetime.timedelta = datetime.timedelta @@ -441,7 +434,7 @@ def test_renew_timezone_handling(self) -> None: self.agreement.refresh_from_db() if self.agreement.accepted_at is not None: self.assertIsNotNone(self.agreement.accepted_at.tzinfo) - self.assertEqual(self.agreement.accepted_at.tzinfo, datetime.timezone.utc) + self.assertEqual(self.agreement.accepted_at.tzinfo, datetime.UTC) # Verify accepted_at was set to the current time self.assertEqual(self.agreement.accepted_at, mock_now) diff --git a/django/nside_wefa/legal_consent/tests/serializers/test_legal_consent_serializer.py b/django/nside_wefa/legal_consent/tests/serializers/test_legal_consent_serializer.py index cc69b360..ebd3841b 100644 --- a/django/nside_wefa/legal_consent/tests/serializers/test_legal_consent_serializer.py +++ b/django/nside_wefa/legal_consent/tests/serializers/test_legal_consent_serializer.py @@ -189,7 +189,5 @@ def test_accepted_at_iso_format(self): self.assertIsInstance(accepted_at_str, str) # Should be able to parse the ISO string back to datetime - parsed_datetime = datetime.datetime.fromisoformat( - accepted_at_str.replace("Z", "+00:00") - ) + parsed_datetime = datetime.datetime.fromisoformat(accepted_at_str) self.assertIsInstance(parsed_datetime, datetime.datetime) diff --git a/django/nside_wefa/legal_consent/tests/test_checks.py b/django/nside_wefa/legal_consent/tests/test_checks.py index ad1ab776..6227c339 100644 --- a/django/nside_wefa/legal_consent/tests/test_checks.py +++ b/django/nside_wefa/legal_consent/tests/test_checks.py @@ -1,5 +1,6 @@ import tempfile from pathlib import Path + from django.core.checks import Error from django.test import TestCase, override_settings @@ -14,24 +15,25 @@ class LegalConsentChecksTest(TestCase): def test_legal_consent_settings_check_missing_setting(self): """Test that missing NSIDE_WEFA.LEGAL_CONSENT setting raises an error.""" - from django.conf import settings from unittest.mock import patch + from django.conf import settings + # Mock settings to not have NSIDE_WEFA - with patch.object(settings, "NSIDE_WEFA", None): - with patch("django.conf.settings.NSIDE_WEFA", None, create=True): - # Also ensure getattr returns None for missing attribute - with patch( - "nside_wefa.legal_consent.checks.getattr", return_value=None - ): - errors = legal_consent_settings_check(None) - - self.assertEqual(len(errors), 1) - self.assertIsInstance(errors[0], Error) - self.assertEqual( - errors[0].msg, - "NSIDE_WEFA.LEGAL_CONSENT is not defined in settings.py", - ) + # Also ensure getattr returns None for missing attribute + with ( + patch.object(settings, "NSIDE_WEFA", None), + patch("django.conf.settings.NSIDE_WEFA", None, create=True), + patch("nside_wefa.legal_consent.checks.getattr", return_value=None), + ): + errors = legal_consent_settings_check(None) + + self.assertEqual(len(errors), 1) + self.assertIsInstance(errors[0], Error) + self.assertEqual( + errors[0].msg, + "NSIDE_WEFA.LEGAL_CONSENT is not defined in settings.py", + ) def test_legal_consent_settings_check_missing_version_key(self): """Test that missing VERSION key in NSIDE_WEFA.LEGAL_CONSENT raises an error.""" @@ -216,10 +218,10 @@ def test_legal_templates_files_check_terms_of_use_missing(self): def test_legal_templates_files_check_both_files_missing(self): """Test that missing both required files raises two errors.""" - with tempfile.TemporaryDirectory() as temp_dir: - # Don't create any files - - with override_settings( + # Don't create any files + with ( + tempfile.TemporaryDirectory() as temp_dir, + override_settings( NSIDE_WEFA={ "LEGAL_CONSENT": { "VERSION": 1, @@ -227,17 +229,16 @@ def test_legal_templates_files_check_both_files_missing(self): "TEMPLATES": temp_dir, } } - ): - errors = legal_templates_files_check(None) - self.assertEqual(len(errors), 2) - self.assertIsInstance(errors[0], Error) - self.assertIsInstance(errors[1], Error) + ), + ): + errors = legal_templates_files_check(None) + self.assertEqual(len(errors), 2) + self.assertIsInstance(errors[0], Error) + self.assertIsInstance(errors[1], Error) - error_messages = [error.msg for error in errors] - self.assertTrue( - any("privacy_notice.md" in msg for msg in error_messages) - ) - self.assertTrue(any("terms_of_use.md" in msg for msg in error_messages)) + error_messages = [error.msg for error in errors] + self.assertTrue(any("privacy_notice.md" in msg for msg in error_messages)) + self.assertTrue(any("terms_of_use.md" in msg for msg in error_messages)) def test_legal_templates_files_check_nonexistent_directory(self): """Test that non-existent TEMPLATES directory raises errors for both files.""" diff --git a/django/nside_wefa/legal_consent/tests/views/test_gdpr_agreement_view.py b/django/nside_wefa/legal_consent/tests/views/test_gdpr_agreement_view.py index f5d8630d..09cd5ac2 100644 --- a/django/nside_wefa/legal_consent/tests/views/test_gdpr_agreement_view.py +++ b/django/nside_wefa/legal_consent/tests/views/test_gdpr_agreement_view.py @@ -5,8 +5,8 @@ from django.urls import reverse from django.utils import timezone from freezegun import freeze_time -from rest_framework.test import APITestCase, APIClient from rest_framework import status +from rest_framework.test import APIClient, APITestCase from nside_wefa.legal_consent.models import LegalConsent @@ -215,8 +215,6 @@ def test_patch_uses_current_configuration(self): # Allow for small time difference in test execution time_diff = abs( - ( - accepted_at - datetime.datetime.now(tz=datetime.timezone.utc) - ).total_seconds() + (accepted_at - datetime.datetime.now(tz=datetime.UTC)).total_seconds() ) self.assertLess(time_diff, 5) # Within 5 seconds diff --git a/django/nside_wefa/legal_consent/tests/views/test_privacy_notice_view.py b/django/nside_wefa/legal_consent/tests/views/test_privacy_notice_view.py index e2f5c0ca..bc413d6b 100644 --- a/django/nside_wefa/legal_consent/tests/views/test_privacy_notice_view.py +++ b/django/nside_wefa/legal_consent/tests/views/test_privacy_notice_view.py @@ -4,8 +4,8 @@ from django.test import override_settings from django.urls import reverse -from rest_framework.test import APITestCase, APIClient from rest_framework import status +from rest_framework.test import APIClient, APITestCase class PrivacyNoticeViewTest(APITestCase): diff --git a/django/nside_wefa/legal_consent/tests/views/test_terms_of_use_view.py b/django/nside_wefa/legal_consent/tests/views/test_terms_of_use_view.py index 81bb0623..38430aa6 100644 --- a/django/nside_wefa/legal_consent/tests/views/test_terms_of_use_view.py +++ b/django/nside_wefa/legal_consent/tests/views/test_terms_of_use_view.py @@ -4,8 +4,8 @@ from django.test import override_settings from django.urls import reverse -from rest_framework.test import APITestCase, APIClient from rest_framework import status +from rest_framework.test import APIClient, APITestCase class TermsOfServiceViewTest(APITestCase): diff --git a/django/nside_wefa/legal_consent/urls.py b/django/nside_wefa/legal_consent/urls.py index f8073e0a..b48d4cb3 100644 --- a/django/nside_wefa/legal_consent/urls.py +++ b/django/nside_wefa/legal_consent/urls.py @@ -23,7 +23,8 @@ """ from django.urls import path -from .views import LegalConsentView, TermsOfUseView, PrivacyNoticeView + +from .views import LegalConsentView, PrivacyNoticeView, TermsOfUseView app_name = "legal_consent" diff --git a/django/nside_wefa/legal_consent/views/__init__.py b/django/nside_wefa/legal_consent/views/__init__.py index 83f63e11..6b129c8c 100644 --- a/django/nside_wefa/legal_consent/views/__init__.py +++ b/django/nside_wefa/legal_consent/views/__init__.py @@ -1,9 +1,9 @@ from .legal_consent_view import LegalConsentView -from .terms_of_use_view import TermsOfUseView from .privacy_notice_view import PrivacyNoticeView +from .terms_of_use_view import TermsOfUseView __all__ = [ "LegalConsentView", - "TermsOfUseView", "PrivacyNoticeView", + "TermsOfUseView", ] diff --git a/django/nside_wefa/legal_consent/views/legal_consent_view.py b/django/nside_wefa/legal_consent/views/legal_consent_view.py index f5939ef7..8944c92a 100644 --- a/django/nside_wefa/legal_consent/views/legal_consent_view.py +++ b/django/nside_wefa/legal_consent/views/legal_consent_view.py @@ -1,8 +1,8 @@ -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework.request import Request +from drf_spectacular.utils import OpenApiResponse, extend_schema from rest_framework.permissions import IsAuthenticated -from drf_spectacular.utils import extend_schema, OpenApiResponse +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.views import APIView from ..models import LegalConsent from ..serializers import LegalConsentSerializer @@ -23,7 +23,7 @@ class LegalConsentView(APIView): Required. Users must be authenticated to access any endpoint. """ - permission_classes = [IsAuthenticated] + permission_classes = (IsAuthenticated,) serializer_class = LegalConsentSerializer @extend_schema( diff --git a/django/nside_wefa/legal_consent/views/privacy_notice_view.py b/django/nside_wefa/legal_consent/views/privacy_notice_view.py index e24bd3d8..ba45672e 100644 --- a/django/nside_wefa/legal_consent/views/privacy_notice_view.py +++ b/django/nside_wefa/legal_consent/views/privacy_notice_view.py @@ -6,10 +6,10 @@ """ from django.http import HttpResponse +from drf_spectacular.utils import OpenApiResponse, extend_schema from rest_framework.permissions import AllowAny -from rest_framework.views import APIView from rest_framework.request import Request -from drf_spectacular.utils import extend_schema, OpenApiResponse +from rest_framework.views import APIView from nside_wefa.legal_consent.views.utils import get_document_content @@ -26,7 +26,7 @@ class PrivacyNoticeView(APIView): GET: Returns the Privacy Notice document as plain text """ - permission_classes = [AllowAny] + permission_classes = (AllowAny,) @extend_schema( operation_id="legal_consent_privacy_notice", diff --git a/django/nside_wefa/legal_consent/views/terms_of_use_view.py b/django/nside_wefa/legal_consent/views/terms_of_use_view.py index 3df9af78..e7cda422 100644 --- a/django/nside_wefa/legal_consent/views/terms_of_use_view.py +++ b/django/nside_wefa/legal_consent/views/terms_of_use_view.py @@ -1,8 +1,8 @@ from django.http import HttpResponse +from drf_spectacular.utils import OpenApiResponse, extend_schema from rest_framework.permissions import AllowAny -from rest_framework.views import APIView from rest_framework.request import Request -from drf_spectacular.utils import extend_schema, OpenApiResponse +from rest_framework.views import APIView from nside_wefa.legal_consent.views.utils import get_document_content @@ -19,7 +19,7 @@ class TermsOfUseView(APIView): GET: Returns the Terms of Use document as plain text """ - permission_classes = [AllowAny] + permission_classes = (AllowAny,) @extend_schema( operation_id="legal_consent_terms_of_use", diff --git a/django/nside_wefa/legal_consent/views/utils.py b/django/nside_wefa/legal_consent/views/utils.py index a790e5f8..661ea624 100644 --- a/django/nside_wefa/legal_consent/views/utils.py +++ b/django/nside_wefa/legal_consent/views/utils.py @@ -1,6 +1,7 @@ -from django.conf import settings from pathlib import Path +from django.conf import settings + """ Utilities for serving Legal Consent markdown documents. diff --git a/django/nside_wefa/locale/README.md b/django/nside_wefa/locale/README.md index 1ffed61e..08f145e6 100644 --- a/django/nside_wefa/locale/README.md +++ b/django/nside_wefa/locale/README.md @@ -29,8 +29,8 @@ before the user authenticates. ```python INSTALLED_APPS = [ # ... other apps - 'nside_wefa.common', - 'nside_wefa.locale', + "nside_wefa.common", + "nside_wefa.locale", ] ``` @@ -47,7 +47,7 @@ before the user authenticates. urlpatterns = [ # ... other URL patterns - path('locale/', include('nside_wefa.locale.urls')), + path("locale/", include("nside_wefa.locale.urls")), ] ``` diff --git a/django/nside_wefa/locale/checks.py b/django/nside_wefa/locale/checks.py index 71ce9fc6..537b0237 100644 --- a/django/nside_wefa/locale/checks.py +++ b/django/nside_wefa/locale/checks.py @@ -13,14 +13,15 @@ """ from typing import Any + from django.conf import settings from django.core.checks import Error, register from nside_wefa.common.apps import CommonConfig from nside_wefa.locale.apps import LocaleConfig from nside_wefa.utils.checks import ( - check_nside_wefa_settings, check_apps_dependencies_order, + check_nside_wefa_settings, ) diff --git a/django/nside_wefa/locale/migrations/0001_initial.py b/django/nside_wefa/locale/migrations/0001_initial.py index 14170abd..0d084915 100644 --- a/django/nside_wefa/locale/migrations/0001_initial.py +++ b/django/nside_wefa/locale/migrations/0001_initial.py @@ -1,3 +1,5 @@ +from typing import ClassVar + import django.db.models.deletion from django.conf import settings from django.db import migrations, models @@ -21,11 +23,11 @@ def reverse_create_user_locales(apps, schema_editor): class Migration(migrations.Migration): initial = True - dependencies = [ + dependencies: ClassVar[list[tuple[str, str]]] = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] - operations = [ + operations: ClassVar[list[migrations.operations.base.Operation]] = [ migrations.CreateModel( name="UserLocale", fields=[ diff --git a/django/nside_wefa/locale/models/user_locale.py b/django/nside_wefa/locale/models/user_locale.py index 8d87337a..38dcc097 100644 --- a/django/nside_wefa/locale/models/user_locale.py +++ b/django/nside_wefa/locale/models/user_locale.py @@ -6,7 +6,7 @@ user creation. """ -from typing import Any, List, TypedDict, cast +from typing import Any, TypedDict, cast from django.conf import settings from django.db import models @@ -14,7 +14,7 @@ class _LocaleSettingsDict(TypedDict): - AVAILABLE: List[str] + AVAILABLE: list[str] DEFAULT: str diff --git a/django/nside_wefa/locale/serializers.py b/django/nside_wefa/locale/serializers.py index 9dcc3e81..13a2abc3 100644 --- a/django/nside_wefa/locale/serializers.py +++ b/django/nside_wefa/locale/serializers.py @@ -6,10 +6,10 @@ for clean API responses and OpenAPI schema generation. """ -from typing import Any +from typing import Any, ClassVar -from rest_framework import serializers from drf_spectacular.utils import extend_schema_serializer +from rest_framework import serializers from nside_wefa.locale.models.user_locale import UserLocale, _LocaleConfiguration @@ -27,8 +27,8 @@ class UserLocaleSerializer(serializers.ModelSerializer): class Meta: model = UserLocale - fields = ["code"] - extra_kwargs = { + fields: ClassVar[list[str]] = ["code"] + extra_kwargs: ClassVar[dict[str, dict[str, Any]]] = { "code": { "help_text": "Preferred locale code for the authenticated user. " "Must be one of the codes listed in NSIDE_WEFA.LOCALE.AVAILABLE.", diff --git a/django/nside_wefa/locale/tests/models/test_user_locale.py b/django/nside_wefa/locale/tests/models/test_user_locale.py index cc2ee016..2671bbb9 100644 --- a/django/nside_wefa/locale/tests/models/test_user_locale.py +++ b/django/nside_wefa/locale/tests/models/test_user_locale.py @@ -1,4 +1,5 @@ from django.contrib.auth.models import User +from django.db import IntegrityError from django.test import TestCase, override_settings from nside_wefa.locale.models import UserLocale @@ -30,7 +31,7 @@ def test_user_locale_str_representation(self): ) def test_user_locale_one_to_one(self): - with self.assertRaises(Exception): + with self.assertRaises(IntegrityError): UserLocale.objects.create(user=self.user) def test_user_locale_cascade_deletion(self): diff --git a/django/nside_wefa/locale/views/__init__.py b/django/nside_wefa/locale/views/__init__.py index 1282e3e5..cfa62d38 100644 --- a/django/nside_wefa/locale/views/__init__.py +++ b/django/nside_wefa/locale/views/__init__.py @@ -1,7 +1,7 @@ -from .user_locale_view import UserLocaleView from .available_locales_view import AvailableLocalesView +from .user_locale_view import UserLocaleView __all__ = [ - "UserLocaleView", "AvailableLocalesView", + "UserLocaleView", ] diff --git a/django/nside_wefa/locale/views/available_locales_view.py b/django/nside_wefa/locale/views/available_locales_view.py index 7e51fab7..8c8d0841 100644 --- a/django/nside_wefa/locale/views/available_locales_view.py +++ b/django/nside_wefa/locale/views/available_locales_view.py @@ -1,8 +1,8 @@ -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework.request import Request +from drf_spectacular.utils import OpenApiResponse, extend_schema from rest_framework.permissions import AllowAny -from drf_spectacular.utils import extend_schema, OpenApiResponse +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.views import APIView from ..models.user_locale import _LocaleConfiguration from ..serializers import AvailableLocalesSerializer @@ -17,7 +17,7 @@ class AvailableLocalesView(APIView): locale picker on a public landing page). """ - permission_classes = [AllowAny] + permission_classes = (AllowAny,) serializer_class = AvailableLocalesSerializer @extend_schema( diff --git a/django/nside_wefa/locale/views/user_locale_view.py b/django/nside_wefa/locale/views/user_locale_view.py index 8eef3ba6..88b19e32 100644 --- a/django/nside_wefa/locale/views/user_locale_view.py +++ b/django/nside_wefa/locale/views/user_locale_view.py @@ -1,8 +1,8 @@ -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework.request import Request +from drf_spectacular.utils import OpenApiResponse, extend_schema from rest_framework.permissions import IsAuthenticated -from drf_spectacular.utils import extend_schema, OpenApiResponse +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.views import APIView from ..models import UserLocale from ..serializers import UserLocaleSerializer @@ -20,7 +20,7 @@ class UserLocaleView(APIView): Required. Users must be authenticated to access any endpoint. """ - permission_classes = [IsAuthenticated] + permission_classes = (IsAuthenticated,) serializer_class = UserLocaleSerializer @extend_schema( diff --git a/django/nside_wefa/utils/checks.py b/django/nside_wefa/utils/checks.py index d24ba0ea..34999936 100644 --- a/django/nside_wefa/utils/checks.py +++ b/django/nside_wefa/utils/checks.py @@ -14,8 +14,9 @@ - Django system check framework: https://docs.djangoproject.com/en/stable/topics/checks/ """ +from collections.abc import Callable from importlib import import_module -from typing import Any, Callable, Dict, List, Optional +from typing import Any from django.apps import apps as django_apps from django.conf import settings @@ -24,9 +25,9 @@ def check_nside_wefa_settings( section_name: str, - required_keys: List[str], - custom_validators: Optional[Dict[str, Callable[[Any], List[Error]]]] = None, -) -> List[Error]: + required_keys: list[str], + custom_validators: dict[str, Callable[[Any], list[Error]]] | None = None, +) -> list[Error]: """Validate a subsection of the ``NSIDE_WEFA`` settings. This function checks that the top-level ``NSIDE_WEFA`` dictionary contains @@ -46,7 +47,7 @@ def check_nside_wefa_settings( :return: A list of configuration errors. Empty if everything is correctly configured. :rtype: list[django.core.checks.Error] """ - errors: List[Error] = [] + errors: list[Error] = [] # Get NSIDE_WEFA settings nside_wefa_settings: Any = getattr(settings, "NSIDE_WEFA", None) @@ -136,10 +137,10 @@ def check_apps_dependencies_order(dependencies: list[str]) -> list[Error]: # --------------------------------------------------------------------------- -def validate_bool(setting_path: str) -> Callable[[Any], List[Error]]: +def validate_bool(setting_path: str) -> Callable[[Any], list[Error]]: """Return a validator that ensures the value is a boolean.""" - def _validator(value: Any) -> List[Error]: + def _validator(value: Any) -> list[Error]: if not isinstance(value, bool): return [ Error( @@ -153,10 +154,10 @@ def _validator(value: Any) -> List[Error]: def validate_optional_positive_int( setting_path: str, -) -> Callable[[Any], List[Error]]: +) -> Callable[[Any], list[Error]]: """Return a validator that accepts ``None`` or a positive integer.""" - def _validator(value: Any) -> List[Error]: + def _validator(value: Any) -> list[Error]: if value is None: return [] if isinstance(value, bool) or not isinstance(value, int): @@ -179,16 +180,16 @@ def _validator(value: Any) -> List[Error]: def validate_string_list( - setting_path: str, allowed: Optional[List[str]] = None -) -> Callable[[Any], List[Error]]: + setting_path: str, allowed: list[str] | None = None +) -> Callable[[Any], list[Error]]: """Return a validator that ensures the value is a list of non-empty strings. When ``allowed`` is provided, every entry must additionally be one of the allowed values. """ - def _validator(value: Any) -> List[Error]: - errors: List[Error] = [] + def _validator(value: Any) -> list[Error]: + errors: list[Error] = [] if not isinstance(value, list): return [ Error( @@ -219,10 +220,10 @@ def _validator(value: Any) -> List[Error]: def validate_dotted_path_callable( setting_path: str, -) -> Callable[[Any], List[Error]]: +) -> Callable[[Any], list[Error]]: """Return a validator that resolves the value as a dotted-path callable.""" - def _validator(value: Any) -> List[Error]: + def _validator(value: Any) -> list[Error]: if not isinstance(value, str) or "." not in value: return [ Error( @@ -258,7 +259,7 @@ def _validator(value: Any) -> List[Error]: return _validator -def validate_model_label(label: Any) -> Optional[Error]: +def validate_model_label(label: Any) -> Error | None: """Resolve an ``"app_label.ModelName"`` string via the Django app registry. Returns ``None`` on success, an :class:`~django.core.checks.Error` instance @@ -284,7 +285,7 @@ def validate_model_label(label: Any) -> Optional[Error]: def validate_model_label_dict( setting_path: str, -) -> Callable[[Any], List[Error]]: +) -> Callable[[Any], list[Error]]: """Return a validator that ensures the value is a ``{label: dict}`` mapping. Each key must be a resolvable ``"app_label.ModelName"`` string. Each value @@ -292,8 +293,8 @@ def validate_model_label_dict( that know which keys make sense for them. """ - def _validator(value: Any) -> List[Error]: - errors: List[Error] = [] + def _validator(value: Any) -> list[Error]: + errors: list[Error] = [] if not isinstance(value, dict): return [ Error(