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 diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index 6122a0e5df..4ae0e9f6da 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' @@ -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 a1838c27fe..ccabf8f058 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@v6 + 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/.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/frappe/__init__.py b/frappe/__init__.py index 7c20ca44b4..a97aad5a2a 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() @@ -88,22 +88,20 @@ 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[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/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}' 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_job/test_rq_job.py b/frappe/core/doctype/rq_job/test_rq_job.py index d616526fb1..5bd94b4c1d 100644 --- a/frappe/core/doctype/rq_job/test_rq_job.py +++ b/frappe/core/doctype/rq_job/test_rq_job.py @@ -178,10 +178,7 @@ class TestRQJob(IntegrationTestCase): LAST_MEASURED_USAGE += 2 # 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/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/doctype/event/event.py b/frappe/desk/doctype/event/event.py index 37c10b3477..49db0596d1 100644 --- a/frappe/desk/doctype/event/event.py +++ b/frappe/desk/doctype/event/event.py @@ -335,7 +335,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/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..a9c4e52c52 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 @@ -43,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 @@ -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..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: 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/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/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_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_perf.py b/frappe/tests/test_perf.py index bcb4cfa448..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): @@ -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/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 93b72edc2d..1564aabee2 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 {} @@ -138,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 @@ -156,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 @@ -205,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..acf9369322 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 @@ -36,10 +35,8 @@ class Sentinel: 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 @@ -254,7 +251,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 @@ -281,4 +278,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/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..b9d444a94d 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -49,15 +49,6 @@ UNSET = object() PropertyType: TypeAlias = 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): """get the full name (first name + last name) of the user from User""" if not user: @@ -925,7 +916,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: @@ -1198,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 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/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') }} 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/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/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: 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/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", {}) 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", 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..4b202ff483 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,85 +4,83 @@ 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 = [ # 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,14 +150,14 @@ 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" [tool.ruff] line-length = 110 -target-version = "py310" +target-version = "py314" exclude = [ "**/doctype/*/boilerplate/*.py" # boilerplate are template strings, not valid python ] @@ -192,6 +189,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"]