refactor(core): Include native Python option in Code node (#18331)

This commit is contained in:
Iván Ovejero
2025-08-18 12:25:47 +02:00
committed by GitHub
parent adaa1180eb
commit 47cb4a07ca
6 changed files with 146 additions and 48 deletions

View File

@@ -1,7 +1,9 @@
/* eslint-disable n8n-nodes-base/node-execute-block-wrong-error-thrown */
import { NodesConfig, TaskRunnersConfig } from '@n8n/config';
import { Container } from '@n8n/di';
import set from 'lodash/set';
import {
type INodeProperties,
NodeConnectionTypes,
UserError,
type CodeExecutionMode,
@@ -12,15 +14,19 @@ import {
type INodeTypeDescription,
} from 'n8n-workflow';
type CodeNodeLanguageOption = CodeNodeEditorLanguage | 'pythonNative';
import { javascriptCodeDescription } from './descriptions/JavascriptCodeDescription';
import { pythonCodeDescription } from './descriptions/PythonCodeDescription';
import { JavaScriptSandbox } from './JavaScriptSandbox';
import { JsTaskRunnerSandbox } from './JsTaskRunnerSandbox';
import { NativePythonWithoutRunnerError } from './native-python-without-runner.error';
import { PythonSandbox } from './PythonSandbox';
import { PythonTaskRunnerSandbox } from './PythonTaskRunnerSandbox';
import { getSandboxContext } from './Sandbox';
import { addPostExecutionWarning, standardizeOutput } from './utils';
const { CODE_ENABLE_STDOUT } = process.env;
const { CODE_ENABLE_STDOUT, N8N_NATIVE_PYTHON_RUNNER } = process.env;
class PythonDisabledError extends UserError {
constructor() {
@@ -30,6 +36,40 @@ class PythonDisabledError extends UserError {
}
}
const getV2LanguageProperty = (): INodeProperties => {
const options = [
{
name: 'JavaScript',
value: 'javaScript',
},
{
name: 'Python (Beta)',
value: 'python',
},
];
if (N8N_NATIVE_PYTHON_RUNNER === 'true') {
options.push({
name: 'Python (Native) (Beta)',
value: 'pythonNative',
});
}
return {
displayName: 'Language',
name: 'language',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
'@version': [2],
},
},
options,
default: 'javaScript',
};
};
export class Code implements INodeType {
description: INodeTypeDescription = {
displayName: 'Code',
@@ -65,28 +105,7 @@ export class Code implements INodeType {
],
default: 'runOnceForAllItems',
},
{
displayName: 'Language',
name: 'language',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
'@version': [2],
},
},
options: [
{
name: 'JavaScript',
value: 'javaScript',
},
{
name: 'Python (Beta)',
value: 'python',
},
],
default: 'javaScript',
},
getV2LanguageProperty(),
{
displayName: 'Language',
name: 'language',
@@ -106,22 +125,24 @@ export class Code implements INodeType {
async execute(this: IExecuteFunctions) {
const node = this.getNode();
const language: CodeNodeEditorLanguage =
const language: CodeNodeLanguageOption =
node.typeVersion === 2
? (this.getNodeParameter('language', 0) as CodeNodeEditorLanguage)
? (this.getNodeParameter('language', 0) as CodeNodeLanguageOption)
: 'javaScript';
if (language === 'python' && !Container.get(NodesConfig).pythonEnabled) {
// eslint-disable-next-line n8n-nodes-base/node-execute-block-wrong-error-thrown
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' ? 'pythonCode' : 'jsCode';
const codeParameterName =
language === 'python' || language === 'pythonNative' ? 'pythonCode' : 'jsCode';
if (runnersConfig.enabled && language === 'javaScript') {
if (language === 'javaScript' && isRunnerEnabled) {
const code = this.getNodeParameter(codeParameterName, 0) as string;
const sandbox = new JsTaskRunnerSandbox(code, nodeMode, workflowMode, this);
const numInputItems = this.getInputData().length;
@@ -131,6 +152,15 @@ export class Code implements INodeType {
: [await sandbox.runCodeForEachItem(numInputItems)];
}
if (language === 'pythonNative' && !isRunnerEnabled) throw new NativePythonWithoutRunnerError();
if (language === 'pythonNative') {
const code = this.getNodeParameter(codeParameterName, 0) as string;
const sandbox = new PythonTaskRunnerSandbox(code, nodeMode, workflowMode, this);
return [await sandbox.runUsingIncomingItems()];
}
const getSandbox = (index = 0) => {
const code = this.getNodeParameter(codeParameterName, index) as string;

View File

@@ -1,13 +1,12 @@
import {
ApplicationError,
type CodeExecutionMode,
type IExecuteFunctions,
type INodeExecutionData,
type WorkflowExecuteMode,
} from 'n8n-workflow';
import { isWrappableError, WrappedExecutionError } from './errors/WrappedExecutionError';
import { validateNoDisallowedMethodsInRunForEach } from './JsCodeValidator';
import { throwExecutionError } from './throw-execution-error';
/**
* JS Code execution sandbox that executes the JS code using task runner.
@@ -37,7 +36,7 @@ export class JsTaskRunnerSandbox {
return executionResult.ok
? executionResult.result
: this.throwExecutionError('error' in executionResult ? executionResult.error : {});
: throwExecutionError('error' in executionResult ? executionResult.error : {});
}
async runCodeForEachItem(numInputItems: number): Promise<INodeExecutionData[]> {
@@ -64,7 +63,7 @@ export class JsTaskRunnerSandbox {
);
if (!executionResult.ok) {
return this.throwExecutionError('error' in executionResult ? executionResult.error : {});
return throwExecutionError('error' in executionResult ? executionResult.error : {});
}
executionResults = executionResults.concat(executionResult.result);
@@ -73,18 +72,6 @@ export class JsTaskRunnerSandbox {
return executionResults;
}
private throwExecutionError(error: unknown): never {
if (error instanceof Error) {
throw error;
} else if (isWrappableError(error)) {
// The error coming from task runner is not an instance of error,
// so we need to wrap it in an error instance.
throw new WrappedExecutionError(error);
}
throw new ApplicationError(`Unknown error: ${JSON.stringify(error)}`);
}
/** Chunks the input items into chunks of 1000 items each */
private chunkInputItems(numInputItems: number) {
const numChunks = Math.ceil(numInputItems / this.chunkSize);

View File

@@ -0,0 +1,46 @@
import {
type CodeExecutionMode,
type IExecuteFunctions,
type INodeExecutionData,
type WorkflowExecuteMode,
} from 'n8n-workflow';
import { throwExecutionError } from './throw-execution-error';
export class PythonTaskRunnerSandbox {
constructor(
private readonly pythonCode: string,
private readonly nodeMode: CodeExecutionMode,
private readonly workflowMode: WorkflowExecuteMode,
private readonly executeFunctions: IExecuteFunctions,
) {}
/**
* Run a script by forwarding it to a Python task runner, together with input items.
*
* The Python runner receives input items together with the task, whereas the
* JavaScript runner does _not_ receive input items together with the task and
* instead retrieves them later, only if needed, via an RPC request.
*/
async runUsingIncomingItems() {
const itemIndex = 0;
const taskSettings: Record<string, unknown> = {
code: this.pythonCode,
nodeMode: this.nodeMode,
workflowMode: this.workflowMode,
continueOnFail: this.executeFunctions.continueOnFail(),
items: this.executeFunctions.getInputData(),
};
const executionResult = await this.executeFunctions.startJob<INodeExecutionData[]>(
'python',
taskSettings,
itemIndex,
);
return executionResult.ok
? executionResult.result
: throwExecutionError('error' in executionResult ? executionResult.error : {});
}
}

View File

@@ -14,12 +14,15 @@ const commonDescription: INodeProperties = {
noDataExpression: true,
};
const PRINT_INSTRUCTION =
'Debug by using <code>print()</code> statements and viewing their output in the browser console.';
export const pythonCodeDescription: INodeProperties[] = [
{
...commonDescription,
displayOptions: {
show: {
language: ['python'],
language: ['python', 'pythonNative'],
mode: ['runOnceForAllItems'],
},
},
@@ -28,14 +31,13 @@ export const pythonCodeDescription: INodeProperties[] = [
...commonDescription,
displayOptions: {
show: {
language: ['python'],
language: ['python', 'pythonNative'],
mode: ['runOnceForEachItem'],
},
},
},
{
displayName:
'Debug by using <code>print()</code> statements and viewing their output in the browser console.',
displayName: PRINT_INSTRUCTION,
name: 'notice',
type: 'notice',
displayOptions: {
@@ -45,4 +47,15 @@ export const pythonCodeDescription: INodeProperties[] = [
},
default: '',
},
{
displayName: `${PRINT_INSTRUCTION}<br><br>The native Python option does not support <code>_</code> syntax and helpers, except for <code>_items</code> and <code>_item</code>.`,
name: 'notice',
type: 'notice',
displayOptions: {
show: {
language: ['pythonNative'],
},
},
default: '',
},
];

View File

@@ -0,0 +1,7 @@
import { UserError } from 'n8n-workflow';
export class NativePythonWithoutRunnerError extends UserError {
constructor() {
super('To use native Python, please use runners by setting `N8N_RUNNERS_ENABLED=true`.');
}
}

View File

@@ -0,0 +1,15 @@
import { ApplicationError } from 'n8n-workflow';
import { isWrappableError, WrappedExecutionError } from './errors/WrappedExecutionError';
export function throwExecutionError(error: unknown): never {
if (error instanceof Error) {
throw error;
} else if (isWrappableError(error)) {
// The error coming from task runner is not an instance of error,
// so we need to wrap it in an error instance.
throw new WrappedExecutionError(error);
}
throw new ApplicationError(`Unknown error: ${JSON.stringify(error)}`);
}