diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index 7a13aa22..4d23bede 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -32,7 +32,7 @@ globalThis.socket = io("https://127.0.0.1:8001", { ## Install the xlwings Python package from its repo into xlwings-server in editable mode - xlwings: delete `project.toml` temporarily -- xlwings: replace `version = "dev"` with the version from `xlwings.js` +- xlwings: replace `version = "dev"` with `0.0.0` - xlwings-server: `pip uninstall xlwings` - xlwings-server: `cd ~/dev/xlwings && python setup.py develop` - xlwings: undo deletion of `project.toml` diff --git a/app/custom_functions/examples.py b/app/custom_functions/examples.py index 5abe8ae4..993d5a7f 100644 --- a/app/custom_functions/examples.py +++ b/app/custom_functions/examples.py @@ -4,6 +4,7 @@ """ import asyncio +import datetime as dt import sys from pathlib import Path from typing import Annotated @@ -35,18 +36,20 @@ def hello(name): @func @arg("rows", doc="The number of rows in the returned array.") @arg("cols", doc="The number of columns in the returned array.") -@ret(index=False) def standard_normal(rows, cols): """Returns an array of standard normally distributed pseudo random numbers""" rng = np.random.default_rng() matrix = rng.standard_normal(size=(rows, cols)) - df = pd.DataFrame(matrix, columns=[f"col{i+1}" for i in range(matrix.shape[1])]) + date_rng = pd.date_range(start=dt.datetime(2025, 6, 15), periods=rows, freq="D") + df = pd.DataFrame( + matrix, columns=[f"col{i+1}" for i in range(matrix.shape[1])], index=date_rng + ) return df # 3) Reading a pandas DataFrames @func -@arg("df", pd.DataFrame, index=False) +@arg("df", pd.DataFrame) def correl(df): """Like CORREL, but it works on whole matrices instead of just 2 arrays.""" return df.corr() @@ -55,7 +58,7 @@ def correl(df): # 4) Type hints: this is the same example as 3), but using type hints instead of # decorators. You could also use type hints and decorators together. In this sample, we # are storing the Annotated type hint outside of the function, so it is easy to reuse. -Df = Annotated[pd.DataFrame, {"index": False}] +Df = Annotated[pd.DataFrame, {"index": True}] @func diff --git a/app/custom_scripts/examples.py b/app/custom_scripts/examples.py index 63287ae2..bd0753f9 100644 --- a/app/custom_scripts/examples.py +++ b/app/custom_scripts/examples.py @@ -36,7 +36,7 @@ def show_alert(book: xw.Book): def setup_custom_functions(book: xw.Book): prefix = f"{settings.functions_namespace}" if settings.environment != "prod": - prefix += f"_{settings.environment}" + prefix += f"_{settings.environment}".upper() sheet = book.sheets.add() sheet["A3"].value = f'={prefix}.HELLO("xlwings")' sheet["A5"].value = f"={prefix}.STANDARD_NORMAL(3, 4)" diff --git a/app/static/js/core/custom-functions-code.js b/app/static/js/core/custom-functions-code.js index 53ada27a..ee7bf629 100644 --- a/app/static/js/core/custom-functions-code.js +++ b/app/static/js/core/custom-functions-code.js @@ -2,7 +2,6 @@ const debug = false; let invocations = new Set(); let bodies = new Set(); let runtime; -let contentLanguage; let socket = null; Office.onReady(function (info) { @@ -47,9 +46,6 @@ Office.onReady(function (info) { } else { runtime = "1.1"; } - - // Content Language - contentLanguage = Office.context.contentLanguage; }); function flattenVarargsArray(arr) { @@ -166,7 +162,8 @@ async function base() { func_name: funcName, args: args, caller_address: `${officeApiClient}[${workbookName}]${invocation.address}`, // not available for streaming functions - content_language: contentLanguage, + culture_info_name: await xlwings.getCultureInfoName(), + date_format: await xlwings.getDateFormat(), version: "placeholder_xlwings_version", runtime: runtime, }; diff --git a/app/static/js/core/xlwingsjs/utils.js b/app/static/js/core/xlwingsjs/utils.js index 3f2b901d..22db6623 100644 --- a/app/static/js/core/xlwingsjs/utils.js +++ b/app/static/js/core/xlwingsjs/utils.js @@ -12,13 +12,49 @@ export async function getActiveBookName() { } } +// Culture Info Name, e.g., en-DE +let cachedCultureInfoName = null; +export async function getCultureInfoName() { + if (cachedCultureInfoName) { + return cachedCultureInfoName; + } + if (!Office.context.requirements.isSetSupported("ExcelApi", "1.12")) { + return null; + } + const context = new Excel.RequestContext(); + context.application.cultureInfo.load(["name"]); + await context.sync(); + cachedCultureInfoName = `${context.application.cultureInfo.name}`; + return cachedCultureInfoName; +} + +// Date format +let cachedDateFormat = null; +export async function getDateFormat() { + if (cachedDateFormat) { + return cachedDateFormat; + } + if (!Office.context.requirements.isSetSupported("ExcelApi", "1.12")) { + return null; + } + const context = new Excel.RequestContext(); + context.application.cultureInfo.datetimeFormat.load(["shortDatePattern"]); + await context.sync(); + cachedDateFormat = `${context.application.cultureInfo.datetimeFormat.shortDatePattern}`; + return cachedDateFormat; +} + export function printSupportedApiVersions() { const versions = [...Array(30)].map((_, i) => `1.${i}`); - function printBuildInfo() { + async function printBuildInfo() { if (Office.context.diagnostics) { console.log(`Office Build: ${Office.context.diagnostics.version}`); console.log(`Office Platform: ${Office.context.diagnostics.platform}`); + console.log( + `Culture Info Name: ${(await getCultureInfoName()) || "N/A"}`, + ); + console.log(`Local Date Format: ${(await getDateFormat()) || "N/A"}`); } } @@ -40,8 +76,8 @@ export function printSupportedApiVersions() { } } - Office.onReady(() => { - printBuildInfo(); + Office.onReady(async () => { + await printBuildInfo(); const apiNames = [ "ExcelAPI", "SharedRuntime", diff --git a/app/static/js/core/xlwingsjs/xlwings.js b/app/static/js/core/xlwingsjs/xlwings.js index 2566756a..499248a4 100644 --- a/app/static/js/core/xlwingsjs/xlwings.js +++ b/app/static/js/core/xlwingsjs/xlwings.js @@ -1,8 +1,13 @@ import { xlAlert } from "./alert.js"; import { getAccessToken } from "./auth.js"; export { getAccessToken }; -import { getActiveBookName, printSupportedApiVersions } from "./utils.js"; -export { getActiveBookName }; +import { + getActiveBookName, + printSupportedApiVersions, + getCultureInfoName, + getDateFormat, +} from "./utils.js"; +export { getActiveBookName, getCultureInfoName, getDateFormat }; // Prints the supported API versions into the Console printSupportedApiVersions(); @@ -37,6 +42,8 @@ const xlwings = { getBookData, runActions, pyscriptAllDone, + getCultureInfoName, + getDateFormat, }; globalThis.xlwings = xlwings; @@ -593,42 +600,6 @@ Object.assign(globalThis.callbacks, funcs); // Callbacks async function setValues(context, action) { - // Handle DateTime (TODO: backend should deliver indices with datetime obj) - let dt; - let dtString; - action.values.forEach((valueRow, rowIndex) => { - valueRow.forEach((value, colIndex) => { - if ( - typeof value === "string" && - value.length > 18 && - value.includes("T") - ) { - dt = new Date(Date.parse(value)); - // Excel on macOS does use the wrong locale if you set a custom one via - // macOS Settings > Date & Time > Open Language & Region > Apps - // as the date format seems to stick to the Region selected under General - // while toLocaleDateString then respects the specific selected language. - // Providing Office.context.contentLanguage fixes this but isn't available for - // Office Scripts - // https://learn.microsoft.com/en-us/office/dev/add-ins/develop/localization#match-datetime-format-with-client-locale - dtString = dt.toLocaleDateString(Office.context.contentLanguage); - // Note that adding the time will format the cell as Custom instead of Date/Time - // which xlwings currently doesn't translate to datetime when reading - if (dtString !== "Invalid Date") { - if ( - dt.getHours() + - dt.getMinutes() + - dt.getSeconds() + - dt.getMilliseconds() !== - 0 - ) { - dtString += " " + dt.toLocaleTimeString(); - } - action.values[rowIndex][colIndex] = dtString; - } - } - }); - }); let range = await getRange(context, action); range.values = action.values; await context.sync(); diff --git a/docs/custom_functions.md b/docs/custom_functions.md index 664ece06..2d02dfea 100644 --- a/docs/custom_functions.md +++ b/docs/custom_functions.md @@ -259,7 +259,7 @@ def pytoday(): return dt.date.today() ``` -By default, it will format the date according to the content language of your Excel instance, but you can also override this by explicitly providing the `date_format` option: +By default, it will format the date according to the cultural info of your Excel instance, but you can also override this by explicitly providing the `date_format` option or the `XLWINGS_DATE_FORMAT` environment variable: ```python import datetime as dt diff --git a/requirements-core.txt b/requirements-core.txt index 0fa03bda..ae98c02d 100644 --- a/requirements-core.txt +++ b/requirements-core.txt @@ -4,7 +4,7 @@ aiocache==0.12.3 # via -r requirements-core.in annotated-types==0.7.0 # via pydantic -anyio==4.7.0 +anyio==4.8.0 # via # httpx # starlette @@ -61,7 +61,7 @@ packaging==24.2 ; sys_platform != 'win32' # via gunicorn pycparser==2.22 ; platform_python_implementation != 'PyPy' # via cffi -pydantic==2.10.4 +pydantic==2.10.5 # via # fastapi-slim # pydantic-settings @@ -103,7 +103,7 @@ uvloop==0.21.0 ; sys_platform != 'win32' # via -r requirements-core.in wsproto==1.2.0 # via simple-websocket -xlwings==0.33.5 +xlwings==0.33.6 # via -r requirements-core.in # The following packages were excluded from the output: diff --git a/requirements-dev.txt b/requirements-dev.txt index 6a008ae6..4edcc068 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -6,7 +6,7 @@ annotated-types==0.7.0 # via # -r requirements.txt # pydantic -anyio==4.7.0 +anyio==4.8.0 # via # -r requirements.txt # httpx @@ -70,7 +70,7 @@ httptools==0.6.4 # via -r requirements.txt httpx==0.28.1 # via -r requirements.txt -identify==2.6.4 +identify==2.6.5 # via pre-commit idna==3.10 # via @@ -118,7 +118,7 @@ pycparser==2.22 ; platform_python_implementation != 'PyPy' # via # -r requirements.txt # cffi -pydantic==2.10.4 +pydantic==2.10.5 # via # -r requirements.txt # fastapi-slim @@ -194,7 +194,7 @@ uvicorn==0.34.0 # via -r requirements.txt uvloop==0.21.0 ; sys_platform != 'win32' # via -r requirements.txt -virtualenv==20.28.0 +virtualenv==20.28.1 # via pre-commit watchfiles==1.0.3 # via -r requirements-dev.in @@ -202,7 +202,7 @@ wsproto==1.2.0 # via # -r requirements.txt # simple-websocket -xlwings==0.33.5 +xlwings==0.33.6 # via -r requirements.txt # The following packages were excluded from the output: diff --git a/requirements.txt b/requirements.txt index 0cca4237..973ea6ea 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ annotated-types==0.7.0 # via # -r requirements-core.txt # pydantic -anyio==4.7.0 +anyio==4.8.0 # via # -r requirements-core.txt # httpx @@ -95,7 +95,7 @@ pycparser==2.22 ; platform_python_implementation != 'PyPy' # via # -r requirements-core.txt # cffi -pydantic==2.10.4 +pydantic==2.10.5 # via # -r requirements-core.txt # fastapi-slim @@ -163,7 +163,7 @@ wsproto==1.2.0 # via # -r requirements-core.txt # simple-websocket -xlwings==0.33.5 +xlwings==0.33.6 # via -r requirements-core.txt # The following packages were excluded from the output: