From 345f17558afa2b3c21a98427129a1af46f1140c7 Mon Sep 17 00:00:00 2001
From: Akhil Narang
Date: Mon, 22 Dec 2025 13:38:24 +0530
Subject: [PATCH 01/13] build(deps): bump to python>=3.14, nodejs>=24
Signed-off-by: Akhil Narang
---
.github/actions/setup/action.yml | 4 ++--
.github/workflows/_base-migration.yml | 19 ++++++++++++++-----
.github/workflows/_base-server-tests.yml | 2 +-
.github/workflows/_base-ui-tests.yml | 2 +-
.github/workflows/create-release.yml | 2 +-
.github/workflows/linters.yml | 2 +-
.github/workflows/on_release.yml | 2 +-
.github/workflows/publish-assets-develop.yml | 2 +-
.github/workflows/run-indinvidual-tests.yml | 2 +-
.github/workflows/server-tests.yml | 4 ++--
package.json | 2 +-
pyproject.toml | 4 ++--
12 files changed, 28 insertions(+), 19 deletions(-)
diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml
index 6122a0e5df..a48cb5778b 100644
--- a/.github/actions/setup/action.yml
+++ b/.github/actions/setup/action.yml
@@ -4,11 +4,11 @@ inputs:
python-version:
description: 'Python version to use'
required: false
- default: '3.12.6'
+ default: '3.14'
node-version:
description: 'Node.js version to use'
required: false
- default: '22'
+ default: '24'
build-assets:
required: false
description: 'Wether to build assets'
diff --git a/.github/workflows/_base-migration.yml b/.github/workflows/_base-migration.yml
index a1838c27fe..cc9a529c87 100644
--- a/.github/workflows/_base-migration.yml
+++ b/.github/workflows/_base-migration.yml
@@ -12,11 +12,11 @@ on:
python-version:
required: false
type: string
- default: '3.10'
+ default: '3.14'
node-version:
required: false
type: number
- default: 22
+ default: 24
db-artifact-url:
required: false
type: string
@@ -49,6 +49,15 @@ jobs:
disable-socketio: true
disable-web: true
db-root-password: ${{ env.DB_ROOT_PASSWORD }}
+
+ - name: Setup Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: |
+ 3.11
+ 3.13
+ ${{ inputs.python-version }}
+
- name: Execute pre-migration tasks
if: inputs.pre
@@ -108,7 +117,7 @@ jobs:
fi
echo "Setting up environment..."
- if rm -rf ${GITHUB_WORKSPACE}/env && python -m venv ${GITHUB_WORKSPACE}/env; then
+ if rm -rf ${GITHUB_WORKSPACE}/env && python"$2" -m venv ${GITHUB_WORKSPACE}/env; then
source ${GITHUB_WORKSPACE}/env/bin/activate
pip install --quiet --upgrade pip
pip install --quiet frappe-bench
@@ -148,13 +157,13 @@ jobs:
- name: Update to v14
run: |
source $RUNNER_TEMP/migrate
- update_to_version 14
+ update_to_version 14 3.11
exit $?
- name: Update to v15
run: |
source $RUNNER_TEMP/migrate
- update_to_version 15
+ update_to_version 15 3.13
exit $?
- name: Update to last commit
diff --git a/.github/workflows/_base-server-tests.yml b/.github/workflows/_base-server-tests.yml
index 74c71dfd80..7117723b38 100644
--- a/.github/workflows/_base-server-tests.yml
+++ b/.github/workflows/_base-server-tests.yml
@@ -13,7 +13,7 @@ on:
node-version:
required: false
type: number
- default: 22
+ default: 24
parallel-runs:
required: false
type: number
diff --git a/.github/workflows/_base-ui-tests.yml b/.github/workflows/_base-ui-tests.yml
index 76cf8b347a..cfa339c593 100644
--- a/.github/workflows/_base-ui-tests.yml
+++ b/.github/workflows/_base-ui-tests.yml
@@ -13,7 +13,7 @@ on:
node-version:
required: false
type: number
- default: 22
+ default: 24
parallel-runs:
required: false
type: number
diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml
index bcf01fdff8..9a84776d01 100644
--- a/.github/workflows/create-release.yml
+++ b/.github/workflows/create-release.yml
@@ -19,7 +19,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
- node-version: 22
+ node-version: 24
- name: Setup dependencies
run: |
npm install @semantic-release/git @semantic-release/exec --no-save
diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml
index bd6ed8a1ea..b9563979b5 100644
--- a/.github/workflows/linters.yml
+++ b/.github/workflows/linters.yml
@@ -24,7 +24,7 @@ jobs:
fetch-depth: 200
- uses: actions/setup-node@v6
with:
- node-version: 22
+ node-version: 24
check-latest: true
- name: Check commit titles
diff --git a/.github/workflows/on_release.yml b/.github/workflows/on_release.yml
index 7acb1fa0b9..962f8268ca 100644
--- a/.github/workflows/on_release.yml
+++ b/.github/workflows/on_release.yml
@@ -22,7 +22,7 @@ jobs:
- uses: actions/setup-node@v6
with:
- node-version: 22
+ node-version: 24
- uses: actions/setup-python@v6
with:
diff --git a/.github/workflows/publish-assets-develop.yml b/.github/workflows/publish-assets-develop.yml
index f7072fbc5d..2d60df6c70 100644
--- a/.github/workflows/publish-assets-develop.yml
+++ b/.github/workflows/publish-assets-develop.yml
@@ -16,7 +16,7 @@ jobs:
path: 'frappe'
- uses: actions/setup-node@v6
with:
- node-version: 22
+ node-version: 24
- uses: actions/setup-python@v6
with:
python-version: '3.14'
diff --git a/.github/workflows/run-indinvidual-tests.yml b/.github/workflows/run-indinvidual-tests.yml
index cd1da7192c..2cf4100aac 100644
--- a/.github/workflows/run-indinvidual-tests.yml
+++ b/.github/workflows/run-indinvidual-tests.yml
@@ -79,7 +79,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v6
with:
- node-version: 22
+ node-version: 24
check-latest: true
- name: Add to Hosts
diff --git a/.github/workflows/server-tests.yml b/.github/workflows/server-tests.yml
index 3c4438f4b7..d8358b11a2 100644
--- a/.github/workflows/server-tests.yml
+++ b/.github/workflows/server-tests.yml
@@ -58,8 +58,8 @@ jobs:
uses: ./.github/workflows/_base-migration.yml
with:
db-artifact-url: https://frappeframework.com/files/v13-frappe.sql.gz
- python-version: '3.10'
- node-version: 22
+ python-version: '3.14'
+ node-version: 24
fake-success: ${{ needs.checkrun.outputs.build != 'strawberry' }}
coverage:
diff --git a/package.json b/package.json
index 8e75cc962e..ff869962c2 100644
--- a/package.json
+++ b/package.json
@@ -16,7 +16,7 @@
"url": "https://github.com/frappe/frappe/issues"
},
"engines": {
- "node": ">=18"
+ "node": ">=24"
},
"homepage": "https://frappeframework.com",
"dependencies": {
diff --git a/pyproject.toml b/pyproject.toml
index 389b67e880..87696fd11b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ authors = [
{ name = "Frappe Technologies Pvt Ltd", email = "developers@frappe.io"}
]
description = "Metadata driven, full-stack low code web framework"
-requires-python = ">=3.10,<3.15"
+requires-python = ">=3.14,<3.15"
readme = "README.md"
dynamic = ["version"]
dependencies = [
@@ -160,7 +160,7 @@ freezegun = "~=1.2.2"
[tool.ruff]
line-length = 110
-target-version = "py310"
+target-version = "py314"
exclude = [
"**/doctype/*/boilerplate/*.py" # boilerplate are template strings, not valid python
]
From 4c871f1c09f009d75d63875fd6058dff218cd9b0 Mon Sep 17 00:00:00 2001
From: Akhil Narang
Date: Mon, 22 Dec 2025 17:09:33 +0530
Subject: [PATCH 02/13] build(deps): bump
Signed-off-by: Akhil Narang
---
.pre-commit-config.yaml | 6 ++--
pyproject.toml | 67 ++++++++++++++++++++---------------------
2 files changed, 35 insertions(+), 38 deletions(-)
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 3606b63941..dc17245c19 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -6,7 +6,7 @@ fail_fast: false
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v5.0.0
+ rev: v6.0.0
hooks:
- id: trailing-whitespace
files: "frappe.*"
@@ -22,7 +22,7 @@ repos:
exclude: ^frappe/tests/classes/context_managers\.py$
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.13.2
+ rev: v0.14.10
hooks:
- id: ruff
name: "Run ruff import sorter"
@@ -71,7 +71,7 @@ repos:
)$
- repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook
- rev: v9.22.0
+ rev: v9.23.0
hooks:
- id: commitlint
stages: [commit-msg]
diff --git a/pyproject.toml b/pyproject.toml
index 87696fd11b..776fcce914 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -10,79 +10,77 @@ dynamic = ["version"]
dependencies = [
# core dependencies
"Babel~=2.16.0",
- "Click~=8.2.0",
+ "Click~=8.3.1",
"filelock~=3.20.1",
"filetype~=1.2.0",
- "GitPython~=3.1.44",
+ "GitPython~=3.1.45",
"Jinja2~=3.1.6",
- "Pillow~=11.3.0",
+ "Pillow~=12.0.0",
"PyJWT~=2.10.1",
# We depend on internal attributes,
# do NOT add loose requirements on PyMySQL versions.
- "PyMySQL==1.1.1",
- "pypdf==6.4.0",
+ "PyMySQL==1.1.2",
+ "pypdf==6.5.0",
"PyPika @ git+https://github.com/frappe/pypika@2c50e6142b2d61d2d243e466fdd5dc03b3d918f2",
"mysqlclient==2.2.7",
"PyQRCode~=1.2.1",
- "PyYAML~=6.0.2",
+ "PyYAML~=6.0.3",
"RestrictedPython~=8.1",
"WeasyPrint==66.0",
- "pydyf==0.11.0",
+ "pydyf==0.12.1",
"Werkzeug==3.1.4",
"Whoosh~=2.7.4",
- "beautifulsoup4~=4.13.4",
+ "beautifulsoup4~=4.13.5",
"bleach-allowlist~=1.0.3",
- "bleach[css]~=6.2.0",
+ "bleach[css]~=6.3.0",
"chardet~=5.2.0",
"croniter~=6.0.0",
- "cryptography~=46.0.2",
+ "cryptography~=46.0.3",
"cssutils~=2.11.1",
"email-reply-parser~=0.5.12",
"gunicorn @ git+https://github.com/frappe/gunicorn@bb554053bb87218120d76ab6676af7015680e8b6",
"html5lib~=1.1",
"ipython~=8.37.0",
- "ldap3~=2.9",
- "markdown2~=2.5.3",
- "MarkupSafe~=3.0.2",
+ "ldap3~=2.9.1",
+ "markdown2~=2.5.4",
+ "MarkupSafe~=3.0.3",
"num2words~=0.5.14",
"oauthlib~=3.2.2",
"openpyxl~=3.1.5",
- "orjson~=3.11.3",
+ "orjson~=3.11.5",
"passlib~=1.7.4",
"pdfkit~=1.0.0",
- "phonenumbers~=9.0.7",
+ "phonenumbers~=9.0.21",
"premailer~=3.10.0",
"psutil~=7.0.0",
- "psycopg2-binary~=2.9.1",
+ "psycopg2-binary~=2.9.11",
"pyOpenSSL~=25.3.0",
- "pydantic~=2.12.0",
+ "pydantic~=2.12.5",
"pyotp~=2.9.0",
- "python-dateutil~=2.9.0",
+ "python-dateutil~=2.9.0.post0",
"pytz==2025.2",
"rauth~=0.7.3",
- "redis~=6.2.0",
- "hiredis~=3.2.1",
+ "redis~=7.1.0",
+ "hiredis~=3.3.0",
"requests-oauthlib~=2.0.0",
- "requests~=2.32.4",
+ "requests~=2.32.5",
# We depend on internal attributes of RQ.
# Do NOT add loose requirements on RQ versions.
# Audit the code changes w.r.t. background_jobs.py before updating.
- "rq==2.4.0",
- "rsa~=4.9",
+ "rq==2.6.1",
+ "rsa~=4.9.1",
"semantic-version~=2.10.0",
"sentry-sdk~=1.45.1",
- "sqlparse~=0.5.0",
- "sql_metadata~=2.17.0",
+ "sqlparse~=0.5.5",
+ "sql_metadata~=2.19.0",
"tenacity~=9.1.2",
"terminaltables~=3.1.10",
- "traceback-with-variables~=2.2.0",
- "typing_extensions>=4.6.1,<5",
- "tomli~=2.2.1",
- "uuid-utils~=0.11.0",
+ "traceback-with-variables~=2.2.1",
+ "typing_extensions>=4.15.0,<5",
+ "uuid-utils~=0.12.0",
"xlrd~=2.0.2",
"zxcvbn~=4.5.0",
- "markdownify~=1.1.0",
-
+ "markdownify~=1.2.2",
# integration dependencies
"google-api-python-client~=2.172.0",
"google-auth-oauthlib~=1.2.2",
@@ -90,8 +88,7 @@ dependencies = [
"posthog~=5.0.0",
"vobject~=0.9.9",
"pycountry~=24.6.1",
-
- "websockets"
+ "websockets~=15.0.1",
]
[project.urls]
@@ -102,7 +99,7 @@ Repository = "https://github.com/frappe/frappe.git"
[project.optional-dependencies]
dev = [
"pyngrok~=6.0.0",
- "watchdog~=3.0.0",
+ "watchdog~=6.0.0",
"responses==0.23.1",
# typechecking
"basedmypy",
@@ -153,7 +150,7 @@ coverage = "~=7.10.0"
Faker = "~=18.10.1"
pyngrok = "~=6.0.0"
unittest-xml-reporting = "~=3.2.0"
-watchdog = "~=3.0.0"
+watchdog = "~=6.0.0"
hypothesis = "~=6.77.0"
responses = "==0.23.1"
freezegun = "~=1.2.2"
From 588cb1e44dce0e9043bb1acd0875f0ee737f4ffe Mon Sep 17 00:00:00 2001
From: Akhil Narang
Date: Mon, 22 Dec 2025 17:10:02 +0530
Subject: [PATCH 03/13] refactor: tomli -> tomllib
Signed-off-by: Akhil Narang
---
frappe/testing/environment.py | 7 +++----
frappe/tests/utils/generators.py | 5 ++---
frappe/utils/change_log.py | 2 +-
frappe/www/attribution.py | 5 ++---
4 files changed, 8 insertions(+), 11 deletions(-)
diff --git a/frappe/testing/environment.py b/frappe/testing/environment.py
index 079daa588f..4faa8f2bd2 100644
--- a/frappe/testing/environment.py
+++ b/frappe/testing/environment.py
@@ -23,10 +23,9 @@ import functools
import inspect
import logging
import pkgutil
+import tomllib
import unittest
-import tomli
-
import frappe
import frappe.utils.scheduler
from frappe.tests.utils import make_test_records, toggle_test_mode
@@ -91,7 +90,7 @@ def _decorate_all_methods_and_functions_with_type_checker():
def _get_config_from_pyproject(app_path):
try:
with open(f"{app_path}/pyproject.toml", "rb") as f:
- config = tomli.load(f)
+ config = tomllib.load(f)
return (
config.get("tool", {})
.get("frappe", {})
@@ -100,7 +99,7 @@ def _decorate_all_methods_and_functions_with_type_checker():
)
except FileNotFoundError:
return {}
- except tomli.TOMLDecodeError:
+ except tomllib.TOMLDecodeError:
logger.warning(f"Failed to parse pyproject.toml for app {app_path}")
return {}
diff --git a/frappe/tests/utils/generators.py b/frappe/tests/utils/generators.py
index 93b72edc2d..e22fc7d2ed 100644
--- a/frappe/tests/utils/generators.py
+++ b/frappe/tests/utils/generators.py
@@ -2,6 +2,7 @@ import datetime
import json
import logging
import os
+import tomllib
from collections import defaultdict
from collections.abc import Generator
from functools import cache
@@ -10,8 +11,6 @@ from pathlib import Path
from types import MappingProxyType, ModuleType
from typing import TYPE_CHECKING, Any
-import tomli
-
import frappe
from frappe.model.naming import revert_series_if_last
from frappe.modules import get_doctype_module, get_module_path, load_doctype_module
@@ -129,7 +128,7 @@ def load_test_records_for(index_doctype) -> dict[str, Any]:
toml_path = os.path.join(module_path, "test_records.toml")
if os.path.exists(toml_path):
with open(toml_path, "rb") as f:
- return tomli.load(f)
+ return tomllib.load(f)
else:
return {}
diff --git a/frappe/utils/change_log.py b/frappe/utils/change_log.py
index 83310567a6..67b1615b1e 100644
--- a/frappe/utils/change_log.py
+++ b/frappe/utils/change_log.py
@@ -394,7 +394,7 @@ def show_update_popup():
def get_pyproject(app: str) -> dict | None:
- from tomli import load
+ from tomllib import load
pyproject_path = frappe.get_app_path(app, "..", "pyproject.toml")
diff --git a/frappe/www/attribution.py b/frappe/www/attribution.py
index 2024aadbd2..35f539dc9e 100644
--- a/frappe/www/attribution.py
+++ b/frappe/www/attribution.py
@@ -2,10 +2,9 @@ import contextlib
import importlib.metadata
import json
import re
+import tomllib
from pathlib import Path
-import tomli
-
import frappe
from frappe import _
from frappe.permissions import is_system_user
@@ -134,7 +133,7 @@ def get_pyproject_info(app: str) -> dict:
return {}
with open(pyproject_toml, "rb") as f:
- pyproject = tomli.load(f)
+ pyproject = tomllib.load(f)
return pyproject.get("project", {})
From 109af28aec9dc148f088b33ee38be0fba3630b8c Mon Sep 17 00:00:00 2001
From: Akhil Narang
Date: Mon, 22 Dec 2025 17:10:29 +0530
Subject: [PATCH 04/13] fix: adjust to bs4 API changes
Signed-off-by: Akhil Narang
---
frappe/core/doctype/communication/communication.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frappe/core/doctype/communication/communication.py b/frappe/core/doctype/communication/communication.py
index 02f790fa9c..59aefc53bf 100644
--- a/frappe/core/doctype/communication/communication.py
+++ b/frappe/core/doctype/communication/communication.py
@@ -230,7 +230,7 @@ class Communication(Document, CommunicationEmailMixin):
html_signature = soup.find("div", {"class": "ql-editor read-mode"})
_signature = None
if html_signature:
- _signature = html_signature.renderContents()
+ _signature = html_signature.encode_contents()
if (cstr(_signature) or signature) not in self.content:
self.content = f'{self.content}
{signature}'
From e0cad22cddb32052abc6b020117bb587ded4c395 Mon Sep 17 00:00:00 2001
From: Akhil Narang
Date: Mon, 22 Dec 2025 17:46:42 +0530
Subject: [PATCH 05/13] chore(ruff): ignore UP037
We do rely heavily on quoted types for deferred evaluation of type annotations.
We can slowly migrate to `from __future__ import annotations`, don't want to enforce it here right now.
Signed-off-by: Akhil Narang
---
pyproject.toml | 1 +
1 file changed, 1 insertion(+)
diff --git a/pyproject.toml b/pyproject.toml
index 776fcce914..cb2cb2ff26 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -189,6 +189,7 @@ ignore = [
"UP030", # Use implicit references for positional format fields (translations)
"UP031", # Use format specifiers instead of percent format
"UP032", # Use f-string instead of `format` call (translations)
+ "UP037", # quoted annotations
]
typing-modules = ["frappe.types.DF"]
From 8723a2b6ee9dbec800077f18202ba53b0ef553e7 Mon Sep 17 00:00:00 2001
From: Akhil Narang
Date: Mon, 22 Dec 2025 17:48:19 +0530
Subject: [PATCH 06/13] fix: ruff fixes
Signed-off-by: Akhil Narang
---
frappe/__init__.py | 12 +++++-------
frappe/core/doctype/doctype/doctype.py | 4 ++--
frappe/core/doctype/file/utils.py | 4 ++--
frappe/core/doctype/permission_log/permission_log.py | 6 +++---
frappe/core/doctype/rq_worker/rq_worker.py | 3 +--
frappe/deferred_insert.py | 4 ++--
frappe/desk/page/backups/backups.py | 2 +-
frappe/desk/search.py | 7 ++++---
frappe/email/__init__.py | 2 +-
frappe/email/receive.py | 2 +-
.../integrations/doctype/token_cache/token_cache.py | 4 ++--
frappe/model/document.py | 11 +++++------
frappe/model/naming.py | 4 ++--
frappe/model/workflow.py | 2 +-
frappe/modules/utils.py | 4 +---
frappe/monitor.py | 6 +++---
frappe/pulse/utils.py | 4 ++--
frappe/sessions.py | 4 ++--
frappe/tests/test_auth.py | 2 +-
frappe/tests/test_utils.py | 6 +++---
frappe/tests/utils/generators.py | 8 +++-----
frappe/types/filter.py | 5 ++---
frappe/types/frappedict.py | 4 ++--
frappe/utils/__init__.py | 2 +-
frappe/utils/data.py | 12 ++++++------
frappe/utils/pdf_generator/cdp_connection.py | 2 +-
frappe/utils/scheduler.py | 2 +-
.../website/doctype/website_theme/website_theme.py | 2 +-
frappe/www/printview.py | 10 +++++-----
29 files changed, 66 insertions(+), 74 deletions(-)
diff --git a/frappe/__init__.py b/frappe/__init__.py
index 7c20ca44b4..bdcb7d3fa9 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -73,8 +73,8 @@ if TYPE_CHECKING: # pragma: no cover
controllers: dict[str, type] = {}
lazy_controllers: dict[str, type] = {}
local = Local()
-cache: Optional["RedisWrapper"] = None
-client_cache: Optional["ClientCache"] = None
+cache: "RedisWrapper" | None = None
+client_cache: "ClientCache" | None = None
STANDARD_USERS = ("Guest", "Administrator")
# this global may be subsequently changed by frappe.tests.utils.toggle_test_mode()
@@ -100,10 +100,8 @@ ResponseDict: TypeAlias = _dict[str, Any] # type: ignore[no-any-explicit]
FlagsDict: TypeAlias = _dict[str, Any] # type: ignore[no-any-explicit]
FormDict: TypeAlias = _dict[str, str]
-db: LocalProxy[Union["PyMariaDBDatabase", "MariaDBDatabase", "PostgresDatabase", "SQLiteDatabase"]] = local(
- "db"
-)
-qb: LocalProxy[Union["MariaDB", "Postgres", "SQLite"]] = local("qb")
+db: LocalProxy["PyMariaDBDatabase" | "MariaDBDatabase" | "PostgresDatabase" | "SQLiteDatabase"] = local("db")
+qb: LocalProxy["MariaDB" | "Postgres" | "SQLite"] = local("qb")
conf: LocalProxy[ConfType] = local("conf")
form_dict: LocalProxy[FormDict] = local("form_dict")
form = form_dict
@@ -675,7 +673,7 @@ def is_table(doctype: str) -> bool:
def get_precision(
- doctype: str, fieldname: str, currency: str | None = None, doc: Optional["Document"] = None
+ doctype: str, fieldname: str, currency: str | None = None, doc: "Document" | None = None
) -> int:
"""Get precision for a given field"""
from frappe.model.meta import get_field_precision
diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py
index 54f798a6c4..22f9620ba6 100644
--- a/frappe/core/doctype/doctype/doctype.py
+++ b/frappe/core/doctype/doctype/doctype.py
@@ -1140,10 +1140,10 @@ def validate_empty_name(dt, autoname):
frappe.toast(_("Warning: Naming is not set"), indicator="yellow")
-def validate_autoincrement_autoname(dt: Union[DocType, "CustomizeForm"]) -> bool:
+def validate_autoincrement_autoname(dt: DocType | "CustomizeForm") -> bool:
"""Checks if can doctype can change to/from autoincrement autoname"""
- def get_autoname_before_save(dt: Union[DocType, "CustomizeForm"]) -> str:
+ def get_autoname_before_save(dt: DocType | "CustomizeForm") -> str:
if dt.doctype == "Customize Form":
property_value = frappe.db.get_value(
"Property Setter", {"doc_type": dt.doc_type, "property": "autoname"}, "value"
diff --git a/frappe/core/doctype/file/utils.py b/frappe/core/doctype/file/utils.py
index b87671b171..f08ca4cd19 100644
--- a/frappe/core/doctype/file/utils.py
+++ b/frappe/core/doctype/file/utils.py
@@ -54,7 +54,7 @@ def get_extension(
filename,
extn: str | None = None,
content: bytes | None = None,
- response: Optional["Response"] = None,
+ response: "Response" | None = None,
) -> str:
mimetype = None
@@ -426,7 +426,7 @@ def decode_file_content(content: bytes) -> bytes:
return safe_b64decode(content)
-def find_file_by_url(path: str, name: str | None = None) -> Optional["File"]:
+def find_file_by_url(path: str, name: str | None = None) -> "File" | None:
filters = {"file_url": str(path)}
if name:
filters["name"] = str(name)
diff --git a/frappe/core/doctype/permission_log/permission_log.py b/frappe/core/doctype/permission_log/permission_log.py
index ca0e005b9c..18e5fbd0cb 100644
--- a/frappe/core/doctype/permission_log/permission_log.py
+++ b/frappe/core/doctype/permission_log/permission_log.py
@@ -43,9 +43,9 @@ def make_perm_log(doc, method=None):
def insert_perm_log(
doc: Document,
doc_before_save: Document = None,
- for_doctype: Optional["str"] = None,
- for_document: Optional["str"] = None,
- fields: Optional["list | tuple"] = None,
+ for_doctype: "str" | None = None,
+ for_document: "str" | None = None,
+ fields: "list | tuple" | None = None,
):
if frappe.flags.in_install or frappe.flags.in_migrate:
# no need to log changes when migrating or installing app/site
diff --git a/frappe/core/doctype/rq_worker/rq_worker.py b/frappe/core/doctype/rq_worker/rq_worker.py
index e0bed94009..ea3d3d1b72 100644
--- a/frappe/core/doctype/rq_worker/rq_worker.py
+++ b/frappe/core/doctype/rq_worker/rq_worker.py
@@ -109,7 +109,6 @@ def serialize_worker(worker: Worker) -> frappe._dict:
def compute_utilization(worker: Worker) -> float:
with suppress(Exception):
total_time = (
- datetime.datetime.now(datetime.timezone.utc)
- - worker.birth_date.replace(tzinfo=datetime.timezone.utc)
+ datetime.datetime.now(datetime.UTC) - worker.birth_date.replace(tzinfo=datetime.UTC)
).total_seconds()
return worker.total_working_time / total_time * 100
diff --git a/frappe/deferred_insert.py b/frappe/deferred_insert.py
index ced404c896..af4f254923 100644
--- a/frappe/deferred_insert.py
+++ b/frappe/deferred_insert.py
@@ -12,7 +12,7 @@ if TYPE_CHECKING:
queue_prefix = "insert_queue_for_"
-def deferred_insert(doctype: str, records: list[Union[dict, "Document"]] | str):
+def deferred_insert(doctype: str, records: list[dict | "Document"] | str):
if isinstance(records, dict | list):
_records = json.dumps(records)
else:
@@ -48,7 +48,7 @@ def save_to_db():
frappe.db.commit()
-def insert_record(record: Union[dict, "Document"], doctype: str):
+def insert_record(record: dict | "Document", doctype: str):
try:
record.update({"doctype": doctype})
frappe.get_doc(record).insert()
diff --git a/frappe/desk/page/backups/backups.py b/frappe/desk/page/backups/backups.py
index c6cf366f07..3e1c1fb20f 100644
--- a/frappe/desk/page/backups/backups.py
+++ b/frappe/desk/page/backups/backups.py
@@ -10,7 +10,7 @@ from frappe.utils.data import convert_utc_to_system_timezone
def get_time(path: Path):
return convert_utc_to_system_timezone(
- datetime.datetime.fromtimestamp(path.stat().st_mtime, tz=datetime.timezone.utc)
+ datetime.datetime.fromtimestamp(path.stat().st_mtime, tz=datetime.UTC)
).strftime("%a %b %d %H:%M %Y")
diff --git a/frappe/desk/search.py b/frappe/desk/search.py
index 9bd9fc1162..46ed85fec7 100644
--- a/frappe/desk/search.py
+++ b/frappe/desk/search.py
@@ -3,9 +3,10 @@
import json
import re
-from typing import TypedDict
-
-from typing_extensions import NotRequired # not required in 3.11+
+from typing import (
+ NotRequired, # not required in 3.11+
+ TypedDict,
+)
import frappe
diff --git a/frappe/email/__init__.py b/frappe/email/__init__.py
index e584bbac2d..42cdea4743 100644
--- a/frappe/email/__init__.py
+++ b/frappe/email/__init__.py
@@ -148,7 +148,7 @@ def sendmail(
email_read_tracker_url=None,
x_priority: Literal[1, 3, 5] = 3,
email_headers=None,
-) -> Optional["EmailQueue"]:
+) -> "EmailQueue" | None:
"""Send email using user's default **Email Account** or global default **Email Account**.
diff --git a/frappe/email/receive.py b/frappe/email/receive.py
index aeca829199..8d1c64217a 100644
--- a/frappe/email/receive.py
+++ b/frappe/email/receive.py
@@ -403,7 +403,7 @@ class Email:
if self.mail["Date"]:
try:
utc = email.utils.mktime_tz(email.utils.parsedate_tz(self.mail["Date"]))
- utc_dt = datetime.datetime.fromtimestamp(utc, tz=datetime.timezone.utc)
+ utc_dt = datetime.datetime.fromtimestamp(utc, tz=datetime.UTC)
self.date = convert_utc_to_system_timezone(utc_dt).strftime("%Y-%m-%d %H:%M:%S")
except Exception:
self.date = now()
diff --git a/frappe/integrations/doctype/token_cache/token_cache.py b/frappe/integrations/doctype/token_cache/token_cache.py
index 1a12c44008..221ca3175f 100644
--- a/frappe/integrations/doctype/token_cache/token_cache.py
+++ b/frappe/integrations/doctype/token_cache/token_cache.py
@@ -74,8 +74,8 @@ class TokenCache(Document):
def get_expires_in(self):
system_timezone = ZoneInfo(get_system_timezone())
modified: datetime.datetime = get_datetime(self.modified).replace(tzinfo=system_timezone)
- expiry_utc = modified.astimezone(datetime.timezone.utc) + datetime.timedelta(seconds=self.expires_in)
- now_utc = datetime.datetime.now(datetime.timezone.utc)
+ expiry_utc = modified.astimezone(datetime.UTC) + datetime.timedelta(seconds=self.expires_in)
+ now_utc = datetime.datetime.now(datetime.UTC)
return cint((expiry_utc - now_utc).total_seconds())
def is_expired(self):
diff --git a/frappe/model/document.py b/frappe/model/document.py
index 459446ac8c..c2713d2cf7 100644
--- a/frappe/model/document.py
+++ b/frappe/model/document.py
@@ -9,9 +9,8 @@ from collections.abc import Generator, Iterable
from contextlib import contextmanager
from functools import wraps
from types import MappingProxyType
-from typing import TYPE_CHECKING, Any, Literal, Optional, TypeAlias, Union, overload
+from typing import TYPE_CHECKING, Any, Literal, Optional, Self, TypeAlias, Union, overload, override
-from typing_extensions import Self, override
from werkzeug.exceptions import NotFound
import frappe
@@ -34,7 +33,7 @@ from frappe.utils.data import get_absolute_url, get_datetime, get_timedelta, get
from frappe.utils.global_search import update_global_search
if TYPE_CHECKING:
- from typing_extensions import Self
+ from typing import Self
from frappe.core.doctype.docfield.docfield import DocField
@@ -614,7 +613,7 @@ class Document(BaseDocument):
for df in self.meta.get_table_fields():
self.update_child_table(df.fieldname, df)
- def update_child_table(self, fieldname: str, df: Optional["DocField"] = None):
+ def update_child_table(self, fieldname: str, df: "DocField" | None = None):
"""sync child table for given fieldname"""
df: DocField = df or self.meta.get_field(fieldname)
if df.is_virtual:
@@ -1994,7 +1993,7 @@ def bulk_insert(
def _document_values_generator(
documents: Iterable["Document"],
columns: list[str],
-) -> Generator[tuple[Any], None, None]:
+) -> Generator[tuple[Any]]:
for doc in documents:
doc.creation = doc.modified = now()
doc.owner = doc.modified_by = frappe.session.user
@@ -2140,7 +2139,7 @@ def copy_doc(doc: "Document", ignore_no_copy: bool = True) -> "Document":
def new_doc(
doctype: str,
*,
- parent_doc: Optional["Document"] = None,
+ parent_doc: "Document" | None = None,
parentfield: str | None = None,
as_dict: bool = False,
**kwargs,
diff --git a/frappe/model/naming.py b/frappe/model/naming.py
index c5db6241cb..a74f5a5b4d 100644
--- a/frappe/model/naming.py
+++ b/frappe/model/naming.py
@@ -200,7 +200,7 @@ def set_new_name(doc):
doc.name = validate_name(doc.doctype, doc.name)
-def is_autoincremented(doctype: str, meta: Optional["Meta"] = None) -> bool:
+def is_autoincremented(doctype: str, meta: "Meta" | None = None) -> bool:
"""Checks if the doctype has autoincrement autoname set"""
if not meta:
@@ -328,7 +328,7 @@ def _generate_random_string(length=10):
def parse_naming_series(
parts: list[str] | str,
doctype=None,
- doc: Optional["Document"] = None,
+ doc: "Document" | None = None,
number_generator: Callable[[str, int], str] | None = None,
) -> str:
"""Parse the naming series and get next name.
diff --git a/frappe/model/workflow.py b/frappe/model/workflow.py
index ed8f2c2dde..4658d04d40 100644
--- a/frappe/model/workflow.py
+++ b/frappe/model/workflow.py
@@ -40,7 +40,7 @@ def get_workflow_name(doctype):
@frappe.whitelist()
def get_transitions(
- doc: Union["Document", str, dict], workflow: "Workflow" = None, raise_exception: bool = False
+ doc: "Document" | str | dict, workflow: "Workflow" = None, raise_exception: bool = False
) -> list[dict]:
"""Return list of possible transitions for the given doc"""
from frappe.model.document import Document
diff --git a/frappe/modules/utils.py b/frappe/modules/utils.py
index 64aaeeddb5..d7d3824fb0 100644
--- a/frappe/modules/utils.py
+++ b/frappe/modules/utils.py
@@ -301,9 +301,7 @@ def get_app_publisher(module: str) -> str:
return frappe.get_hooks(hook="app_publisher", app_name=app)[0]
-def make_boilerplate(
- template: str, doc: Union["Document", "frappe._dict"], opts: Union[dict, "frappe._dict"] = None
-):
+def make_boilerplate(template: str, doc: "Document" | "frappe._dict", opts: dict | "frappe._dict" = None):
target_path = get_doc_path(doc.module, doc.doctype, doc.name)
template_name = template.replace("controller", scrub(doc.name))
if template_name.endswith("._py"):
diff --git a/frappe/monitor.py b/frappe/monitor.py
index 48e0965e5e..87417e08ad 100644
--- a/frappe/monitor.py
+++ b/frappe/monitor.py
@@ -52,7 +52,7 @@ class Monitor:
self.data = frappe._dict(
{
"site": frappe.local.site,
- "timestamp": datetime.datetime.now(datetime.timezone.utc),
+ "timestamp": datetime.datetime.now(datetime.UTC),
"transaction_type": transaction_type,
"uuid": str(uuid.uuid4()),
}
@@ -85,7 +85,7 @@ class Monitor:
if job := rq.get_current_job():
self.data.job_id = job.id
- waitdiff = self.data.timestamp - job.enqueued_at.replace(tzinfo=datetime.timezone.utc)
+ waitdiff = self.data.timestamp - job.enqueued_at.replace(tzinfo=datetime.UTC)
self.data.job.wait = int(waitdiff.total_seconds() * 1000000)
def add_custom_data(self, **kwargs):
@@ -94,7 +94,7 @@ class Monitor:
def dump(self, response=None):
try:
- timediff = datetime.datetime.now(datetime.timezone.utc) - self.data.timestamp
+ timediff = datetime.datetime.now(datetime.UTC) - self.data.timestamp
# Obtain duration in microseconds
self.data.duration = int(timediff.total_seconds() * 1000000)
diff --git a/frappe/pulse/utils.py b/frappe/pulse/utils.py
index fd68a93a01..a680f1cd51 100644
--- a/frappe/pulse/utils.py
+++ b/frappe/pulse/utils.py
@@ -1,5 +1,5 @@
import hashlib
-from datetime import datetime, timezone
+from datetime import UTC, datetime, timezone
import frappe
@@ -83,7 +83,7 @@ def get_frappe_version() -> str:
def utc_iso() -> str:
- return datetime.now(timezone.utc).isoformat()
+ return datetime.now(UTC).isoformat()
def get_app_version(app_name: str) -> str:
diff --git a/frappe/sessions.py b/frappe/sessions.py
index 5b82d2e816..4e07290f66 100644
--- a/frappe/sessions.py
+++ b/frappe/sessions.py
@@ -8,7 +8,7 @@ permission, homepage, default variables, system defaults etc
"""
import json
-from datetime import datetime, timezone
+from datetime import UTC, datetime, timezone
from urllib.parse import unquote
import redis
@@ -370,7 +370,7 @@ class Session:
if self.time_diff > expiry or (
(session_end := session_data.get("session_end"))
- and datetime.now(tz=timezone.utc) > datetime.fromisoformat(session_end)
+ and datetime.now(tz=UTC) > datetime.fromisoformat(session_end)
):
self._delete_session()
data = None
diff --git a/frappe/tests/test_auth.py b/frappe/tests/test_auth.py
index 9ad7550baf..483d5e3b32 100644
--- a/frappe/tests/test_auth.py
+++ b/frappe/tests/test_auth.py
@@ -165,7 +165,7 @@ class TestAuth(IntegrationTestCase):
client = FrappeClient(self.HOST_NAME, self.test_user_email, self.test_user_password)
expiry_time = next(x for x in client.session.cookies if x.name == "sid").expires
- current_time = datetime.datetime.now(tz=datetime.timezone.utc).timestamp()
+ current_time = datetime.datetime.now(tz=datetime.UTC).timestamp()
self.assertAlmostEqual(get_expiry_in_seconds(), expiry_time - current_time, delta=60 * 60)
diff --git a/frappe/tests/test_utils.py b/frappe/tests/test_utils.py
index e1aada6c2a..a87b7a3793 100644
--- a/frappe/tests/test_utils.py
+++ b/frappe/tests/test_utils.py
@@ -5,7 +5,7 @@ import io
import json
import os
import sys
-from datetime import date, datetime, time, timedelta, timezone
+from datetime import UTC, date, datetime, time, timedelta, timezone
from decimal import ROUND_HALF_UP, Decimal, localcontext
from enum import Enum
from io import StringIO
@@ -961,9 +961,9 @@ class TestResponse(IntegrationTestCase):
minute=23,
second=23,
microsecond=23,
- tzinfo=timezone.utc,
+ tzinfo=UTC,
),
- time(hour=23, minute=23, second=23, microsecond=23, tzinfo=timezone.utc),
+ time(hour=23, minute=23, second=23, microsecond=23, tzinfo=UTC),
timedelta(days=10, hours=12, minutes=120, seconds=10),
],
"float": [
diff --git a/frappe/tests/utils/generators.py b/frappe/tests/utils/generators.py
index e22fc7d2ed..1564aabee2 100644
--- a/frappe/tests/utils/generators.py
+++ b/frappe/tests/utils/generators.py
@@ -137,9 +137,7 @@ def load_test_records_for(index_doctype) -> dict[str, Any]:
# Test record generation
-def _generate_all_records_towards(
- index_doctype, reset=False, commit=False
-) -> Generator[tuple[str, int], None, None]:
+def _generate_all_records_towards(index_doctype, reset=False, commit=False) -> Generator[tuple[str, int]]:
"""Generate test records for the given doctype and its dependencies."""
# NOTE: visited excludes dependency discovery of any index doctype which
@@ -155,7 +153,7 @@ def _generate_all_records_towards(
def _generate_records_for(
index_doctype: str, reset: bool = False, commit: bool = False, initial_doctype: str | None = None
-) -> Generator[tuple[str, "Document"], None, None]:
+) -> Generator[tuple[str, "Document"]]:
"""Create and yield test records for a specific doctype."""
test_module: ModuleType
@@ -204,7 +202,7 @@ test_record_manager_instance = None
def _sync_records(
index_doctype: str, test_records: dict[str, list], reset: bool = False, commit: bool = False
-) -> Generator[tuple[str, "Document"], None, None]:
+) -> Generator[tuple[str, "Document"]]:
"""Generate test objects for a register doctype from provided records, with caching and persistence."""
# NOTE: This method is called in roughly these situations:
# 1. First sync of a index doctype's records
diff --git a/frappe/types/filter.py b/frappe/types/filter.py
index 17f64389ca..b1ea96aa5e 100644
--- a/frappe/types/filter.py
+++ b/frappe/types/filter.py
@@ -4,10 +4,9 @@ from collections.abc import Generator, Iterable, Mapping, Sequence
from datetime import date, datetime
from itertools import groupby
from operator import attrgetter
-from typing import Any, NamedTuple, TypeAlias, TypeGuard, TypeVar, cast
+from typing import Any, NamedTuple, Self, TypeAlias, TypeGuard, TypeVar, cast, override
from pypika import Column
-from typing_extensions import Self, override
Doct: TypeAlias = str
Fld: TypeAlias = str
@@ -254,7 +253,7 @@ class Filters(list[FilterTuple]):
optimized.extend(filters)
else:
- def _values() -> Generator[_Value, None, None]:
+ def _values() -> Generator[_Value]:
for f in filters:
# f.value is already narrowed to Val when we optimize over fully initialized FilterTuple
yield cast(_Value, f.value) # = operator only is allowed to have _Value
diff --git a/frappe/types/frappedict.py b/frappe/types/frappedict.py
index 08c60fd376..518ba5c946 100644
--- a/frappe/types/frappedict.py
+++ b/frappe/types/frappedict.py
@@ -1,12 +1,12 @@
from collections.abc import Iterable, Mapping
from typing import (
TYPE_CHECKING,
+ Self,
TypeVar,
overload,
+ override,
)
-from typing_extensions import Self, override
-
_KT = TypeVar("_KT")
_VT = TypeVar("_VT")
diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py
index a729f1a44e..22ff0d0d80 100644
--- a/frappe/utils/__init__.py
+++ b/frappe/utils/__init__.py
@@ -925,7 +925,7 @@ def get_safe_filters(filters):
return filters
-def create_batch(iterable: Iterable, size: int) -> Generator[Iterable, None, None]:
+def create_batch(iterable: Iterable, size: int) -> Generator[Iterable]:
"""Convert an iterable to multiple batches of constant size of batch_size.
Args:
diff --git a/frappe/utils/data.py b/frappe/utils/data.py
index 9ed2a201ba..33d17c4d7f 100644
--- a/frappe/utils/data.py
+++ b/frappe/utils/data.py
@@ -116,7 +116,7 @@ def is_invalid_date_string(date_string: str) -> bool:
def getdate(
- string_date: Optional["DateTimeLikeObject"] = None, parse_day_first: bool = False
+ string_date: "DateTimeLikeObject" | None = None, parse_day_first: bool = False
) -> datetime.date | None:
"""
Convert string date (yyyy-mm-dd) to datetime.date object.
@@ -148,7 +148,7 @@ def getdate(
def get_datetime(
- datetime_str: Optional["DateTimeLikeObject"] | tuple | list = None,
+ datetime_str: "DateTimeLikeObject" | None | tuple | list = None,
) -> datetime.datetime | None:
"""Return the below mentioned values based on the given `datetime_str`:
@@ -373,7 +373,7 @@ def now_datetime() -> datetime.datetime:
return datetime.datetime.now(ZoneInfo(get_system_timezone())).replace(tzinfo=None)
-def get_timestamp(date: Optional["DateTimeLikeObject"] = None) -> float:
+def get_timestamp(date: "DateTimeLikeObject" | None = None) -> float:
"""Return the Unix timestamp (seconds since Epoch) for the given `date`.
If `date` is None, the current timestamp is returned.
"""
@@ -402,7 +402,7 @@ def convert_utc_to_timezone(utc_timestamp: datetime.datetime, time_zone: str) ->
def get_datetime_in_timezone(time_zone: str) -> datetime.datetime:
"""Return the current datetime in the given timezone (e.g. 'Asia/Kolkata')."""
- utc_timestamp = datetime.datetime.now(datetime.timezone.utc)
+ utc_timestamp = datetime.datetime.now(datetime.UTC)
return convert_utc_to_timezone(utc_timestamp, time_zone)
@@ -2404,7 +2404,7 @@ def to_markdown(html: str) -> str:
pass
-def md_to_html(markdown_text: str) -> Optional["UnicodeWithAttrs"]:
+def md_to_html(markdown_text: str) -> "UnicodeWithAttrs" | None:
"""Convert the given markdown text to HTML and returns it."""
from markdown2 import MarkdownError
from markdown2 import markdown as _markdown
@@ -2424,7 +2424,7 @@ def md_to_html(markdown_text: str) -> Optional["UnicodeWithAttrs"]:
pass
-def markdown(markdown_text: str) -> Optional["UnicodeWithAttrs"]:
+def markdown(markdown_text: str) -> "UnicodeWithAttrs" | None:
"""Convert the given markdown text to HTML and returns it."""
return md_to_html(markdown_text)
diff --git a/frappe/utils/pdf_generator/cdp_connection.py b/frappe/utils/pdf_generator/cdp_connection.py
index b3b06bd6b3..d462633a3d 100644
--- a/frappe/utils/pdf_generator/cdp_connection.py
+++ b/frappe/utils/pdf_generator/cdp_connection.py
@@ -171,7 +171,7 @@ class CDPSocketClient:
event = event[1]
try:
self.loop.run_until_complete(asyncio.wait_for(event, timeout))
- except asyncio.TimeoutError:
+ except TimeoutError:
frappe.log_error(title="Timeout waiting for event", message=f"{frappe.get_traceback()}")
def remove_listener(self, method, event):
diff --git a/frappe/utils/scheduler.py b/frappe/utils/scheduler.py
index 12090d629c..2d8075f8e9 100644
--- a/frappe/utils/scheduler.py
+++ b/frappe/utils/scheduler.py
@@ -84,7 +84,7 @@ def sleep_duration(tick):
# This makes scheduler aligned with real clock,
# so event scheduled at 12:00 happen at 12:00 and not 12:00:35.
minutes = tick // 60
- now = datetime.datetime.now(datetime.timezone.utc)
+ now = datetime.datetime.now(datetime.UTC)
left_minutes = minutes - now.minute % minutes
next_execution = now.replace(second=0) + datetime.timedelta(minutes=left_minutes)
diff --git a/frappe/website/doctype/website_theme/website_theme.py b/frappe/website/doctype/website_theme/website_theme.py
index 2ae69a92ba..ef22d568c8 100644
--- a/frappe/website/doctype/website_theme/website_theme.py
+++ b/frappe/website/doctype/website_theme/website_theme.py
@@ -151,7 +151,7 @@ class WebsiteTheme(Document):
return [{"name": app, "title": values["title"]} for app, values in apps.items()]
-def get_active_theme() -> Optional["WebsiteTheme"]:
+def get_active_theme() -> "WebsiteTheme" | None:
if website_theme := frappe.get_website_settings("website_theme"):
try:
return frappe.client_cache.get_doc("Website Theme", website_theme)
diff --git a/frappe/www/printview.py b/frappe/www/printview.py
index 84ebc1368c..b6b7d9aa8d 100644
--- a/frappe/www/printview.py
+++ b/frappe/www/printview.py
@@ -118,7 +118,7 @@ def get_context(context) -> PrintContext:
}
-def get_print_format_doc(print_format_name: str, meta: "Meta") -> Optional["PrintFormat"]:
+def get_print_format_doc(print_format_name: str, meta: "Meta") -> "PrintFormat" | None:
"""Return print format document."""
if not print_format_name:
print_format_name = frappe.form_dict.format or meta.default_print_format or "Standard"
@@ -135,7 +135,7 @@ def get_print_format_doc(print_format_name: str, meta: "Meta") -> Optional["Prin
def get_rendered_template(
doc: "Document",
- print_format: Optional["PrintFormat"] = None,
+ print_format: "PrintFormat" | None = None,
meta: "Meta" = None,
no_letterhead: bool | None = None,
letterhead: str | None = None,
@@ -281,7 +281,7 @@ def set_link_titles(doc: "Document") -> None:
def set_title_values_for_link_and_dynamic_link_fields(
- meta: "Meta", doc: "Document", parent_doc: Optional["Document"] = None
+ meta: "Meta", doc: "Document", parent_doc: "Document" | None = None
) -> None:
if parent_doc and not parent_doc.get("__link_titles"):
setattr(parent_doc, "__link_titles", {})
@@ -586,7 +586,7 @@ def has_value(df: "DocField", doc: "Document") -> bool:
def get_print_style(
- style: str | None = None, print_format: Optional["PrintFormat"] = None, for_legacy: bool = False
+ style: str | None = None, print_format: "PrintFormat" | None = None, for_legacy: bool = False
) -> str:
print_settings = frappe.get_doc("Print Settings")
@@ -618,7 +618,7 @@ def get_print_style(
def get_font(
- print_settings: "PrintSettings", print_format: Optional["PrintFormat"] = None, for_legacy=False
+ print_settings: "PrintSettings", print_format: "PrintFormat" | None = None, for_legacy=False
) -> str:
default = """
"InterVariable", "Inter", -apple-system", "BlinkMacSystemFont",
From f7feeea0a047dcc30e12b1ce454f468d7185965b Mon Sep 17 00:00:00 2001
From: Akhil Narang
Date: Mon, 22 Dec 2025 17:15:13 +0530
Subject: [PATCH 07/13] fix: ruff "unsafe" fixes
Signed-off-by: Akhil Narang
---
frappe/__init__.py | 14 ++++++-------
frappe/core/doctype/rq_job/test_rq_job.py | 3 +--
frappe/desk/doctype/event/event.py | 2 +-
frappe/model/document.py | 4 ++--
frappe/types/filter.py | 24 +++++++++++------------
frappe/utils/__init__.py | 9 +--------
frappe/utils/background_jobs.py | 5 ++---
frappe/utils/local.py | 2 +-
8 files changed, 27 insertions(+), 36 deletions(-)
diff --git a/frappe/__init__.py b/frappe/__init__.py
index bdcb7d3fa9..a97aad5a2a 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -88,17 +88,17 @@ if _dev_server:
# local-globals
-ConfType: TypeAlias = _dict[str, Any] # type: ignore[no-any-explicit]
+type ConfType = _dict[str, Any] # type: ignore[no-any-explicit]
# TODO: make session a dataclass instead of undtyped _dict
-SessionType: TypeAlias = _dict[str, Any] # type: ignore[no-any-explicit]
+type SessionType = _dict[str, Any] # type: ignore[no-any-explicit]
# TODO: implement dataclass
-LogMessageType: TypeAlias = _dict[str, Any] # type: ignore[no-any-explicit]
+type LogMessageType = _dict[str, Any] # type: ignore[no-any-explicit]
# TODO: implement dataclass
# holds job metadata if the code is run in a background job context
-JobMetaType: TypeAlias = _dict[str, Any] # type: ignore[no-any-explicit]
-ResponseDict: TypeAlias = _dict[str, Any] # type: ignore[no-any-explicit]
-FlagsDict: TypeAlias = _dict[str, Any] # type: ignore[no-any-explicit]
-FormDict: TypeAlias = _dict[str, str]
+type JobMetaType = _dict[str, Any] # type: ignore[no-any-explicit]
+type ResponseDict = _dict[str, Any] # type: ignore[no-any-explicit]
+type FlagsDict = _dict[str, Any] # type: ignore[no-any-explicit]
+type FormDict = _dict[str, str]
db: LocalProxy["PyMariaDBDatabase" | "MariaDBDatabase" | "PostgresDatabase" | "SQLiteDatabase"] = local("db")
qb: LocalProxy["MariaDB" | "Postgres" | "SQLite"] = local("qb")
diff --git a/frappe/core/doctype/rq_job/test_rq_job.py b/frappe/core/doctype/rq_job/test_rq_job.py
index d616526fb1..cac1cb99e6 100644
--- a/frappe/core/doctype/rq_job/test_rq_job.py
+++ b/frappe/core/doctype/rq_job/test_rq_job.py
@@ -180,8 +180,7 @@ class TestRQJob(IntegrationTestCase):
# Observed higher usage on 3.14. Temporarily raising the limit
from sys import version_info
- if version_info >= (3, 14):
- LAST_MEASURED_USAGE += 5
+ LAST_MEASURED_USAGE += 5
self.assertLessEqual(rss, LAST_MEASURED_USAGE * 1.05, msg)
diff --git a/frappe/desk/doctype/event/event.py b/frappe/desk/doctype/event/event.py
index 3b999a3aa8..b1129f3c0c 100644
--- a/frappe/desk/doctype/event/event.py
+++ b/frappe/desk/doctype/event/event.py
@@ -285,7 +285,7 @@ def get_events(
start: date, end: date, user: str | None = None, for_reminder: bool = False, filters=None
) -> list[frappe._dict]:
user = user or frappe.session.user
- EventLikeDict: TypeAlias = Event | frappe._dict
+ type EventLikeDict = Event | frappe._dict
resolved_events: list[EventLikeDict] = []
if isinstance(filters, str):
diff --git a/frappe/model/document.py b/frappe/model/document.py
index c2713d2cf7..a9c4e52c52 100644
--- a/frappe/model/document.py
+++ b/frappe/model/document.py
@@ -42,8 +42,8 @@ DOCUMENT_LOCK_EXPIRY = 3 * 60 * 60 # All locks expire in 3 hours automatically
DOCUMENT_LOCK_SOFT_EXPIRY = 30 * 60 # Let users force-unlock after 30 minutes
-_SingleDocument: TypeAlias = "Document"
-_NewDocument: TypeAlias = "Document"
+type _SingleDocument = "Document"
+type _NewDocument = "Document"
@overload
diff --git a/frappe/types/filter.py b/frappe/types/filter.py
index b1ea96aa5e..1c4b4b9c85 100644
--- a/frappe/types/filter.py
+++ b/frappe/types/filter.py
@@ -8,20 +8,20 @@ from typing import Any, NamedTuple, Self, TypeAlias, TypeGuard, TypeVar, cast, o
from pypika import Column
-Doct: TypeAlias = str
-Fld: TypeAlias = str
-Op: TypeAlias = str
-DateTime: TypeAlias = datetime | date
-_Value: TypeAlias = str | int | float | None | DateTime | Column
-_InputValue: TypeAlias = _Value | bool
-Value: TypeAlias = _Value | Sequence[_Value]
-InputValue: TypeAlias = _InputValue | Sequence[_InputValue]
+type Doct = str
+type Fld = str
+type Op = str
+type DateTime = datetime | date
+type _Value = str | int | float | None | DateTime | Column
+type _InputValue = _Value | bool
+type Value = _Value | Sequence[_Value]
+type InputValue = _InputValue | Sequence[_InputValue]
-FilterTupleSpec: TypeAlias = (
+type FilterTupleSpec = (
tuple[Fld, InputValue] | tuple[Fld, Op, InputValue] | tuple[Doct, Fld, Op, InputValue]
)
-FilterMappingSpec: TypeAlias = Mapping[Fld, _InputValue | tuple[Op, InputValue]]
+type FilterMappingSpec = Mapping[Fld, _InputValue | tuple[Op, InputValue]]
class Sentinel:
@@ -38,7 +38,7 @@ UNSPECIFIED = Sentinel()
T = TypeVar("T")
-def is_unspecified(value: T | Sentinel) -> TypeGuard[Sentinel]:
+def is_unspecified[T](value: T | Sentinel) -> TypeGuard[Sentinel]:
return value is UNSPECIFIED
@@ -280,4 +280,4 @@ class Filters(list[FilterTuple]):
return f"Filters(\n{filters_str}\n)"
-FilterSignature: TypeAlias = Filters | FilterTuple | FilterMappingSpec | FilterTupleSpec
+type FilterSignature = Filters | FilterTuple | FilterMappingSpec | FilterTupleSpec
diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py
index 22ff0d0d80..0533aaaa0d 100644
--- a/frappe/utils/__init__.py
+++ b/frappe/utils/__init__.py
@@ -46,16 +46,9 @@ EMAIL_MATCH_PATTERN = re.compile(
UNSET = object()
-PropertyType: TypeAlias = property | functools.cached_property
+type PropertyType = property | functools.cached_property
-if sys.version_info < (3, 11):
-
- def exception():
- _exc_type, exc_value, _exc_traceback = sys.exc_info()
- return exc_value
-
- sys.exception = exception
def get_fullname(user=None):
diff --git a/frappe/utils/background_jobs.py b/frappe/utils/background_jobs.py
index 9c57ed5ddb..2fe4bcff40 100644
--- a/frappe/utils/background_jobs.py
+++ b/frappe/utils/background_jobs.py
@@ -451,10 +451,9 @@ def start_worker_pool(
if sbool(os.environ.get("FRAPPE_BACKGROUND_WORKERS_NOFORK", False)):
worker_klass = FrappeWorkerNoFork
else:
- if sys.version_info >= (3, 14):
- import multiprocessing
+ import multiprocessing
- multiprocessing.set_start_method("fork", force=True)
+ multiprocessing.set_start_method("fork", force=True)
worker_klass = FrappeWorker
pool = WorkerPool(
diff --git a/frappe/utils/local.py b/frappe/utils/local.py
index 970c29d81a..2093f757c7 100644
--- a/frappe/utils/local.py
+++ b/frappe/utils/local.py
@@ -59,7 +59,7 @@ class Local:
return lp
-class LocalProxy(WerkzeugLocalProxy, Generic[T]):
+class LocalProxy[T](WerkzeugLocalProxy):
__slots__ = ()
def __getattr__(self, name: str) -> Any:
From 90a40dbfe0c286b359e85ce99e0a3f48fe8a1176 Mon Sep 17 00:00:00 2001
From: Akhil Narang
Date: Mon, 22 Dec 2025 17:27:06 +0530
Subject: [PATCH 08/13] fix: ruff manual fixes
Signed-off-by: Akhil Narang
---
frappe/tests/test_api.py | 2 +-
frappe/tests/test_perf.py | 2 +-
frappe/utils/__init__.py | 43 ---------------------------------------
3 files changed, 2 insertions(+), 45 deletions(-)
diff --git a/frappe/tests/test_api.py b/frappe/tests/test_api.py
index 105da8e2dc..c1cdb58cde 100644
--- a/frappe/tests/test_api.py
+++ b/frappe/tests/test_api.py
@@ -432,7 +432,7 @@ def after_request(*args, **kwargs):
_test_REQ_HOOK["after_request"] = time()
-class TestResponse(FrappeAPITestCase):
+class TestAPIResponse(FrappeAPITestCase):
def test_generate_pdf(self):
response = self.get(
"/api/method/frappe.utils.print_format.download_pdf",
diff --git a/frappe/tests/test_perf.py b/frappe/tests/test_perf.py
index bcb4cfa448..25732c8673 100644
--- a/frappe/tests/test_perf.py
+++ b/frappe/tests/test_perf.py
@@ -249,7 +249,7 @@ class TestPerformance(IntegrationTestCase):
default_affinity_16 = list(range(16))
# "linear" siblings = (0,1) (2,3) ...
- linear_siblings_16 = list(itertools.batched(range(16), 2))
+ linear_siblings_16 = list(itertools.batched(range(16), 2, strict=True))
logical_cores = list(range(16))
expected_assignments = [*(l[0] for l in linear_siblings_16), *(l[1] for l in linear_siblings_16)]
for pid, expected_core in zip(logical_cores, expected_assignments, strict=True):
diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py
index 0533aaaa0d..fc86783fe7 100644
--- a/frappe/utils/__init__.py
+++ b/frappe/utils/__init__.py
@@ -49,8 +49,6 @@ UNSET = object()
type PropertyType = property | functools.cached_property
-
-
def get_fullname(user=None):
"""get the full name (first name + last name) of the user from User"""
if not user:
@@ -1191,44 +1189,3 @@ def create_folder(path, with_init=False):
cached_property = functools.cached_property
-if sys.version_info.minor < 12:
- T = TypeVar("T")
-
- class cached_property(functools.cached_property, Generic[T]):
- """
- A simpler `functools.cached_property` implementation without locks.
- This isn't needed in Python 3.12+, since lock was removed in newer versions.
- Hence, in those versions, it returns the `functools.cached_property` object.
-
- This does not prevent a possible race condition in multi-threaded usage.
- The getter function could run more than once on the same instance,
- with the latest run setting the cached value. If the cached property is
- idempotent or otherwise not harmful to run more than once on an instance,
- this is fine. If synchronization is needed, implement the necessary locking
- inside the decorated getter function or around the cached property access.
- """
-
- def __init__(self, func: Callable[[Any], T]):
- self.func = func
- self.attrname = None
- self.__doc__ = func.__doc__
- self.__module__ = func.__module__
-
- def __set_name__(self, owner, name):
- if self.attrname is None:
- self.attrname = name
-
- elif name != self.attrname:
- raise TypeError(
- "Cannot assign the same cached_property to two different names "
- f"({self.attrname!r} and {name!r})."
- )
-
- def __get__(self, instance, owner=None) -> T:
- if instance is None:
- return self
-
- value = self.func(instance)
- instance.__dict__[self.attrname] = value
- return value
-# end: custom cached_property implementation
From 7aec123a29656edc25c99f232ae259bab056b137 Mon Sep 17 00:00:00 2001
From: Akhil Narang
Date: Mon, 22 Dec 2025 17:28:08 +0530
Subject: [PATCH 09/13] chore: drop some version checks
Signed-off-by: Akhil Narang
---
frappe/core/doctype/rq_job/test_rq_job.py | 2 --
frappe/tests/test_perf.py | 4 ++--
2 files changed, 2 insertions(+), 4 deletions(-)
diff --git a/frappe/core/doctype/rq_job/test_rq_job.py b/frappe/core/doctype/rq_job/test_rq_job.py
index cac1cb99e6..5bd94b4c1d 100644
--- a/frappe/core/doctype/rq_job/test_rq_job.py
+++ b/frappe/core/doctype/rq_job/test_rq_job.py
@@ -178,8 +178,6 @@ class TestRQJob(IntegrationTestCase):
LAST_MEASURED_USAGE += 2
# Observed higher usage on 3.14. Temporarily raising the limit
- from sys import version_info
-
LAST_MEASURED_USAGE += 5
self.assertLessEqual(rss, LAST_MEASURED_USAGE * 1.05, msg)
diff --git a/frappe/tests/test_perf.py b/frappe/tests/test_perf.py
index 25732c8673..e733cdb519 100644
--- a/frappe/tests/test_perf.py
+++ b/frappe/tests/test_perf.py
@@ -193,7 +193,7 @@ class TestPerformance(IntegrationTestCase):
"""
query = "select * from tabUser"
- expected_refcount = 1 if sys.version_info >= (3, 14) else 2
+ expected_refcount = 1
for kwargs in ({}, {"as_dict": True}, {"as_list": True}):
result = frappe.db.sql(query, **kwargs)
self.assertEqual(sys.getrefcount(result), expected_refcount) # Note: This always returns +1
@@ -201,7 +201,7 @@ class TestPerformance(IntegrationTestCase):
def test_no_cyclic_references(self):
doc = frappe.get_doc("User", "Administrator")
- expected_refcount = 1 if sys.version_info >= (3, 14) else 2
+ expected_refcount = 1
self.assertEqual(sys.getrefcount(doc), expected_refcount) # Note: This always returns +1
def test_get_doc_cache_calls(self):
From e859b1d3124fa07593785c0157f1805beb109125 Mon Sep 17 00:00:00 2001
From: Akhil Narang
Date: Mon, 22 Dec 2025 19:00:38 +0530
Subject: [PATCH 10/13] fix: disable UP040, revert some instances to TypeAlias
Signed-off-by: Akhil Narang
---
frappe/model/workflow.py | 7 +++++--
frappe/types/filter.py | 22 ++++++++++------------
frappe/utils/__init__.py | 2 +-
pyproject.toml | 1 +
4 files changed, 17 insertions(+), 15 deletions(-)
diff --git a/frappe/model/workflow.py b/frappe/model/workflow.py
index 4658d04d40..d089f75a62 100644
--- a/frappe/model/workflow.py
+++ b/frappe/model/workflow.py
@@ -1,8 +1,11 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
+
+from __future__ import annotations
+
import json
from collections import defaultdict
-from typing import TYPE_CHECKING, Union
+from typing import TYPE_CHECKING
import frappe
from frappe import _
@@ -40,7 +43,7 @@ def get_workflow_name(doctype):
@frappe.whitelist()
def get_transitions(
- doc: "Document" | str | dict, workflow: "Workflow" = None, raise_exception: bool = False
+ doc: Document | str | dict, workflow: Workflow = None, raise_exception: bool = False
) -> list[dict]:
"""Return list of possible transitions for the given doc"""
from frappe.model.document import Document
diff --git a/frappe/types/filter.py b/frappe/types/filter.py
index 1c4b4b9c85..acf9369322 100644
--- a/frappe/types/filter.py
+++ b/frappe/types/filter.py
@@ -8,20 +8,20 @@ from typing import Any, NamedTuple, Self, TypeAlias, TypeGuard, TypeVar, cast, o
from pypika import Column
-type Doct = str
-type Fld = str
-type Op = str
-type DateTime = datetime | date
-type _Value = str | int | float | None | DateTime | Column
-type _InputValue = _Value | bool
-type Value = _Value | Sequence[_Value]
-type InputValue = _InputValue | Sequence[_InputValue]
+Doct: TypeAlias = str
+Fld: TypeAlias = str
+Op: TypeAlias = str
+DateTime: TypeAlias = datetime | date
+_Value: TypeAlias = str | int | float | None | DateTime | Column
+_InputValue: TypeAlias = _Value | bool
+Value: TypeAlias = _Value | Sequence[_Value]
+InputValue: TypeAlias = _InputValue | Sequence[_InputValue]
-type FilterTupleSpec = (
+FilterTupleSpec: TypeAlias = (
tuple[Fld, InputValue] | tuple[Fld, Op, InputValue] | tuple[Doct, Fld, Op, InputValue]
)
-type FilterMappingSpec = Mapping[Fld, _InputValue | tuple[Op, InputValue]]
+FilterMappingSpec: TypeAlias = Mapping[Fld, _InputValue | tuple[Op, InputValue]]
class Sentinel:
@@ -35,8 +35,6 @@ class Sentinel:
UNSPECIFIED = Sentinel()
-T = TypeVar("T")
-
def is_unspecified[T](value: T | Sentinel) -> TypeGuard[Sentinel]:
return value is UNSPECIFIED
diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py
index fc86783fe7..b9d444a94d 100644
--- a/frappe/utils/__init__.py
+++ b/frappe/utils/__init__.py
@@ -46,7 +46,7 @@ EMAIL_MATCH_PATTERN = re.compile(
UNSET = object()
-type PropertyType = property | functools.cached_property
+PropertyType: TypeAlias = property | functools.cached_property
def get_fullname(user=None):
diff --git a/pyproject.toml b/pyproject.toml
index cb2cb2ff26..4b202ff483 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -190,6 +190,7 @@ ignore = [
"UP031", # Use format specifiers instead of percent format
"UP032", # Use f-string instead of `format` call (translations)
"UP037", # quoted annotations
+ "UP040", # Use type aliases instead of type annotations
]
typing-modules = ["frappe.types.DF"]
From d3c4bb7750491ec895c05e78011d92430294f001 Mon Sep 17 00:00:00 2001
From: Akhil Narang
Date: Tue, 23 Dec 2025 12:49:33 +0530
Subject: [PATCH 11/13] chore: update list of revisions for git blame to ignore
Signed-off-by: Akhil Narang
---
.git-blame-ignore-revs | 3 +++
1 file changed, 3 insertions(+)
diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs
index 391bf41308..99eb746a47 100644
--- a/.git-blame-ignore-revs
+++ b/.git-blame-ignore-revs
@@ -64,3 +64,6 @@ e9bbe03354079cfcef65a77b0c33f57b047a7c93
# another ruff update
6ca4d4d167a1a009d99062747711de7a994aa633
+
+# some more ruff
+8723a2b6ee9dbec800077f18202ba53b0ef553e7
From 47f1c676f9ca6ecad15d5f9e95df686e18e8905d Mon Sep 17 00:00:00 2001
From: Akhil Narang
Date: Tue, 23 Dec 2025 13:27:43 +0530
Subject: [PATCH 12/13] chore: update actions
Signed-off-by: Akhil Narang
---
.github/actions/setup/action.yml | 8 ++++----
.github/workflows/_base-migration.yml | 2 +-
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml
index a48cb5778b..4ae0e9f6da 100644
--- a/.github/actions/setup/action.yml
+++ b/.github/actions/setup/action.yml
@@ -45,12 +45,12 @@ runs:
git config --global advice.detachedHead false
- name: Clone
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
with:
path: apps/${{ github.event.repository.name }}
- name: Setup Python
- uses: actions/setup-python@v5
+ uses: actions/setup-python@v6
with:
python-version: ${{ inputs.python-version }}
@@ -64,14 +64,14 @@ runs:
fi
- name: Checkout Frappe
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
with:
repository: ${{ env.FRAPPE_GH_ORG || github.repository_owner }}/frappe
ref: ${{ github.event.client_payload.frappe_sha || github.base_ref || github.ref_name }}
path: apps/frappe
if: github.event.repository.name != 'frappe'
- - uses: actions/setup-node@v4
+ - uses: actions/setup-node@v6
with:
node-version: ${{ inputs.node-version }}
check-latest: true
diff --git a/.github/workflows/_base-migration.yml b/.github/workflows/_base-migration.yml
index cc9a529c87..ccabf8f058 100644
--- a/.github/workflows/_base-migration.yml
+++ b/.github/workflows/_base-migration.yml
@@ -51,7 +51,7 @@ jobs:
db-root-password: ${{ env.DB_ROOT_PASSWORD }}
- name: Setup Python
- uses: actions/setup-python@v5
+ uses: actions/setup-python@v6
with:
python-version: |
3.11
From 0ac52c1bb4f7c646053c0acff1411adb5a6857a1 Mon Sep 17 00:00:00 2001
From: Akhil Narang
Date: Tue, 23 Dec 2025 13:27:48 +0530
Subject: [PATCH 13/13] chore: update boilerplate
Signed-off-by: Akhil Narang
---
frappe/utils/boilerplate.py | 40 ++++++++++++++++++-------------------
1 file changed, 20 insertions(+), 20 deletions(-)
diff --git a/frappe/utils/boilerplate.py b/frappe/utils/boilerplate.py
index f00207f63b..31fa50774c 100644
--- a/frappe/utils/boilerplate.py
+++ b/frappe/utils/boilerplate.py
@@ -341,11 +341,11 @@ authors = [
{{ name = "{app_publisher}", email = "{app_email}"}}
]
description = "{app_description}"
-requires-python = ">=3.10"
+requires-python = ">=3.14"
readme = "README.md"
dynamic = ["version"]
dependencies = [
- # "frappe~=15.0.0" # Installed and managed by bench.
+ # "frappe~=16.0.0" # Installed and managed by bench.
]
[build-system]
@@ -358,7 +358,7 @@ build-backend = "flit_core.buildapi"
[tool.ruff]
line-length = 110
-target-version = "py310"
+target-version = "py314"
[tool.ruff.lint]
select = [
@@ -387,6 +387,8 @@ ignore = [
"UP030", # Use implicit references for positional format fields (translations)
"UP031", # Use format specifiers instead of percent format
"UP032", # Use f-string instead of `format` call (translations)
+ "UP037", # quoted annotations
+ "UP040", # Use type aliases instead of type annotations
]
typing-modules = ["frappe.types.DF"]
@@ -736,7 +738,7 @@ jobs:
ports:
- 11000:6379
mariadb:
- image: mariadb:10.6
+ image: mariadb:11.8
env:
MYSQL_ROOT_PASSWORD: root
ports:
@@ -745,7 +747,7 @@ jobs:
steps:
- name: Clone
- uses: actions/checkout@v3
+ uses: actions/checkout@v6
- name: Find tests
run: |
@@ -753,14 +755,14 @@ jobs:
grep -rn "def test" > /dev/null
- name: Setup Python
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v6
with:
- python-version: '3.10'
+ python-version: '3.14'
- name: Setup Node
- uses: actions/setup-node@v3
+ uses: actions/setup-node@v6
with:
- node-version: 18
+ node-version: 24
check-latest: true
- name: Cache pip
@@ -793,8 +795,6 @@ jobs:
run: |
pip install frappe-bench
bench init --skip-redis-config-generation --skip-assets --python "$(which python)" ~/frappe-bench
- mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL character_set_server = 'utf8mb4'"
- mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"
- name: Install
working-directory: /home/runner/frappe-bench
@@ -831,7 +831,7 @@ fail_fast: false
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v5.0.0
+ rev: v6.0.0
hooks:
- id: trailing-whitespace
files: "{app_name}.*"
@@ -844,7 +844,7 @@ repos:
- id: debug-statements
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.8.1
+ rev: v0.14.10
hooks:
- id: ruff
name: "Run ruff import sorter"
@@ -915,10 +915,10 @@ jobs:
if: github.event_name == 'pull_request'
steps:
- - uses: actions/checkout@v4
- - uses: actions/setup-python@v5
+ - uses: actions/checkout@v6
+ - uses: actions/setup-python@v6
with:
- python-version: '3.10'
+ python-version: '3.14'
cache: pip
- uses: pre-commit/action@v3.0.0
@@ -935,14 +935,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/setup-python@v5
+ - uses: actions/setup-python@v6
with:
- python-version: '3.10'
+ python-version: '3.14'
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v6
- name: Cache pip
- uses: actions/cache@v4
+ uses: actions/cache@v5
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py') }}