From 052e24ef0e80a97b8e8ea9d0d35748993a2c124a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Thu, 11 Sep 2025 11:28:12 +0200 Subject: [PATCH] refactor: Replace Pyodide with native Python with env flag is enabled (#19403) --- .../@n8n/config/src/configs/runners.config.ts | 15 +++++++++++++ packages/@n8n/config/test/config.test.ts | 1 + packages/cli/src/services/frontend.service.ts | 3 ++- .../src/task-runners/task-runner-module.ts | 2 +- .../composables/useActionsGeneration.ts | 4 ++-- .../src/components/ParameterInput.vue | 10 ++++++--- packages/nodes-base/nodes/Code/Code.node.ts | 21 ++++++++++++------- 7 files changed, 42 insertions(+), 14 deletions(-) diff --git a/packages/@n8n/config/src/configs/runners.config.ts b/packages/@n8n/config/src/configs/runners.config.ts index 7c1390ee8d..124244b5f8 100644 --- a/packages/@n8n/config/src/configs/runners.config.ts +++ b/packages/@n8n/config/src/configs/runners.config.ts @@ -69,4 +69,19 @@ export class TaskRunnersConfig { */ @Env('N8N_RUNNERS_INSECURE_MODE') insecureMode: boolean = false; + + /** + * Whether to enable the Python task runner (beta). This will replace the + * Pyodide option with the native Python option in the Code node. Expects a + * Python task runner to be available, typically in a sidecar container. + * + * Actions required: + * - Any Code node set to the legacy `python` parameter will need to be manually + * updated to use the new `pythonNative` parameter. + * - Any Code node script relying on Pyodide syntax is likely to need to be manually + * adjusted to account for breaking changes: + * https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.code/#python-native-beta + */ + @Env('N8N_NATIVE_PYTHON_RUNNER') + isNativePythonRunnerEnabled: boolean = false; } diff --git a/packages/@n8n/config/test/config.test.ts b/packages/@n8n/config/test/config.test.ts index 7b43c01d16..eca4ef00bb 100644 --- a/packages/@n8n/config/test/config.test.ts +++ b/packages/@n8n/config/test/config.test.ts @@ -260,6 +260,7 @@ describe('GlobalConfig', () => { taskTimeout: 300, heartbeatInterval: 30, insecureMode: false, + isNativePythonRunnerEnabled: false, }, sentry: { backendDsn: '', diff --git a/packages/cli/src/services/frontend.service.ts b/packages/cli/src/services/frontend.service.ts index 2e21f0ea61..05273c7c0f 100644 --- a/packages/cli/src/services/frontend.service.ts +++ b/packages/cli/src/services/frontend.service.ts @@ -132,7 +132,8 @@ export class FrontendService { versionCli: N8N_VERSION, concurrency: this.globalConfig.executions.concurrency.productionLimit, isNativePythonRunnerEnabled: - this.globalConfig.taskRunners.enabled && process.env.N8N_NATIVE_PYTHON_RUNNER === 'true', + this.globalConfig.taskRunners.enabled && + this.globalConfig.taskRunners.isNativePythonRunnerEnabled, authCookie: { secure: this.globalConfig.auth.cookie.secure, }, diff --git a/packages/cli/src/task-runners/task-runner-module.ts b/packages/cli/src/task-runners/task-runner-module.ts index 0e5f67256e..63c0429cda 100644 --- a/packages/cli/src/task-runners/task-runner-module.ts +++ b/packages/cli/src/task-runners/task-runner-module.ts @@ -118,7 +118,7 @@ export class TaskRunnerModule { await this.jsRunnerProcess.start(); - if (process.env.N8N_NATIVE_PYTHON_RUNNER === 'true') { + if (this.runnerConfig.isNativePythonRunnerEnabled) { const { PyTaskRunnerProcess } = await import('@/task-runners/task-runner-process-py'); this.pyRunnerProcess = Container.get(PyTaskRunnerProcess); this.pyRunnerProcessRestartLoopDetector = new TaskRunnerProcessRestartLoopDetector( diff --git a/packages/frontend/editor-ui/src/components/Node/NodeCreator/composables/useActionsGeneration.ts b/packages/frontend/editor-ui/src/components/Node/NodeCreator/composables/useActionsGeneration.ts index a6db5ced5d..c3e72f3312 100644 --- a/packages/frontend/editor-ui/src/components/Node/NodeCreator/composables/useActionsGeneration.ts +++ b/packages/frontend/editor-ui/src/components/Node/NodeCreator/composables/useActionsGeneration.ts @@ -111,9 +111,9 @@ function operationsCategory(nodeTypeDescription: INodeTypeDescription): ActionTy nodeTypeDescription, ); if (customParsedItems) { - // temporary filter until native Python runner is GA + // temporary until native Python runner is GA return useSettingsStore().isNativePythonRunnerEnabled - ? customParsedItems + ? customParsedItems.filter((item) => item.actionKey !== 'language_python') : customParsedItems.filter((item) => item.actionKey !== 'language_pythonNative'); } } diff --git a/packages/frontend/editor-ui/src/components/ParameterInput.vue b/packages/frontend/editor-ui/src/components/ParameterInput.vue index 9cf3f34341..9386ec9f69 100644 --- a/packages/frontend/editor-ui/src/components/ParameterInput.vue +++ b/packages/frontend/editor-ui/src/components/ParameterInput.vue @@ -241,9 +241,13 @@ const parameterOptions = computed(() => { const options = hasRemoteMethod.value ? remoteParameterOptions.value : props.parameter.options; const safeOptions = (options ?? []).filter(isValidParameterOption); - // temporary filter until native Python runner is GA - if (props.parameter.name === 'language' && !settingsStore.isNativePythonRunnerEnabled) { - return safeOptions.filter((o) => o.value !== 'pythonNative'); + // temporary until native Python runner is GA + if (props.parameter.name === 'language') { + if (settingsStore.isNativePythonRunnerEnabled) { + return safeOptions.filter((o) => o.value !== 'python'); + } else { + return safeOptions.filter((o) => o.value !== 'pythonNative'); + } } return safeOptions; diff --git a/packages/nodes-base/nodes/Code/Code.node.ts b/packages/nodes-base/nodes/Code/Code.node.ts index 1ada7babe9..a9eb3d2f28 100644 --- a/packages/nodes-base/nodes/Code/Code.node.ts +++ b/packages/nodes-base/nodes/Code/Code.node.ts @@ -123,19 +123,22 @@ export class Code implements INodeType { ? (this.getNodeParameter('language', 0) as CodeNodeLanguageOption) : 'javaScript'; - if (language === 'python' && !Container.get(NodesConfig).pythonEnabled) { + const isJsLang = language === 'javaScript'; + const isPyLang = language === 'python' || language === 'pythonNative'; + const runnersConfig = Container.get(TaskRunnersConfig); + const isJsRunner = runnersConfig.enabled; + const isPyRunner = runnersConfig.isNativePythonRunnerEnabled; + + if (isPyLang && !Container.get(NodesConfig).pythonEnabled) { throw new PythonDisabledError(); } - const runnersConfig = Container.get(TaskRunnersConfig); - const isRunnerEnabled = runnersConfig.enabled; - const nodeMode = this.getNodeParameter('mode', 0) as CodeExecutionMode; const workflowMode = this.getMode(); const codeParameterName = language === 'python' || language === 'pythonNative' ? 'pythonCode' : 'jsCode'; - if (language === 'javaScript' && isRunnerEnabled) { + if (isJsLang && isJsRunner) { const code = this.getNodeParameter(codeParameterName, 0) as string; const sandbox = new JsTaskRunnerSandbox(code, nodeMode, workflowMode, this); const numInputItems = this.getInputData().length; @@ -145,9 +148,13 @@ export class Code implements INodeType { : [await sandbox.runCodeForEachItem(numInputItems)]; } - if (language === 'pythonNative' && !isRunnerEnabled) throw new NativePythonWithoutRunnerError(); + if (language === 'pythonNative' && !isPyRunner) { + throw new NativePythonWithoutRunnerError(); + } - if (language === 'pythonNative') { + if (isPyLang && isPyRunner) { + // When the native Python runner is enabled, both `python` and `pythonNative` are + // sent to the runner, to ensure there is no path to run Pyodide in this scenario. const code = this.getNodeParameter(codeParameterName, 0) as string; const sandbox = new PythonTaskRunnerSandbox(code, nodeMode, workflowMode, this);