Merge pull request #35387 from akhilnarang/dependency-update
build(deps)!: only allow python>=3.14, nodejs>=24
This commit is contained in:
commit
84d346f683
54 changed files with 191 additions and 244 deletions
|
|
@ -64,3 +64,6 @@ e9bbe03354079cfcef65a77b0c33f57b047a7c93
|
|||
|
||||
# another ruff update
|
||||
6ca4d4d167a1a009d99062747711de7a994aa633
|
||||
|
||||
# some more ruff
|
||||
8723a2b6ee9dbec800077f18202ba53b0ef553e7
|
||||
|
|
|
|||
12
.github/actions/setup/action.yml
vendored
12
.github/actions/setup/action.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
19
.github/workflows/_base-migration.yml
vendored
19
.github/workflows/_base-migration.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
2
.github/workflows/_base-server-tests.yml
vendored
2
.github/workflows/_base-server-tests.yml
vendored
|
|
@ -13,7 +13,7 @@ on:
|
|||
node-version:
|
||||
required: false
|
||||
type: number
|
||||
default: 22
|
||||
default: 24
|
||||
parallel-runs:
|
||||
required: false
|
||||
type: number
|
||||
|
|
|
|||
2
.github/workflows/_base-ui-tests.yml
vendored
2
.github/workflows/_base-ui-tests.yml
vendored
|
|
@ -13,7 +13,7 @@ on:
|
|||
node-version:
|
||||
required: false
|
||||
type: number
|
||||
default: 22
|
||||
default: 24
|
||||
parallel-runs:
|
||||
required: false
|
||||
type: number
|
||||
|
|
|
|||
2
.github/workflows/create-release.yml
vendored
2
.github/workflows/create-release.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
2
.github/workflows/linters.yml
vendored
2
.github/workflows/linters.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
2
.github/workflows/on_release.yml
vendored
2
.github/workflows/on_release.yml
vendored
|
|
@ -22,7 +22,7 @@ jobs:
|
|||
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 24
|
||||
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
|
|
|
|||
2
.github/workflows/publish-assets-develop.yml
vendored
2
.github/workflows/publish-assets-develop.yml
vendored
|
|
@ -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'
|
||||
|
|
|
|||
2
.github/workflows/run-indinvidual-tests.yml
vendored
2
.github/workflows/run-indinvidual-tests.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
4
.github/workflows/server-tests.yml
vendored
4
.github/workflows/server-tests.yml
vendored
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}</p><br><p class="signature">{signature}'
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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**.
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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') }}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ class Local:
|
|||
return lp
|
||||
|
||||
|
||||
class LocalProxy(WerkzeugLocalProxy, Generic[T]):
|
||||
class LocalProxy[T](WerkzeugLocalProxy):
|
||||
__slots__ = ()
|
||||
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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", {})
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
"url": "https://github.com/frappe/frappe/issues"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
"node": ">=24"
|
||||
},
|
||||
"homepage": "https://frappeframework.com",
|
||||
"dependencies": {
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue