From ce416a1eb0c9dde89e1a67c2f655c34098105a48 Mon Sep 17 00:00:00 2001 From: Marc Lehner Date: Tue, 6 Jan 2026 13:48:16 +0100 Subject: [PATCH 01/34] AP-25245: add pixi port to python scripting node AP-25245 () --- .../META-INF/MANIFEST.MF | 5 +- .../AbstractPythonScriptingNodeModel.java | 102 +++++++++++++++++- .../nodes/PortsConfigurationUtils.java | 47 +++++++- .../nodes/script/PythonScriptNodeFactory.java | 19 +++- .../nodes/script/PythonScriptNodeModel.java | 11 +- .../nodes/view/PythonViewNodeFactory.java | 3 +- .../nodes/view/PythonViewNodeModel.java | 4 +- .../nodes2/PythonScriptNodeModel.java | 83 +++++++++++++- .../PythonScriptPortsConfiguration.java | 33 +++++- .../PythonScriptingInputOutputModelUtils.java | 16 +++ .../script/PythonScriptNodeFactory.java | 18 ++++ 11 files changed, 315 insertions(+), 26 deletions(-) diff --git a/org.knime.python3.scripting.nodes/META-INF/MANIFEST.MF b/org.knime.python3.scripting.nodes/META-INF/MANIFEST.MF index 686ad0f7b..c39c9471a 100644 --- a/org.knime.python3.scripting.nodes/META-INF/MANIFEST.MF +++ b/org.knime.python3.scripting.nodes/META-INF/MANIFEST.MF @@ -23,8 +23,9 @@ Require-Bundle: org.knime.core;bundle-version="[5.11.0,6.0.0)", org.eclipse.ui;bundle-version="3.119.0", org.knime.conda;bundle-version="[5.11.0,6.0.0)", org.knime.conda.envbundling;bundle-version="[5.10.0,6.0.0)", - org.knime.core.ui;bundle-version="[5.11.0,6.0.0)", - org.knime.workbench.editor;bundle-version="[5.10.0,6.0.0)", + org.knime.pixi.nodes;bundle-version="[5.9.0,6.0.0)", + org.knime.core.ui;bundle-version="[5.10.0,6.0.0)", + org.knime.workbench.editor;bundle-version="[5.9.0,6.0.0)", org.apache.batik.util;bundle-version="[1.16.0,2.0.0)", org.apache.batik.dom;bundle-version="[1.16.0,2.0.0)", org.apache.batik.anim;bundle-version="[1.16.0,2.0.0)", diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/AbstractPythonScriptingNodeModel.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/AbstractPythonScriptingNodeModel.java index a8e7f34e7..830686f4e 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/AbstractPythonScriptingNodeModel.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/AbstractPythonScriptingNodeModel.java @@ -81,6 +81,7 @@ import org.knime.core.util.PathUtils; import org.knime.core.util.asynclose.AsynchronousCloseableTracker; import org.knime.core.webui.node.view.NodeView; +import org.knime.pixi.nodes.PixiEnvironmentPortObject; import org.knime.python2.PythonCommand; import org.knime.python2.PythonModuleSpec; import org.knime.python2.PythonVersion; @@ -101,6 +102,7 @@ import org.knime.python2.ports.OutputPort; import org.knime.python2.ports.PickledObjectOutputPort; import org.knime.python2.ports.Port; +import org.knime.python3.AbstractCondaPythonCommand; import org.knime.python3.scripting.Python3KernelBackend; import org.knime.python3.scripting.nodes.prefs.Python3ScriptingPreferences; @@ -155,6 +157,8 @@ static void setExpectedOutputView(final PythonKernel kernel, final boolean expec private final boolean m_hasView; + private final boolean m_hasPixiPort; + private String m_script; private final PythonCommandConfig m_command = createCommandConfig(); @@ -165,11 +169,12 @@ static void setExpectedOutputView(final PythonKernel kernel, final boolean expec new AsynchronousCloseableTracker<>(t -> LOGGER.debug("Kernel shutdown failed.", t)); protected AbstractPythonScriptingNodeModel(final InputPort[] inPorts, final OutputPort[] outPorts, - final boolean hasView, final String defaultScript) { - super(toPortTypes(inPorts), toPortTypes(outPorts)); + final boolean hasView, final boolean hasPixiPort, final String defaultScript) { + super(toPortTypes(inPorts, hasPixiPort), toPortTypes(outPorts)); m_inPorts = inPorts; m_outPorts = outPorts; m_hasView = hasView; + m_hasPixiPort = hasPixiPort; m_view = Optional.empty(); m_script = defaultScript; } @@ -178,6 +183,23 @@ private static final PortType[] toPortTypes(final Port[] ports) { return Arrays.stream(ports).map(Port::getPortType).toArray(PortType[]::new); } + private static final PortType[] toPortTypes(final Port[] ports, final boolean hasPixiPort) { + if (!hasPixiPort) { + return toPortTypes(ports); + } + // Add the optional Pixi port at the end of the input ports + final PortType[] portTypes = new PortType[ports.length + 1]; + for (int i = 0; i < ports.length; i++) { + portTypes[i] = ports[i].getPortType(); + } + try { + portTypes[ports.length] = PixiEnvironmentPortObject.TYPE_OPTIONAL; + } catch (NoClassDefFoundError e) { + throw new IllegalStateException("Could not load PixiEnvironmentPortObject class", e); + } + return portTypes; + } + @Override protected void saveSettingsTo(final NodeSettingsWO settings) { saveScriptTo(m_script, settings); @@ -198,7 +220,9 @@ protected void loadValidatedSettingsFrom(final NodeSettingsRO settings) throws I @Override protected PortObjectSpec[] configure(final PortObjectSpec[] inSpecs) throws InvalidSettingsException { - for (int i = 0; i < m_inPorts.length; i++) { + // The Pixi port (if present) is at the end of the input specs + final int numRegularPorts = m_inPorts.length; + for (int i = 0; i < numRegularPorts; i++) { m_inPorts[i].configure(inSpecs[i]); } return null; // NOSONAR Conforms to KNIME API. @@ -206,6 +230,15 @@ protected PortObjectSpec[] configure(final PortObjectSpec[] inSpecs) throws Inva @Override protected PortObject[] execute(final PortObject[] inObjects, final ExecutionContext exec) throws Exception { + // Extract Pixi environment if present + // The Pixi port (if present) is at the end of the input objects + final PythonCommand pythonCommandFromPixi; + if (m_hasPixiPort && inObjects.length > m_inPorts.length) { + pythonCommandFromPixi = extractPythonCommandFromPixiPort(inObjects[inObjects.length - 1]); + } else { + pythonCommandFromPixi = null; + } + double inWeight = 0d; final Set requiredAdditionalModules = new HashSet<>(); for (int i = 0; i < m_inPorts.length; i++) { @@ -217,7 +250,8 @@ protected PortObject[] execute(final PortObject[] inObjects, final ExecutionCont final var cancelable = new PythonExecutionMonitorCancelable(exec); try (final PythonKernel kernel = - getNextKernelFromQueue(requiredAdditionalModules, Collections.emptySet(), cancelable)) { + getNextKernelFromQueue(requiredAdditionalModules, Collections.emptySet(), cancelable, + pythonCommandFromPixi)) { final Collection inFlowVariables = getAvailableFlowVariables(Python3KernelBackend.getCompatibleFlowVariableTypes()).values(); kernel.putFlowVariables(null, inFlowVariables); @@ -349,10 +383,68 @@ private static Path persistedViewPath(final File nodeInternDir) { return nodeInternDir.toPath().resolve("view.html"); } + /** + * Extract the Python command from a PixiEnvironmentPortObject. + * + * @param portObject the port object (may be null if optional port is not connected) + * @return the Python command, or null if the port is not connected or doesn't contain a valid Python executable + * @throws InvalidSettingsException if the Python executable path from the Pixi environment doesn't exist + */ + private static PythonCommand extractPythonCommandFromPixiPort(final PortObject portObject) + throws InvalidSettingsException { + if (portObject == null) { + return null; + } + + try { + if (!(portObject instanceof PixiEnvironmentPortObject)) { + return null; + } + + final PixiEnvironmentPortObject pixiPort = (PixiEnvironmentPortObject)portObject; + final Path pythonExecPath = pixiPort.getPythonExecutablePath(); + + if (pythonExecPath == null) { + throw new InvalidSettingsException( + "The Pixi environment does not contain a Python executable. " + + "Please ensure the environment includes Python."); + } + + if (!Files.exists(pythonExecPath)) { + throw new InvalidSettingsException( + "The Python executable from the Pixi environment does not exist at path: " + pythonExecPath + + ". Please check that the Pixi environment was created successfully."); + } + + // Use AbstractCondaPythonCommand which handles environment variable patching + final org.knime.python3.PythonCommand pythonCommand = + new AbstractCondaPythonCommand(pixiPort.getAsCondaEnvironmentDirectory()) {}; + + return new LegacyPythonCommand(pythonCommand); + + } catch (NoClassDefFoundError e) { + // Pixi nodes bundle is not available - this is fine since it's optional + LOGGER.debug("PixiEnvironmentPortObject class not available - pixi nodes bundle may not be installed", e); + return null; + } + } + protected PythonKernel getNextKernelFromQueue(final Set requiredAdditionalModules, final Set optionalAdditionalModules, final PythonCancelable cancelable) throws PythonCanceledExecutionException, PythonIOException { - return PythonKernelQueue.getNextKernel(m_command.getCommand(), PythonKernelBackendType.PYTHON3, + return getNextKernelFromQueue(requiredAdditionalModules, optionalAdditionalModules, cancelable, null); + } + + protected PythonKernel getNextKernelFromQueue(final Set requiredAdditionalModules, + final Set optionalAdditionalModules, final PythonCancelable cancelable, + final PythonCommand pythonCommandFromPixi) + throws PythonCanceledExecutionException, PythonIOException { + // Use Python command from Pixi port if available + // TODO: We might want to consider flow variables in addition to the Pixi port in the future + final PythonCommand commandToUse = + pythonCommandFromPixi != null ? pythonCommandFromPixi : m_command.getCommand(); + + return PythonKernelQueue.getNextKernel(commandToUse, PythonKernelBackendType.PYTHON3, requiredAdditionalModules, optionalAdditionalModules, new PythonKernelOptions(), cancelable); } diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/PortsConfigurationUtils.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/PortsConfigurationUtils.java index c4be4324e..fc49a7bef 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/PortsConfigurationUtils.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/PortsConfigurationUtils.java @@ -60,6 +60,7 @@ import org.knime.python2.ports.OutputPort; import org.knime.python2.ports.PickledObjectInputPort; import org.knime.python2.ports.PickledObjectOutputPort; +import org.knime.pixi.nodes.PixiEnvironmentPortObject; /** * @author Marcel Wiedenmann, KNIME GmbH, Konstanz, Germany @@ -71,6 +72,27 @@ private PortsConfigurationUtils() { // Utility class } + /** + * Check if the ports configuration contains a Pixi environment port. + * + * @param config the ports configuration + * @return true if a Pixi environment port is present + */ + public static boolean hasPixiPort(final PortsConfiguration config) { + final PortType[] inTypes = config.getInputPorts(); + try { + // Check if any input port is a PixiEnvironmentPortObject + for (final PortType inType : inTypes) { + if (inType.equals(PixiEnvironmentPortObject.TYPE) || inType.equals(PixiEnvironmentPortObject.TYPE_OPTIONAL)) { + return true; + } + } + } catch (NoClassDefFoundError e) { + // Pixi nodes bundle is not available - this is fine since it's optional + } + return false; + } + /** * Extract the input ports from the given ports configuration. * @@ -81,9 +103,21 @@ public static InputPort[] createInputPorts(final PortsConfiguration config) { final PortType[] inTypes = config.getInputPorts(); int inTableIndex = 0; int inObjectIndex = 0; - final var inPorts = new InputPort[inTypes.length]; + // Count non-Pixi ports for the result array + int numNonPixiPorts = 0; + for (final PortType inType : inTypes) { + if (!isPixiPort(inType)) { + numNonPixiPorts++; + } + } + final var inPorts = new InputPort[numNonPixiPorts]; + int portIndex = 0; for (int i = 0; i < inTypes.length; i++) { final PortType inType = inTypes[i]; + // Skip Pixi ports - they are not InputPorts in the traditional sense + if (isPixiPort(inType)) { + continue; + } final InputPort inPort; if (BufferedDataTable.TYPE.equals(inType)) { inPort = new DataTableInputPort("knio.input_tables[" + inTableIndex++ + "]"); @@ -92,11 +126,20 @@ public static InputPort[] createInputPorts(final PortsConfiguration config) { } else { throw new IllegalStateException("Unsupported input type: " + inType.getName()); } - inPorts[i] = inPort; + inPorts[portIndex++] = inPort; } return inPorts; } + private static boolean isPixiPort(final PortType inType) { + try { + return inType.equals(PixiEnvironmentPortObject.TYPE) || inType.equals(PixiEnvironmentPortObject.TYPE_OPTIONAL); + } catch (NoClassDefFoundError e) { + // Pixi nodes bundle is not available - this is fine since it's optional + return false; + } + } + /** * Extract the output ports from the given ports configuration. * diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/script/PythonScriptNodeFactory.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/script/PythonScriptNodeFactory.java index 0fbfb7dce..147d25b76 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/script/PythonScriptNodeFactory.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/script/PythonScriptNodeFactory.java @@ -58,10 +58,12 @@ import org.knime.core.node.BufferedDataTable; import org.knime.core.node.ConfigurableNodeFactory; import org.knime.core.node.NodeDialogPane; +import org.knime.core.node.NodeLogger; import org.knime.core.node.NodeView; import org.knime.core.node.context.NodeCreationConfiguration; import org.knime.core.node.port.PortType; import org.knime.core.node.port.image.ImagePortObject; +import org.knime.pixi.nodes.PixiEnvironmentPortObject; import org.knime.python2.port.PickledObjectFileStorePortObject; import org.knime.python2.ports.InputPort; import org.knime.python2.ports.OutputPort; @@ -72,12 +74,26 @@ */ public final class PythonScriptNodeFactory extends ConfigurableNodeFactory { + private static final NodeLogger LOGGER = NodeLogger.getLogger(PythonScriptNodeFactory.class); + @Override protected Optional createPortsConfigBuilder() { final var b = new PortsConfigurationBuilder(); b.addExtendableInputPortGroup("Input object (pickled)", PickledObjectFileStorePortObject.TYPE); b.addExtendableInputPortGroupWithDefault("Input table", new PortType[0], new PortType[]{BufferedDataTable.TYPE}, BufferedDataTable.TYPE); + boolean pixiPortAdded = false; + try { + final Class pixiClass = PixiEnvironmentPortObject.class; + final PortType pixiPortType = PixiEnvironmentPortObject.TYPE_OPTIONAL; + b.addOptionalInputPortGroup("Pixi environment", pixiPortType); + pixiPortAdded = true; + LOGGER.info("Successfully added optional Pixi environment port"); + } catch (NoClassDefFoundError e) { + LOGGER.warn("Could not add Pixi environment port - pixi bundle not available: " + e.getMessage()); + } catch (Exception e) { + LOGGER.error("Unexpected error adding Pixi environment port", e); + } b.addExtendableOutputPortGroupWithDefault("Output table", new PortType[0], new PortType[]{BufferedDataTable.TYPE}, BufferedDataTable.TYPE); b.addExtendableOutputPortGroup("Output image", ImagePortObject.TYPE); @@ -92,7 +108,8 @@ protected PythonScriptNodeModel createNodeModel(final NodeCreationConfiguration if (urlConfig.isPresent()) { return PythonScriptNodeModel.createDnDNodeModel(urlConfig.get().getUrl()); } - return new PythonScriptNodeModel(createInputPorts(config), createOutputPorts(config)); + return new PythonScriptNodeModel(createInputPorts(config), createOutputPorts(config), + PortsConfigurationUtils.hasPixiPort(config)); } @Override diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/script/PythonScriptNodeModel.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/script/PythonScriptNodeModel.java index ab9441a72..20bb426a2 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/script/PythonScriptNodeModel.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/script/PythonScriptNodeModel.java @@ -66,12 +66,13 @@ */ final class PythonScriptNodeModel extends AbstractPythonScriptingNodeModel { - public PythonScriptNodeModel(final InputPort[] inPorts, final OutputPort[] outPorts) { - super(inPorts, outPorts, false, createDefaultScript(inPorts, outPorts)); + public PythonScriptNodeModel(final InputPort[] inPorts, final OutputPort[] outPorts, final boolean hasPixiPort) { + super(inPorts, outPorts, false, hasPixiPort, createDefaultScript(inPorts, outPorts)); } - PythonScriptNodeModel(final InputPort[] inPorts, final OutputPort[] outPorts, final String defaultScript) { - super(inPorts, outPorts, false, defaultScript); + PythonScriptNodeModel(final InputPort[] inPorts, final OutputPort[] outPorts, final boolean hasPixiPort, + final String defaultScript) { + super(inPorts, outPorts, false, hasPixiPort, defaultScript); } @@ -81,7 +82,7 @@ static PythonScriptNodeModel createDnDNodeModel(final URL url) { var variableNames = VariableNamesUtils.getVariableNames(inPorts, outPorts); var defaultScript = "import knime.scripting.io as knio\n\n" + getPythonObjectReaderDefaultScript(variableNames, getPath(url)); - return new PythonScriptNodeModel(inPorts, outPorts, defaultScript); + return new PythonScriptNodeModel(inPorts, outPorts, false, defaultScript); } private static String getPath(final URL url) { diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/view/PythonViewNodeFactory.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/view/PythonViewNodeFactory.java index b745307a5..8a8464504 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/view/PythonViewNodeFactory.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/view/PythonViewNodeFactory.java @@ -88,7 +88,8 @@ protected Optional createPortsConfigBuilder() { @Override protected PythonViewNodeModel createNodeModel(final NodeCreationConfiguration creationConfig) { final var config = creationConfig.getPortConfig().get(); // NOSONAR - return new PythonViewNodeModel(createInputPorts(config), createOutputPorts(config)); + // Python View nodes currently don't support Pixi environment ports + return new PythonViewNodeModel(createInputPorts(config), createOutputPorts(config), false); } @Override diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/view/PythonViewNodeModel.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/view/PythonViewNodeModel.java index 3ffa2ea0c..39237e1c2 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/view/PythonViewNodeModel.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/view/PythonViewNodeModel.java @@ -61,8 +61,8 @@ */ final class PythonViewNodeModel extends AbstractPythonScriptingNodeModel { - public PythonViewNodeModel(final InputPort[] inPorts, final OutputPort[] outPorts) { - super(inPorts, outPorts, true, createDefaultScript(inPorts)); + public PythonViewNodeModel(final InputPort[] inPorts, final OutputPort[] outPorts, final boolean hasPixiPort) { + super(inPorts, outPorts, true, hasPixiPort, createDefaultScript(inPorts)); } Path getPathToHtml() { diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptNodeModel.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptNodeModel.java index eb698fa5f..057f44fbb 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptNodeModel.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptNodeModel.java @@ -51,7 +51,9 @@ import java.io.File; import java.io.IOException; import java.net.ConnectException; +import java.nio.file.Files; import java.nio.file.Path; +import java.util.Arrays; import java.util.Collection; import java.util.Map; import java.util.Objects; @@ -86,6 +88,8 @@ import org.knime.core.node.workflow.VariableTypeRegistry; import org.knime.core.util.asynclose.AsynchronousCloseableTracker; import org.knime.core.webui.node.dialog.scripting.ScriptingService.ConsoleText; +import org.knime.pixi.nodes.PixiEnvironmentPortObject; +import org.knime.python3.AbstractCondaPythonCommand; import org.knime.python3.PythonCommand; import org.knime.python3.PythonProcessTerminatedException; import org.knime.python3.scripting.nodes2.ConsoleOutputUtils.ConsoleOutputStorage; @@ -177,16 +181,46 @@ protected PortObjectSpec[] configure(final PortObjectSpec[] inSpecs) throws Inva @Override protected PortObject[] execute(final PortObject[] inObjects, final ExecutionContext exec) throws IOException, InterruptedException, CanceledExecutionException, KNIMEException { - final PythonCommand pythonCommand = - ExecutableSelectionUtils.getPythonCommand(m_settings.getExecutableSelection()); + // Check if Pixi port is connected and use it, otherwise use configured Python command + final PythonCommand pythonCommand; + if (m_ports.hasPixiPort()) { + LOGGER.debug("Checking for Pixi environment port"); + // The Pixi port is after all regular input ports + final int pixiPortIndex = inObjects.length - 1; + PythonCommand pixiCommand = null; + try { + pixiCommand = extractPythonCommandFromPixiPort(inObjects[pixiPortIndex]); + } catch (InvalidSettingsException ex) { + // TODO Auto-generated catch block + } + if (pixiCommand != null) { + LOGGER.debug("Using Python from Pixi environment"); + pythonCommand = pixiCommand; + // TODO: Consider if flow variable should take precedence over Pixi port + } else { + LOGGER.debug("Pixi port not connected, using configured Python command"); + pythonCommand = ExecutableSelectionUtils.getPythonCommand(m_settings.getExecutableSelection()); + } + } else { + pythonCommand = ExecutableSelectionUtils.getPythonCommand(m_settings.getExecutableSelection()); + } m_consoleOutputStorage = null; final var consoleConsumer = ConsoleOutputUtils.createConsoleConsumer(); try (final var session = new PythonScriptingSession(pythonCommand, consoleConsumer, new ModelFileStoreHandlerSupplier())) { + // Filter out Pixi port from inObjects - it's not a data port + final PortObject[] dataPortObjects; + if (m_ports.hasPixiPort()) { + // Pixi port is at the end, so exclude it + dataPortObjects = Arrays.copyOf(inObjects, inObjects.length - 1); + } else { + dataPortObjects = inObjects; + } + exec.setProgress(0.0, "Setting up inputs"); - session.setupIO(inObjects, getAvailableFlowVariables(KNOWN_FLOW_VARIABLE_TYPES).values(), + session.setupIO(dataPortObjects, getAvailableFlowVariables(KNOWN_FLOW_VARIABLE_TYPES).values(), m_ports.getNumOutTables(), m_ports.getNumOutImages(), m_ports.getNumOutObjects(), m_hasView, exec.createSubProgress(0.3)); exec.setProgress(0.3, "Running script"); @@ -272,12 +306,53 @@ private void addNewFlowVariables(final Collection newVariables) { } } - @SuppressWarnings({"unchecked", "rawtypes"}) + @SuppressWarnings("unchecked") private void pushNewFlowVariable(final FlowVariable variable) { pushFlowVariable(variable.getName(), (VariableType)variable.getVariableType(), variable.getValue(variable.getVariableType())); } + /** + * Extract the Python command from a PixiEnvironmentPortObject. + * + * @param portObject the port object (may be null if optional port is not connected) + * @return the Python command, or null if the port is not connected or doesn't contain a valid Python executable + * @throws InvalidSettingsException if the Python executable path from the Pixi environment doesn't exist + */ + private static PythonCommand extractPythonCommandFromPixiPort(final PortObject portObject) + throws InvalidSettingsException { + if (portObject == null) { + return null; + } + + try { + // Check if this is a Pixi environment port object + if (!(portObject instanceof PixiEnvironmentPortObject)) { + return null; + } + + final PixiEnvironmentPortObject pixiPort = (PixiEnvironmentPortObject)portObject; + final Path pythonExecPath = pixiPort.getPythonExecutablePath(); + + if (pythonExecPath == null) { + throw new InvalidSettingsException("The Pixi environment does not contain a Python executable.\nPlease ensure that Python is installed in the Pixi environment."); + } + + if (!Files.exists(pythonExecPath)) { + throw new InvalidSettingsException("The Python executable from the Pixi environment does not exist: " + + pythonExecPath + ". Please check that the Pixi environment is valid."); + } + LOGGER.debug("Using Python executable from Pixi environment: " + pythonExecPath); + + // Use AbstractCondaPythonCommand which handles environment variable patching + return new AbstractCondaPythonCommand(pixiPort.getAsCondaEnvironmentDirectory()) {}; + } catch (NoClassDefFoundError e) { + // Pixi bundle not available - this should not happen if the port was added successfully + LOGGER.debug("PixiEnvironmentPortObject class not available", e); + return null; + } + } + /** Get the output view from the session if the node has a view and remember the path */ private void collectViewFromSession(final PythonScriptingSession session) throws IOException, KNIMEException { if (m_hasView) { diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptPortsConfiguration.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptPortsConfiguration.java index 2897d4b48..5eab1fd0b 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptPortsConfiguration.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptPortsConfiguration.java @@ -55,6 +55,7 @@ import org.knime.core.node.context.ports.PortsConfiguration; import org.knime.core.node.port.image.ImagePortObject; import org.knime.core.node.workflow.NodeContext; +import org.knime.pixi.nodes.PixiEnvironmentPortObject; import org.knime.python2.port.PickledObjectFileStorePortObject; /** @@ -79,6 +80,9 @@ public final class PythonScriptPortsConfiguration { /** Name of the object output port */ public static final String PORTGR_ID_OUT_OBJECT = "Output object (pickled)"; + /** Name of the Pixi environment port */ + public static final String PORTGR_ID_PIXI_ENV = "Pixi environment"; + private final int m_numInTables; private final int m_numInObjects; @@ -91,6 +95,8 @@ public final class PythonScriptPortsConfiguration { private final boolean m_hasView; + private final boolean m_hasPixiPort; + /** * Create a new {@link PythonScriptPortsConfiguration} from the given {@link PortsConfiguration}. * @@ -103,12 +109,14 @@ static PythonScriptPortsConfiguration fromPortsConfiguration(final PortsConfigur final Map inPortsLocation = portsConfig.getInputPortLocation(); final var numInTables = ArrayUtils.getLength(inPortsLocation.get(PORTGR_ID_INP_TABLE)); final var numInObjects = ArrayUtils.getLength(inPortsLocation.get(PORTGR_ID_INP_OBJECT)); + final var hasPixiPort = inPortsLocation.containsKey(PORTGR_ID_PIXI_ENV) + && ArrayUtils.getLength(inPortsLocation.get(PORTGR_ID_PIXI_ENV)) > 0; final Map outPortsLocation = portsConfig.getOutputPortLocation(); final var numOutTables = ArrayUtils.getLength(outPortsLocation.get(PORTGR_ID_OUT_TABLE)); final var numOutImages = ArrayUtils.getLength(outPortsLocation.get(PORTGR_ID_OUT_IMAGE)); final var numOutObjects = ArrayUtils.getLength(outPortsLocation.get(PORTGR_ID_OUT_OBJECT)); - return new PythonScriptPortsConfiguration(numInTables, numInObjects, numOutTables, numOutImages, numOutObjects, hasView); + return new PythonScriptPortsConfiguration(numInTables, numInObjects, numOutTables, numOutImages, numOutObjects, hasView, hasPixiPort); } /** @@ -134,6 +142,7 @@ static PythonScriptPortsConfiguration fromCurrentNodeContext() { // Count the number of the different ports (skip the flow var port) var numInTables = 0; var numInObjects = 0; + var hasPixiPort = false; for (int i = 1; i < nodeContainer.getNrInPorts(); i++) { var portType = nodeContainer.getInPort(i).getPortType(); if (BufferedDataTable.TYPE.equals(portType)) { @@ -141,6 +150,16 @@ static PythonScriptPortsConfiguration fromCurrentNodeContext() { } else if (PickledObjectFileStorePortObject.TYPE.equals(portType)) { numInObjects++; } else { + // Check if it's a Pixi environment port + try { + if (PixiEnvironmentPortObject.TYPE.equals(portType) + || PixiEnvironmentPortObject.TYPE_OPTIONAL.equals(portType)) { + hasPixiPort = true; + continue; // Don't count as error + } + } catch (NoClassDefFoundError e) { + // Pixi bundle not available, ignore + } throw new IllegalStateException("Unsupported input port configured. This is an implementation error."); } } @@ -162,17 +181,18 @@ static PythonScriptPortsConfiguration fromCurrentNodeContext() { } var hasView = nodeContainer.getNrViews() > 0; - return new PythonScriptPortsConfiguration(numInTables, numInObjects, numOutTables, numOutImages, numOutObjects, hasView); + return new PythonScriptPortsConfiguration(numInTables, numInObjects, numOutTables, numOutImages, numOutObjects, hasView, hasPixiPort); } private PythonScriptPortsConfiguration(final int numInTables, final int numInObjects, final int numOutTables, - final int numOutImages, final int numOutObjects, final boolean hasView) { + final int numOutImages, final int numOutObjects, final boolean hasView, final boolean hasPixiPort) { m_numInTables = numInTables; m_numInObjects = numInObjects; m_numOutTables = numOutTables; m_numOutImages = numOutImages; m_numOutObjects = numOutObjects; m_hasView = hasView; + m_hasPixiPort = hasPixiPort; } /** @@ -216,5 +236,10 @@ public int getNumOutObjects() { public boolean hasView() { return m_hasView; } - + /** + * @return if the node has a Pixi environment port + */ + public boolean hasPixiPort() { + return m_hasPixiPort; + } } diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingInputOutputModelUtils.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingInputOutputModelUtils.java index a194596a7..ebc0b38ab 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingInputOutputModelUtils.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingInputOutputModelUtils.java @@ -64,6 +64,7 @@ import org.knime.core.node.workflow.FlowVariable; import org.knime.core.webui.node.dialog.scripting.InputOutputModel; import org.knime.core.webui.node.dialog.scripting.WorkflowControl.InputPortInfo; +import org.knime.pixi.nodes.PixiEnvironmentPortObject; import org.knime.python2.port.PickledObjectFileStorePortObject; /** @@ -127,6 +128,12 @@ static List getInputObjects(final InputPortInfo[] inputPorts) var objectIdx = 0; for (int i = 0; i < inputPorts.length; i++) { final var type = inputPorts[i].portType(); + + // Skip Pixi environment ports - they are not data ports + if (isPixiEnvironmentPort(type)) { + continue; + } + final var spec = inputPorts[i].portSpec(); if (spec instanceof DataTableSpec dataTableSpec) { // Table with specs available @@ -204,6 +211,15 @@ private static boolean isNoFlowVariablePort(final PortType portType) { return !portType.acceptsPortObjectClass(FlowVariablePortObject.class); } + private static boolean isPixiEnvironmentPort(final PortType portType) { + try { + return portType.acceptsPortObjectClass(PixiEnvironmentPortObject.class); + } catch (NoClassDefFoundError e) { + // Pixi nodes bundle not available + return false; + } + } + private static String portTypeToInputOutputType(final PortType portType) { if (portType.acceptsPortObjectClass(BufferedDataTable.class)) { return INPUT_OUTPUT_TYPE_TABLE; diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/script/PythonScriptNodeFactory.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/script/PythonScriptNodeFactory.java index 756f3bf9e..fe0dd6dbb 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/script/PythonScriptNodeFactory.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/script/PythonScriptNodeFactory.java @@ -59,6 +59,7 @@ import org.knime.core.node.BufferedDataTable; import org.knime.core.node.ConfigurableNodeFactory; import org.knime.core.node.NodeDialogPane; +import org.knime.core.node.NodeLogger; import org.knime.core.node.NodeView; import org.knime.core.node.context.NodeCreationConfiguration; import org.knime.core.node.port.PortType; @@ -66,6 +67,7 @@ import org.knime.core.webui.node.dialog.NodeDialog; import org.knime.core.webui.node.dialog.NodeDialogFactory; import org.knime.core.webui.node.dialog.NodeDialogManager; +import org.knime.pixi.nodes.PixiEnvironmentPortObject; import org.knime.python2.port.PickledObjectFileStorePortObject; import org.knime.python3.scripting.nodes2.PythonScriptNodeDialog; import org.knime.python3.scripting.nodes2.PythonScriptNodeModel; @@ -79,6 +81,10 @@ public final class PythonScriptNodeFactory extends ConfigurableNodeFactory implements NodeDialogFactory { + private static final NodeLogger LOGGER = NodeLogger.getLogger(PythonScriptNodeFactory.class); + + private static final String PORTGR_ID_PIXI_ENV = "Pixi environment"; + @Override public NodeDialog createNodeDialog() { return new PythonScriptNodeDialog(false); @@ -118,6 +124,18 @@ protected Optional createPortsConfigBuilder() { b.addExtendableInputPortGroup(PORTGR_ID_INP_OBJECT, PickledObjectFileStorePortObject.TYPE); b.addExtendableInputPortGroupWithDefault(PORTGR_ID_INP_TABLE, new PortType[0], new PortType[]{BufferedDataTable.TYPE}, BufferedDataTable.TYPE); + boolean pixiPortAdded = false; + try { + final Class pixiClass = PixiEnvironmentPortObject.class; + final PortType pixiPortType = PixiEnvironmentPortObject.TYPE_OPTIONAL; + b.addOptionalInputPortGroup(PORTGR_ID_PIXI_ENV, pixiPortType); + pixiPortAdded = true; + LOGGER.info("Successfully added optional Pixi environment port"); + } catch (NoClassDefFoundError e) { + LOGGER.warn("Could not add Pixi environment port - pixi bundle not available: " + e.getMessage()); + } catch (Exception e) { + LOGGER.error("Unexpected error adding Pixi environment port", e); + } b.addExtendableOutputPortGroupWithDefault(PORTGR_ID_OUT_TABLE, new PortType[0], new PortType[]{BufferedDataTable.TYPE}, BufferedDataTable.TYPE); b.addExtendableOutputPortGroup(PORTGR_ID_OUT_IMAGE, ImagePortObject.TYPE); From ecb8f801f1659d61803d6b64582c2d3af23753cc Mon Sep 17 00:00:00 2001 From: Marc Lehner Date: Wed, 7 Jan 2026 14:45:31 +0100 Subject: [PATCH 02/34] AP-25245: move ports to dedicated plugin AP-25245 () --- org.knime.python3.scripting.nodes/META-INF/MANIFEST.MF | 2 +- .../scripting/nodes/AbstractPythonScriptingNodeModel.java | 2 +- .../knime/python3/scripting/nodes/PortsConfigurationUtils.java | 2 +- .../python3/scripting/nodes/script/PythonScriptNodeFactory.java | 2 +- .../knime/python3/scripting/nodes2/PythonScriptNodeModel.java | 2 +- .../scripting/nodes2/PythonScriptPortsConfiguration.java | 2 +- .../scripting/nodes2/PythonScriptingInputOutputModelUtils.java | 2 +- .../scripting/nodes2/script/PythonScriptNodeFactory.java | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/org.knime.python3.scripting.nodes/META-INF/MANIFEST.MF b/org.knime.python3.scripting.nodes/META-INF/MANIFEST.MF index c39c9471a..1ad1db24f 100644 --- a/org.knime.python3.scripting.nodes/META-INF/MANIFEST.MF +++ b/org.knime.python3.scripting.nodes/META-INF/MANIFEST.MF @@ -23,7 +23,7 @@ Require-Bundle: org.knime.core;bundle-version="[5.11.0,6.0.0)", org.eclipse.ui;bundle-version="3.119.0", org.knime.conda;bundle-version="[5.11.0,6.0.0)", org.knime.conda.envbundling;bundle-version="[5.10.0,6.0.0)", - org.knime.pixi.nodes;bundle-version="[5.9.0,6.0.0)", + org.knime.pixi.port;bundle-version="[5.10.0,6.0.0)";resolution:=optional, org.knime.core.ui;bundle-version="[5.10.0,6.0.0)", org.knime.workbench.editor;bundle-version="[5.9.0,6.0.0)", org.apache.batik.util;bundle-version="[1.16.0,2.0.0)", diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/AbstractPythonScriptingNodeModel.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/AbstractPythonScriptingNodeModel.java index 830686f4e..84edd3eed 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/AbstractPythonScriptingNodeModel.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/AbstractPythonScriptingNodeModel.java @@ -81,7 +81,7 @@ import org.knime.core.util.PathUtils; import org.knime.core.util.asynclose.AsynchronousCloseableTracker; import org.knime.core.webui.node.view.NodeView; -import org.knime.pixi.nodes.PixiEnvironmentPortObject; +import org.knime.pixi.port.PixiEnvironmentPortObject; import org.knime.python2.PythonCommand; import org.knime.python2.PythonModuleSpec; import org.knime.python2.PythonVersion; diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/PortsConfigurationUtils.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/PortsConfigurationUtils.java index fc49a7bef..695cf26f0 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/PortsConfigurationUtils.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/PortsConfigurationUtils.java @@ -60,7 +60,7 @@ import org.knime.python2.ports.OutputPort; import org.knime.python2.ports.PickledObjectInputPort; import org.knime.python2.ports.PickledObjectOutputPort; -import org.knime.pixi.nodes.PixiEnvironmentPortObject; +import org.knime.pixi.port.PixiEnvironmentPortObject; /** * @author Marcel Wiedenmann, KNIME GmbH, Konstanz, Germany diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/script/PythonScriptNodeFactory.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/script/PythonScriptNodeFactory.java index 147d25b76..ebf8d9a10 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/script/PythonScriptNodeFactory.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/script/PythonScriptNodeFactory.java @@ -63,7 +63,7 @@ import org.knime.core.node.context.NodeCreationConfiguration; import org.knime.core.node.port.PortType; import org.knime.core.node.port.image.ImagePortObject; -import org.knime.pixi.nodes.PixiEnvironmentPortObject; +import org.knime.pixi.port.PixiEnvironmentPortObject; import org.knime.python2.port.PickledObjectFileStorePortObject; import org.knime.python2.ports.InputPort; import org.knime.python2.ports.OutputPort; diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptNodeModel.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptNodeModel.java index 057f44fbb..7efa598e0 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptNodeModel.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptNodeModel.java @@ -88,7 +88,7 @@ import org.knime.core.node.workflow.VariableTypeRegistry; import org.knime.core.util.asynclose.AsynchronousCloseableTracker; import org.knime.core.webui.node.dialog.scripting.ScriptingService.ConsoleText; -import org.knime.pixi.nodes.PixiEnvironmentPortObject; +import org.knime.pixi.port.PixiEnvironmentPortObject; import org.knime.python3.AbstractCondaPythonCommand; import org.knime.python3.PythonCommand; import org.knime.python3.PythonProcessTerminatedException; diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptPortsConfiguration.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptPortsConfiguration.java index 5eab1fd0b..6b223c68e 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptPortsConfiguration.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptPortsConfiguration.java @@ -55,7 +55,7 @@ import org.knime.core.node.context.ports.PortsConfiguration; import org.knime.core.node.port.image.ImagePortObject; import org.knime.core.node.workflow.NodeContext; -import org.knime.pixi.nodes.PixiEnvironmentPortObject; +import org.knime.pixi.port.PixiEnvironmentPortObject; import org.knime.python2.port.PickledObjectFileStorePortObject; /** diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingInputOutputModelUtils.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingInputOutputModelUtils.java index ebc0b38ab..b28e62330 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingInputOutputModelUtils.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingInputOutputModelUtils.java @@ -64,7 +64,7 @@ import org.knime.core.node.workflow.FlowVariable; import org.knime.core.webui.node.dialog.scripting.InputOutputModel; import org.knime.core.webui.node.dialog.scripting.WorkflowControl.InputPortInfo; -import org.knime.pixi.nodes.PixiEnvironmentPortObject; +import org.knime.pixi.port.PixiEnvironmentPortObject; import org.knime.python2.port.PickledObjectFileStorePortObject; /** diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/script/PythonScriptNodeFactory.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/script/PythonScriptNodeFactory.java index fe0dd6dbb..b13c0b79e 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/script/PythonScriptNodeFactory.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/script/PythonScriptNodeFactory.java @@ -67,7 +67,7 @@ import org.knime.core.webui.node.dialog.NodeDialog; import org.knime.core.webui.node.dialog.NodeDialogFactory; import org.knime.core.webui.node.dialog.NodeDialogManager; -import org.knime.pixi.nodes.PixiEnvironmentPortObject; +import org.knime.pixi.port.PixiEnvironmentPortObject; import org.knime.python2.port.PickledObjectFileStorePortObject; import org.knime.python3.scripting.nodes2.PythonScriptNodeDialog; import org.knime.python3.scripting.nodes2.PythonScriptNodeModel; From 566f3c80e374605993685f1884bfb4bbd9f40e64 Mon Sep 17 00:00:00 2001 From: Marc Lehner Date: Tue, 13 Jan 2026 16:11:24 +0100 Subject: [PATCH 03/34] AP-25245: use pixi run in pixiPythonCommand for all pixi environment ports AP-25245 () --- .../AbstractPythonScriptingNodeModel.java | 23 ++- .../nodes2/PythonScriptNodeModel.java | 21 +-- org.knime.python3/META-INF/MANIFEST.MF | 1 + .../python3/AbstractPixiPythonCommand.java | 164 ++++++++++++++++++ .../org/knime/python3/PixiPythonCommand.java | 86 +++++++++ 5 files changed, 272 insertions(+), 23 deletions(-) create mode 100644 org.knime.python3/src/main/java/org/knime/python3/AbstractPixiPythonCommand.java create mode 100644 org.knime.python3/src/main/java/org/knime/python3/PixiPythonCommand.java diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/AbstractPythonScriptingNodeModel.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/AbstractPythonScriptingNodeModel.java index 84edd3eed..185118072 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/AbstractPythonScriptingNodeModel.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/AbstractPythonScriptingNodeModel.java @@ -103,6 +103,7 @@ import org.knime.python2.ports.PickledObjectOutputPort; import org.knime.python2.ports.Port; import org.knime.python3.AbstractCondaPythonCommand; +import org.knime.python3.PixiPythonCommand; import org.knime.python3.scripting.Python3KernelBackend; import org.knime.python3.scripting.nodes.prefs.Python3ScriptingPreferences; @@ -402,24 +403,20 @@ private static PythonCommand extractPythonCommandFromPixiPort(final PortObject p } final PixiEnvironmentPortObject pixiPort = (PixiEnvironmentPortObject)portObject; - final Path pythonExecPath = pixiPort.getPythonExecutablePath(); - - if (pythonExecPath == null) { - throw new InvalidSettingsException( - "The Pixi environment does not contain a Python executable. " - + "Please ensure the environment includes Python."); - } - + + // Create PixiPythonCommand from the pixi.toml path + final Path pixiTomlPath = pixiPort.getPixiTomlPath(); + final org.knime.python3.PythonCommand pythonCommand = new PixiPythonCommand(pixiTomlPath); + + // Verify that the Python executable exists + final Path pythonExecPath = pythonCommand.getPythonExecutablePath(); if (!Files.exists(pythonExecPath)) { throw new InvalidSettingsException( "The Python executable from the Pixi environment does not exist at path: " + pythonExecPath + ". Please check that the Pixi environment was created successfully."); } - - // Use AbstractCondaPythonCommand which handles environment variable patching - final org.knime.python3.PythonCommand pythonCommand = - new AbstractCondaPythonCommand(pixiPort.getAsCondaEnvironmentDirectory()) {}; - + + LOGGER.debug("Using Python from Pixi environment via pixi run: " + pythonCommand); return new LegacyPythonCommand(pythonCommand); } catch (NoClassDefFoundError e) { diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptNodeModel.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptNodeModel.java index 7efa598e0..a3f84126b 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptNodeModel.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptNodeModel.java @@ -90,6 +90,7 @@ import org.knime.core.webui.node.dialog.scripting.ScriptingService.ConsoleText; import org.knime.pixi.port.PixiEnvironmentPortObject; import org.knime.python3.AbstractCondaPythonCommand; +import org.knime.python3.PixiPythonCommand; import org.knime.python3.PythonCommand; import org.knime.python3.PythonProcessTerminatedException; import org.knime.python3.scripting.nodes2.ConsoleOutputUtils.ConsoleOutputStorage; @@ -332,20 +333,20 @@ private static PythonCommand extractPythonCommandFromPixiPort(final PortObject p } final PixiEnvironmentPortObject pixiPort = (PixiEnvironmentPortObject)portObject; - final Path pythonExecPath = pixiPort.getPythonExecutablePath(); - - if (pythonExecPath == null) { - throw new InvalidSettingsException("The Pixi environment does not contain a Python executable.\nPlease ensure that Python is installed in the Pixi environment."); - } - + + // Create PixiPythonCommand from the pixi.toml path + final Path pixiTomlPath = pixiPort.getPixiTomlPath(); + final PythonCommand pythonCommand = new PixiPythonCommand(pixiTomlPath); + + // Verify that the Python executable exists + final Path pythonExecPath = pythonCommand.getPythonExecutablePath(); if (!Files.exists(pythonExecPath)) { throw new InvalidSettingsException("The Python executable from the Pixi environment does not exist: " + pythonExecPath + ". Please check that the Pixi environment is valid."); } - LOGGER.debug("Using Python executable from Pixi environment: " + pythonExecPath); - - // Use AbstractCondaPythonCommand which handles environment variable patching - return new AbstractCondaPythonCommand(pixiPort.getAsCondaEnvironmentDirectory()) {}; + + LOGGER.debug("Using Python from Pixi environment via pixi run: " + pythonCommand); + return pythonCommand; } catch (NoClassDefFoundError e) { // Pixi bundle not available - this should not happen if the port was added successfully LOGGER.debug("PixiEnvironmentPortObject class not available", e); diff --git a/org.knime.python3/META-INF/MANIFEST.MF b/org.knime.python3/META-INF/MANIFEST.MF index c8075592b..e62ab5e67 100644 --- a/org.knime.python3/META-INF/MANIFEST.MF +++ b/org.knime.python3/META-INF/MANIFEST.MF @@ -23,3 +23,4 @@ Automatic-Module-Name: org.knime.python3 Eclipse-RegisterBuddy: org.knime.ext.py4j Eclipse-BundleShape: dir Bundle-Activator: org.knime.python3.Activator +Import-Package: org.knime.conda.envinstall.pixi diff --git a/org.knime.python3/src/main/java/org/knime/python3/AbstractPixiPythonCommand.java b/org.knime.python3/src/main/java/org/knime/python3/AbstractPixiPythonCommand.java new file mode 100644 index 000000000..5c1d3ff01 --- /dev/null +++ b/org.knime.python3/src/main/java/org/knime/python3/AbstractPixiPythonCommand.java @@ -0,0 +1,164 @@ +/* + * ------------------------------------------------------------------------ + * + * Copyright by KNIME AG, Zurich, Switzerland + * Website: http://www.knime.com; Email: contact@knime.com + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License, Version 3, as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + * + * Additional permission under GNU GPL version 3 section 7: + * + * KNIME interoperates with ECLIPSE solely via ECLIPSE's plug-in APIs. + * Hence, KNIME and ECLIPSE are both independent programs and are not + * derived from each other. Should, however, the interpretation of the + * GNU GPL Version 3 ("License") under any applicable laws result in + * KNIME and ECLIPSE being a combined program, KNIME AG herewith grants + * you the additional permission to use and propagate KNIME together with + * ECLIPSE with only the license terms in place for ECLIPSE applying to + * ECLIPSE and the GNU GPL Version 3 applying for KNIME, provided the + * license terms of ECLIPSE themselves allow for the respective use and + * propagation of ECLIPSE together with KNIME. + * + * Additional permission relating to nodes for KNIME that extend the Node + * Extension (and in particular that are based on subclasses of NodeModel, + * NodeDialog, and NodeView) and that only interoperate with KNIME through + * standard APIs ("Nodes"): + * Nodes are deemed to be separate and independent programs and to not be + * covered works. Notwithstanding anything to the contrary in the + * License, the License does not apply to Nodes, you are not required to + * license Nodes under the License, and you are granted a license to + * prepare and propagate Nodes, in each case even if such Nodes are + * propagated with or for interoperation with KNIME. The owner of a Node + * may freely choose the license terms applicable to such Node, including + * when such Node is propagated with or for interoperation with KNIME. + * --------------------------------------------------------------------- + * + * History + * Jan 13, 2026 (Marc Lehner): created + */ +package org.knime.python3; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import org.knime.conda.envinstall.pixi.PixiBinary; +import org.knime.conda.envinstall.pixi.PixiBinary.PixiBinaryLocationException; + +/** + * Abstract base class for Python commands that use Pixi environments. Executes Python via {@code pixi run python} + * to ensure proper environment activation and variable setup. + *

+ * Implementation note: Implementors must provide value-based implementations of {@link #hashCode()}, + * {@link #equals(Object)}, and {@link #toString()}. + * + * @author Marc Lehner, KNIME GmbH, Zurich, Switzerland + */ +abstract class AbstractPixiPythonCommand implements PythonCommand { + + private final Path m_pixiTomlPath; + + private final String m_pixiEnvironmentName; + + /** + * @param pixiTomlPath The path to the pixi.toml manifest file that describes the environment + * @param environmentName The name of the environment within the pixi project (typically "default") + */ + protected AbstractPixiPythonCommand(final Path pixiTomlPath, final String environmentName) { + m_pixiTomlPath = Objects.requireNonNull(pixiTomlPath, "pixiTomlPath must not be null"); + m_pixiEnvironmentName = Objects.requireNonNull(environmentName, "environmentName must not be null"); + } + + /** + * @param pixiTomlPath The path to the pixi.toml manifest file that describes the environment + */ + protected AbstractPixiPythonCommand(final Path pixiTomlPath) { + this(pixiTomlPath, "default"); + } + + @Override + public ProcessBuilder createProcessBuilder() { + try { + final String pixiBinaryPath = PixiBinary.getPixiBinaryPath(); + final List command = new ArrayList<>(); + command.add(pixiBinaryPath); + command.add("run"); + command.add("--manifest-path"); + command.add(m_pixiTomlPath.toString()); + command.add("--environment"); + command.add(m_pixiEnvironmentName); + command.add("--no-progress"); + command.add("python"); + return new ProcessBuilder(command); + } catch (PixiBinaryLocationException ex) { + throw new IllegalStateException( + "Could not locate pixi binary. Please ensure the pixi bundle is properly installed.", ex); + } + } + + @Override + public Path getPythonExecutablePath() { + // Resolve the actual Python executable path within the environment + // This is used for informational purposes only, not for execution + final Path projectDir = m_pixiTomlPath.getParent(); + final Path envDir = projectDir.resolve(".pixi").resolve("envs").resolve(m_pixiEnvironmentName); + final boolean isWindows = System.getProperty("os.name").toLowerCase().contains("win"); + final Path pythonPath = isWindows + ? envDir.resolve("python.exe") + : envDir.resolve("bin").resolve("python"); + + // Return the path even if it doesn't exist yet - the environment might not be installed + // The caller is responsible for checking existence if needed + return pythonPath; + } + + /** + * @return The path to the pixi.toml manifest file + */ + protected Path getPixiTomlPath() { + return m_pixiTomlPath; + } + + /** + * @return The environment name + */ + protected String getEnvironmentName() { + return m_pixiEnvironmentName; + } + + @Override + public int hashCode() { + return Objects.hash(m_pixiTomlPath, m_pixiEnvironmentName); + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final AbstractPixiPythonCommand other = (AbstractPixiPythonCommand)obj; + return Objects.equals(m_pixiTomlPath, other.m_pixiTomlPath) + && Objects.equals(m_pixiEnvironmentName, other.m_pixiEnvironmentName); + } + + @Override + public String toString() { + return "pixi run --manifest-path " + m_pixiTomlPath + " --environment " + m_pixiEnvironmentName + + " --no-progress python"; + } +} diff --git a/org.knime.python3/src/main/java/org/knime/python3/PixiPythonCommand.java b/org.knime.python3/src/main/java/org/knime/python3/PixiPythonCommand.java new file mode 100644 index 000000000..cb9f0eed0 --- /dev/null +++ b/org.knime.python3/src/main/java/org/knime/python3/PixiPythonCommand.java @@ -0,0 +1,86 @@ +/* + * ------------------------------------------------------------------------ + * + * Copyright by KNIME AG, Zurich, Switzerland + * Website: http://www.knime.com; Email: contact@knime.com + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License, Version 3, as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + * + * Additional permission under GNU GPL version 3 section 7: + * + * KNIME interoperates with ECLIPSE solely via ECLIPSE's plug-in APIs. + * Hence, KNIME and ECLIPSE are both independent programs and are not + * derived from each other. Should, however, the interpretation of the + * GNU GPL Version 3 ("License") under any applicable laws result in + * KNIME and ECLIPSE being a combined program, KNIME AG herewith grants + * you the additional permission to use and propagate KNIME together with + * ECLIPSE with only the license terms in place for ECLIPSE applying to + * ECLIPSE and the GNU GPL Version 3 applying for KNIME, provided the + * license terms of ECLIPSE themselves allow for the respective use and + * propagation of ECLIPSE together with KNIME. + * + * Additional permission relating to nodes for KNIME that extend the Node + * Extension (and in particular that are based on subclasses of NodeModel, + * NodeDialog, and NodeView) and that only interoperate with KNIME through + * standard APIs ("Nodes"): + * Nodes are deemed to be separate and independent programs and to not be + * covered works. Notwithstanding anything to the contrary in the + * License, the License does not apply to Nodes, you are not required to + * license Nodes under the License, and you are granted a license to + * prepare and propagate Nodes, in each case even if such Nodes are + * propagated with or for interoperation with KNIME. The owner of a Node + * may freely choose the license terms applicable to such Node, including + * when such Node is propagated with or for interoperation with KNIME. + * --------------------------------------------------------------------- + * + * History + * Jan 13, 2026 (Marc Lehner): created + */ +package org.knime.python3; + +import java.nio.file.Path; + +/** + * Pixi-specific implementation of {@link PythonCommand}. Executes Python processes via {@code pixi run python} + * to ensure proper environment activation and variable setup. + *

+ * This command resolves the pixi binary and constructs a command line that invokes Python through pixi's + * environment runner, which handles all necessary environment setup automatically. + * + * @author Marc Lehner, KNIME GmbH, Zurich, Switzerland + */ +public final class PixiPythonCommand extends AbstractPixiPythonCommand { + + /** + * Constructs a {@link PythonCommand} that describes a Python process run via pixi in the environment + * identified by the given pixi.toml manifest file.
+ * The validity of the given arguments is not tested. + * + * @param pixiTomlPath The path to the pixi.toml manifest file that describes the environment. + * @param environmentName The name of the environment within the pixi project (e.g., "default"). + */ + public PixiPythonCommand(final Path pixiTomlPath, final String environmentName) { + super(pixiTomlPath, environmentName); + } + + /** + * Constructs a {@link PythonCommand} that describes a Python process run via pixi in the default environment + * identified by the given pixi.toml manifest file.
+ * The validity of the given arguments is not tested. + * + * @param pixiTomlPath The path to the pixi.toml manifest file that describes the environment. + */ + public PixiPythonCommand(final Path pixiTomlPath) { + super(pixiTomlPath, "default"); + } +} From 9682bbfc34536536f20d0592c55fcebd07ce4653 Mon Sep 17 00:00:00 2001 From: Marc Lehner Date: Wed, 14 Jan 2026 08:42:56 +0100 Subject: [PATCH 04/34] AP-25245: add pixi port to python view AP-25245 () --- .../META-INF/MANIFEST.MF | 1 + .../nodes/view/PythonViewNodeFactory.java | 19 +++++++++++++++++-- .../scripting/nodes2/PythonIOUtils.java | 16 +++++++++++----- .../nodes2/view/PythonViewNodeFactory.java | 16 ++++++++++++++++ 4 files changed, 45 insertions(+), 7 deletions(-) diff --git a/org.knime.python3.scripting.nodes/META-INF/MANIFEST.MF b/org.knime.python3.scripting.nodes/META-INF/MANIFEST.MF index 1ad1db24f..c3d82d285 100644 --- a/org.knime.python3.scripting.nodes/META-INF/MANIFEST.MF +++ b/org.knime.python3.scripting.nodes/META-INF/MANIFEST.MF @@ -24,6 +24,7 @@ Require-Bundle: org.knime.core;bundle-version="[5.11.0,6.0.0)", org.knime.conda;bundle-version="[5.11.0,6.0.0)", org.knime.conda.envbundling;bundle-version="[5.10.0,6.0.0)", org.knime.pixi.port;bundle-version="[5.10.0,6.0.0)";resolution:=optional, + org.knime.pixi.nodes;bundle-version="[5.9.0,6.0.0)";resolution:=optional, org.knime.core.ui;bundle-version="[5.10.0,6.0.0)", org.knime.workbench.editor;bundle-version="[5.9.0,6.0.0)", org.apache.batik.util;bundle-version="[1.16.0,2.0.0)", diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/view/PythonViewNodeFactory.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/view/PythonViewNodeFactory.java index 8a8464504..bb33637c2 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/view/PythonViewNodeFactory.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/view/PythonViewNodeFactory.java @@ -57,15 +57,18 @@ import org.knime.core.node.BufferedDataTable; import org.knime.core.node.ConfigurableNodeFactory; import org.knime.core.node.NodeDialogPane; +import org.knime.core.node.NodeLogger; import org.knime.core.node.NodeView; import org.knime.core.node.context.NodeCreationConfiguration; import org.knime.core.node.context.ports.PortsConfiguration; import org.knime.core.node.port.PortType; import org.knime.core.node.port.image.ImagePortObject; import org.knime.core.webui.node.view.NodeViewFactory; +import org.knime.pixi.port.PixiEnvironmentPortObject; import org.knime.python2.port.PickledObjectFileStorePortObject; import org.knime.python2.ports.ImageOutputPort; import org.knime.python2.ports.OutputPort; +import org.knime.python3.scripting.nodes.PortsConfigurationUtils; import org.knime.python3.views.HtmlFileNodeView; /** @@ -75,12 +78,24 @@ public final class PythonViewNodeFactory extends ConfigurableNodeFactory implements NodeViewFactory { + private static final NodeLogger LOGGER = NodeLogger.getLogger(PythonViewNodeFactory.class); + @Override protected Optional createPortsConfigBuilder() { final var b = new PortsConfigurationBuilder(); b.addExtendableInputPortGroup("Input object (pickled)", PickledObjectFileStorePortObject.TYPE); b.addExtendableInputPortGroupWithDefault("Input table", new PortType[0], new PortType[]{BufferedDataTable.TYPE}, BufferedDataTable.TYPE); + try { + final Class pixiClass = PixiEnvironmentPortObject.class; + final PortType pixiPortType = PixiEnvironmentPortObject.TYPE_OPTIONAL; + b.addOptionalInputPortGroup("Pixi environment", pixiPortType); + LOGGER.info("Successfully added optional Pixi environment port"); + } catch (NoClassDefFoundError e) { + LOGGER.warn("Could not add Pixi environment port - pixi bundle not available: " + e.getMessage()); + } catch (Exception e) { + LOGGER.error("Unexpected error adding Pixi environment port", e); + } b.addOptionalOutputPortGroup("Output image", ImagePortObject.TYPE); return Optional.of(b); } @@ -88,8 +103,8 @@ protected Optional createPortsConfigBuilder() { @Override protected PythonViewNodeModel createNodeModel(final NodeCreationConfiguration creationConfig) { final var config = creationConfig.getPortConfig().get(); // NOSONAR - // Python View nodes currently don't support Pixi environment ports - return new PythonViewNodeModel(createInputPorts(config), createOutputPorts(config), false); + return new PythonViewNodeModel(createInputPorts(config), createOutputPorts(config), + PortsConfigurationUtils.hasPixiPort(config)); } @Override diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonIOUtils.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonIOUtils.java index 06bcb9375..0779375ea 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonIOUtils.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonIOUtils.java @@ -79,6 +79,7 @@ import org.knime.core.node.port.image.ImagePortObjectSpec; import org.knime.core.node.port.inactive.InactiveBranchPortObject; import org.knime.core.util.PathUtils; +import org.knime.pixi.port.PixiEnvironmentPortObject; import org.knime.python2.port.PickledObjectFile; import org.knime.python2.port.PickledObjectFileStorePortObject; import org.knime.python3.PythonDataSource; @@ -100,10 +101,12 @@ private PythonIOUtils() { /** * Create an array of Python data sources for the given input ports. The input ports can be either a - * {@link BufferedDataTable} or a {@link PickledObjectFileStorePortObject}. + * {@link BufferedDataTable}, a {@link PickledObjectFileStorePortObject}, or a {@link PixiEnvironmentPortObject}. + * Note that {@link PixiEnvironmentPortObject}s are filtered out as they are only used for environment + * configuration and not passed to Python as data. * - * @param data a list of port objects. Only {@link BufferedDataTable} and {@link PickledObjectFileStorePortObject} - * are supported. + * @param data a list of port objects. Only {@link BufferedDataTable}, {@link PickledObjectFileStorePortObject}, + * and {@link PixiEnvironmentPortObject} are supported. * @param tableConverter a table converter that is used to convert the {@link BufferedDataTable}s to Python sources * @param exec for progress reporting and cancellation * @return an array of Python data sources @@ -120,9 +123,12 @@ static PythonDataSource[] createSources(final PortObject[] data, final PythonArr final var tablePortObjects = Arrays.stream(data) // .filter(BufferedDataTable.class::isInstance) // .toArray(BufferedDataTable[]::new); + final var pixiPortObjects = Arrays.stream(data) // + .filter(PixiEnvironmentPortObject.class::isInstance) // + .toArray(PixiEnvironmentPortObject[]::new); - // Make sure that all ports are tables or pickled port objects - if (pickledPortObjects.length + tablePortObjects.length < data.length) { + // Make sure that all ports are tables, pickled port objects, or pixi environment ports + if (pickledPortObjects.length + tablePortObjects.length + pixiPortObjects.length < data.length) { throw new IllegalArgumentException("Unsupported port type connected. This is an implementation error."); } diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/view/PythonViewNodeFactory.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/view/PythonViewNodeFactory.java index 43eb35c8c..c5cf3efa1 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/view/PythonViewNodeFactory.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/view/PythonViewNodeFactory.java @@ -57,6 +57,7 @@ import org.knime.core.node.BufferedDataTable; import org.knime.core.node.ConfigurableNodeFactory; import org.knime.core.node.NodeDialogPane; +import org.knime.core.node.NodeLogger; import org.knime.core.node.context.NodeCreationConfiguration; import org.knime.core.node.port.PortType; import org.knime.core.node.port.image.ImagePortObject; @@ -65,6 +66,7 @@ import org.knime.core.webui.node.dialog.NodeDialogManager; import org.knime.core.webui.node.view.NodeView; import org.knime.core.webui.node.view.NodeViewFactory; +import org.knime.pixi.port.PixiEnvironmentPortObject; import org.knime.python2.port.PickledObjectFileStorePortObject; import org.knime.python3.scripting.nodes2.PythonScriptNodeDialog; import org.knime.python3.scripting.nodes2.PythonScriptNodeModel; @@ -79,6 +81,10 @@ public class PythonViewNodeFactory extends ConfigurableNodeFactory implements NodeDialogFactory, NodeViewFactory { + private static final NodeLogger LOGGER = NodeLogger.getLogger(PythonViewNodeFactory.class); + + private static final String PORTGR_ID_PIXI_ENV = "Pixi environment"; + @Override public NodeDialog createNodeDialog() { return new PythonScriptNodeDialog(true); @@ -127,6 +133,16 @@ protected Optional createPortsConfigBuilder() { b.addExtendableInputPortGroup(PORTGR_ID_INP_OBJECT, PickledObjectFileStorePortObject.TYPE); b.addExtendableInputPortGroupWithDefault(PORTGR_ID_INP_TABLE, new PortType[0], new PortType[]{BufferedDataTable.TYPE}, BufferedDataTable.TYPE); + try { + final Class pixiClass = PixiEnvironmentPortObject.class; + final PortType pixiPortType = PixiEnvironmentPortObject.TYPE_OPTIONAL; + b.addOptionalInputPortGroup(PORTGR_ID_PIXI_ENV, pixiPortType); + LOGGER.info("Successfully added optional Pixi environment port"); + } catch (NoClassDefFoundError e) { + LOGGER.warn("Could not add Pixi environment port - pixi bundle not available: " + e.getMessage()); + } catch (Exception e) { + LOGGER.error("Unexpected error adding Pixi environment port", e); + } b.addOptionalOutputPortGroup(PORTGR_ID_OUT_IMAGE, ImagePortObject.TYPE); return Optional.of(b); } From 65cbd5eede5c4928066254a8995cdbefc603a1a3 Mon Sep 17 00:00:00 2001 From: Marc Lehner Date: Wed, 14 Jan 2026 16:42:40 +0100 Subject: [PATCH 05/34] AP-25245: add pixi port to interactive execution AP-25245 () --- .../nodes2/PythonScriptingService.java | 75 ++++++++++++++++++- 1 file changed, 72 insertions(+), 3 deletions(-) diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingService.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingService.java index be15da380..b3b5dfae0 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingService.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingService.java @@ -67,7 +67,9 @@ import org.knime.core.data.filestore.internal.NotInWorkflowWriteFileStoreHandler; import org.knime.core.node.CanceledExecutionException; import org.knime.core.node.ExecutionMonitor; +import org.knime.core.node.InvalidSettingsException; import org.knime.core.node.NodeLogger; +import org.knime.core.node.port.PortObject; import org.knime.core.node.workflow.FlowObjectStack; import org.knime.core.node.workflow.NodeContext; import org.knime.core.util.ThreadUtils; @@ -75,6 +77,9 @@ import org.knime.core.webui.node.dialog.scripting.CodeGenerationRequest; import org.knime.core.webui.node.dialog.scripting.InputOutputModel; import org.knime.core.webui.node.dialog.scripting.ScriptingService; +import org.knime.pixi.port.PixiEnvironmentPortObject; +import org.knime.python3.PixiPythonCommand; +import org.knime.python3.PythonCommand; import org.knime.python3.scripting.nodes2.PythonScriptingService.ExecutableOption.ExecutableOptionType; import org.knime.python3.scripting.nodes2.PythonScriptingSession.ExecutionInfo; import org.knime.python3.scripting.nodes2.PythonScriptingSession.ExecutionStatus; @@ -246,13 +251,36 @@ private void startNewInteractiveSession() throws IOException, InterruptedExcepti // Start the interactive Python session and setup the IO final var workflowControl = getWorkflowControl(); - final var pythonCommand = - ExecutableSelectionUtils.getPythonCommand(getExecutableOption(m_executableSelection)); + final var inputData = workflowControl.getInputData(); + + // Check if Pixi port is connected (it's the last port if present) + PythonCommand pythonCommand = null; + PortObject[] dataPortObjects = inputData; // By default, all inputs are data ports + + if (m_ports.hasPixiPort() && inputData != null && inputData.length > 0) { + // The Pixi port is at the end, after all data ports + final int pixiPortIndex = inputData.length - 1; + try { + pythonCommand = extractPythonCommandFromPixiPort(inputData[pixiPortIndex]); + if (pythonCommand != null) { + LOGGER.info("Using Python environment from connected Pixi port for interactive session"); + // Filter out Pixi port from data ports - it's not a data port for setupIO + dataPortObjects = java.util.Arrays.copyOf(inputData, inputData.length - 1); + } + } catch (Exception e) { + LOGGER.warn("Failed to extract Python command from Pixi port: " + e.getMessage()); + } + } + + // Fall back to user selection if no Pixi port or extraction failed + if (pythonCommand == null) { + pythonCommand = ExecutableSelectionUtils.getPythonCommand(getExecutableOption(m_executableSelection)); + } // TODO report the progress of converting the tables using the ExecutionMonitor? m_interactiveSession = new PythonScriptingSession(pythonCommand, PythonScriptingService.this::addConsoleOutputEvent, new DialogFileStoreHandlerSupplier()); - m_interactiveSession.setupIO(workflowControl.getInputData(), getSupportedFlowVariables(), + m_interactiveSession.setupIO(dataPortObjects, getSupportedFlowVariables(), m_ports.getNumOutTables(), m_ports.getNumOutImages(), m_ports.getNumOutObjects(), m_hasView, new ExecutionMonitor()); } @@ -501,4 +529,45 @@ public void close() { } } } + + /** + * Extract the Python command from a PixiEnvironmentPortObject. + * + * @param portObject the port object (may be null if optional port is not connected) + * @return the Python command, or null if the port is not connected or doesn't contain a valid Python executable + * @throws InvalidSettingsException if the Python executable path from the Pixi environment doesn't exist + */ + private static PythonCommand extractPythonCommandFromPixiPort(final PortObject portObject) + throws InvalidSettingsException { + if (portObject == null) { + return null; + } + + try { + // Check if this is a Pixi environment port object + if (!(portObject instanceof PixiEnvironmentPortObject)) { + return null; + } + + final PixiEnvironmentPortObject pixiPort = (PixiEnvironmentPortObject)portObject; + + // Create PixiPythonCommand from the pixi.toml path + final Path pixiTomlPath = pixiPort.getPixiTomlPath(); + final PythonCommand pythonCommand = new PixiPythonCommand(pixiTomlPath); + + // Verify that the Python executable exists + final Path pythonExecPath = pythonCommand.getPythonExecutablePath(); + if (!Files.exists(pythonExecPath)) { + throw new InvalidSettingsException("The Python executable from the Pixi environment does not exist: " + + pythonExecPath + ". Please check that the Pixi environment is valid."); + } + + LOGGER.debug("Using Python from Pixi environment via pixi run: " + pythonCommand); + return pythonCommand; + } catch (NoClassDefFoundError e) { + // Pixi bundle not available - this should not happen if the port was added successfully + LOGGER.debug("PixiEnvironmentPortObject class not available", e); + return null; + } + } } From 5644fcb6161dec90184e8740ea896aa317268b92 Mon Sep 17 00:00:00 2001 From: Marc Lehner Date: Mon, 19 Jan 2026 18:37:14 +0100 Subject: [PATCH 06/34] AP-25245: replace pixi port with python port AP-25245 () --- .../nodes/PythonExtensionPreferences.java | 2 +- .../nodes/PythonNodeGatewayFactory.java | 4 +- .../META-INF/MANIFEST.MF | 1 - .../AbstractPythonScriptingNodeModel.java | 31 +++++---- .../nodes/PortsConfigurationUtils.java | 17 ++--- .../prefs/BundledCondaEnvironmentConfig.java | 3 +- .../prefs/Python3ScriptingPreferences.java | 2 +- .../nodes/script/PythonScriptNodeFactory.java | 15 ++--- .../nodes/view/PythonViewNodeFactory.java | 13 ++-- .../scripting/nodes2/PythonIOUtils.java | 22 ++++--- .../nodes2/PythonScriptNodeModel.java | 63 +++++++++---------- .../PythonScriptPortsConfiguration.java | 56 +++++++++++------ .../PythonScriptingInputOutputModelUtils.java | 12 +++- .../nodes2/PythonScriptingService.java | 42 ++++++------- .../nodes2/PythonScriptingSession.java | 47 +++++++++++++- .../script/PythonScriptNodeFactory.java | 20 +++--- .../nodes2/view/PythonViewNodeFactory.java | 18 +++--- org.knime.python3/META-INF/MANIFEST.MF | 3 +- 18 files changed, 223 insertions(+), 148 deletions(-) diff --git a/org.knime.python3.nodes/src/main/java/org/knime/python3/nodes/PythonExtensionPreferences.java b/org.knime.python3.nodes/src/main/java/org/knime/python3/nodes/PythonExtensionPreferences.java index f483668fb..6aad57b37 100644 --- a/org.knime.python3.nodes/src/main/java/org/knime/python3/nodes/PythonExtensionPreferences.java +++ b/org.knime.python3.nodes/src/main/java/org/knime/python3/nodes/PythonExtensionPreferences.java @@ -59,9 +59,9 @@ import java.util.Optional; import java.util.stream.Stream; +import org.knime.python3.CondaPythonCommand; import org.knime.conda.prefs.CondaPreferences; import org.knime.core.node.NodeLogger; -import org.knime.python3.CondaPythonCommand; import org.knime.python3.PythonCommand; import org.knime.python3.SimplePythonCommand; import org.yaml.snakeyaml.Yaml; diff --git a/org.knime.python3.nodes/src/main/java/org/knime/python3/nodes/PythonNodeGatewayFactory.java b/org.knime.python3.nodes/src/main/java/org/knime/python3/nodes/PythonNodeGatewayFactory.java index 12c6a7724..c5c0cdf6a 100644 --- a/org.knime.python3.nodes/src/main/java/org/knime/python3/nodes/PythonNodeGatewayFactory.java +++ b/org.knime.python3.nodes/src/main/java/org/knime/python3/nodes/PythonNodeGatewayFactory.java @@ -52,12 +52,12 @@ import java.nio.file.Path; import java.util.Objects; +import org.knime.python3.BundledPythonCommand; import org.knime.conda.envbundling.environment.CondaEnvironmentRegistry; import org.knime.python3.Activator; -import org.knime.python3.BundledPythonCommand; import org.knime.python3.FreshPythonGatewayFactory; -import org.knime.python3.Python3SourceDirectory; import org.knime.python3.PythonCommand; +import org.knime.python3.Python3SourceDirectory; import org.knime.python3.PythonEntryPointUtils; import org.knime.python3.PythonGateway; import org.knime.python3.PythonGatewayFactory; diff --git a/org.knime.python3.scripting.nodes/META-INF/MANIFEST.MF b/org.knime.python3.scripting.nodes/META-INF/MANIFEST.MF index c3d82d285..1ad1db24f 100644 --- a/org.knime.python3.scripting.nodes/META-INF/MANIFEST.MF +++ b/org.knime.python3.scripting.nodes/META-INF/MANIFEST.MF @@ -24,7 +24,6 @@ Require-Bundle: org.knime.core;bundle-version="[5.11.0,6.0.0)", org.knime.conda;bundle-version="[5.11.0,6.0.0)", org.knime.conda.envbundling;bundle-version="[5.10.0,6.0.0)", org.knime.pixi.port;bundle-version="[5.10.0,6.0.0)";resolution:=optional, - org.knime.pixi.nodes;bundle-version="[5.9.0,6.0.0)";resolution:=optional, org.knime.core.ui;bundle-version="[5.10.0,6.0.0)", org.knime.workbench.editor;bundle-version="[5.9.0,6.0.0)", org.apache.batik.util;bundle-version="[1.16.0,2.0.0)", diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/AbstractPythonScriptingNodeModel.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/AbstractPythonScriptingNodeModel.java index 185118072..f30ad5ca4 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/AbstractPythonScriptingNodeModel.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/AbstractPythonScriptingNodeModel.java @@ -81,7 +81,8 @@ import org.knime.core.util.PathUtils; import org.knime.core.util.asynclose.AsynchronousCloseableTracker; import org.knime.core.webui.node.view.NodeView; -import org.knime.pixi.port.PixiEnvironmentPortObject; + +import org.knime.pixi.port.PythonEnvironmentPortObject; import org.knime.python2.PythonCommand; import org.knime.python2.PythonModuleSpec; import org.knime.python2.PythonVersion; @@ -188,15 +189,15 @@ private static final PortType[] toPortTypes(final Port[] ports, final boolean ha if (!hasPixiPort) { return toPortTypes(ports); } - // Add the optional Pixi port at the end of the input ports + // Add the optional Python environment port at the end of the input ports final PortType[] portTypes = new PortType[ports.length + 1]; for (int i = 0; i < ports.length; i++) { portTypes[i] = ports[i].getPortType(); } try { - portTypes[ports.length] = PixiEnvironmentPortObject.TYPE_OPTIONAL; + portTypes[ports.length] = PythonEnvironmentPortObject.TYPE_OPTIONAL; } catch (NoClassDefFoundError e) { - throw new IllegalStateException("Could not load PixiEnvironmentPortObject class", e); + throw new IllegalStateException("Could not load PythonEnvironmentPortObject class", e); } return portTypes; } @@ -385,7 +386,7 @@ private static Path persistedViewPath(final File nodeInternDir) { } /** - * Extract the Python command from a PixiEnvironmentPortObject. + * Extract the Python command from a PythonEnvironmentPortObject. * * @param portObject the port object (may be null if optional port is not connected) * @return the Python command, or null if the port is not connected or doesn't contain a valid Python executable @@ -398,14 +399,20 @@ private static PythonCommand extractPythonCommandFromPixiPort(final PortObject p } try { - if (!(portObject instanceof PixiEnvironmentPortObject)) { + if (!(portObject instanceof PythonEnvironmentPortObject)) { return null; } - - final PixiEnvironmentPortObject pixiPort = (PixiEnvironmentPortObject)portObject; + + // Handle PythonEnvironmentPortObject + final PythonEnvironmentPortObject pythonEnvPort = (PythonEnvironmentPortObject)portObject; + final Path pixiTomlPath; + try { + pixiTomlPath = pythonEnvPort.getPixiEnvironmentPath().resolve("pixi.toml"); + } catch (IOException e) { + throw new InvalidSettingsException("Failed to get pixi.toml path from PythonEnvironmentPortObject: " + e.getMessage(), e); + } // Create PixiPythonCommand from the pixi.toml path - final Path pixiTomlPath = pixiPort.getPixiTomlPath(); final org.knime.python3.PythonCommand pythonCommand = new PixiPythonCommand(pixiTomlPath); // Verify that the Python executable exists @@ -420,8 +427,8 @@ private static PythonCommand extractPythonCommandFromPixiPort(final PortObject p return new LegacyPythonCommand(pythonCommand); } catch (NoClassDefFoundError e) { - // Pixi nodes bundle is not available - this is fine since it's optional - LOGGER.debug("PixiEnvironmentPortObject class not available - pixi nodes bundle may not be installed", e); + // Python environment bundle is not available - this is fine since it's optional + LOGGER.debug("PythonEnvironmentPortObject class not available - bundle may not be installed", e); return null; } } @@ -470,7 +477,7 @@ private void pushNewFlowVariable(final FlowVariable variable) { } /** - * Wraps a {@link org.knime.python3.PythonCommand} into the legacy implementation for using it in a + * Wraps a {@link org.knime.pixi.port.PythonCommand} into the legacy implementation for using it in a * {@link PythonKernelBackend}. */ private static final class LegacyPythonCommand implements PythonCommand { diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/PortsConfigurationUtils.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/PortsConfigurationUtils.java index 695cf26f0..ccfe6766a 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/PortsConfigurationUtils.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/PortsConfigurationUtils.java @@ -52,6 +52,7 @@ import org.knime.core.node.context.ports.PortsConfiguration; import org.knime.core.node.port.PortType; import org.knime.core.node.port.image.ImagePortObject; +import org.knime.pixi.port.PythonEnvironmentPortObject; import org.knime.python2.port.PickledObjectFileStorePortObject; import org.knime.python2.ports.DataTableInputPort; import org.knime.python2.ports.DataTableOutputPort; @@ -60,7 +61,7 @@ import org.knime.python2.ports.OutputPort; import org.knime.python2.ports.PickledObjectInputPort; import org.knime.python2.ports.PickledObjectOutputPort; -import org.knime.pixi.port.PixiEnvironmentPortObject; + /** * @author Marcel Wiedenmann, KNIME GmbH, Konstanz, Germany @@ -73,22 +74,22 @@ private PortsConfigurationUtils() { } /** - * Check if the ports configuration contains a Pixi environment port. + * Check if the ports configuration contains a Python environment port. * * @param config the ports configuration - * @return true if a Pixi environment port is present + * @return true if a Python environment port is present */ public static boolean hasPixiPort(final PortsConfiguration config) { final PortType[] inTypes = config.getInputPorts(); try { - // Check if any input port is a PixiEnvironmentPortObject + // Check if any input port is a PythonEnvironmentPortObject for (final PortType inType : inTypes) { - if (inType.equals(PixiEnvironmentPortObject.TYPE) || inType.equals(PixiEnvironmentPortObject.TYPE_OPTIONAL)) { + if (inType.equals(PythonEnvironmentPortObject.TYPE) || inType.equals(PythonEnvironmentPortObject.TYPE_OPTIONAL)) { return true; } } } catch (NoClassDefFoundError e) { - // Pixi nodes bundle is not available - this is fine since it's optional + // Python environment bundle is not available - this is fine since it's optional } return false; } @@ -133,9 +134,9 @@ public static InputPort[] createInputPorts(final PortsConfiguration config) { private static boolean isPixiPort(final PortType inType) { try { - return inType.equals(PixiEnvironmentPortObject.TYPE) || inType.equals(PixiEnvironmentPortObject.TYPE_OPTIONAL); + return inType.equals(PythonEnvironmentPortObject.TYPE) || inType.equals(PythonEnvironmentPortObject.TYPE_OPTIONAL); } catch (NoClassDefFoundError e) { - // Pixi nodes bundle is not available - this is fine since it's optional + // Python environment bundle is not available - this is fine since it's optional return false; } } diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/BundledCondaEnvironmentConfig.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/BundledCondaEnvironmentConfig.java index 8ccf449a2..15f365b2b 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/BundledCondaEnvironmentConfig.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/BundledCondaEnvironmentConfig.java @@ -52,6 +52,7 @@ import org.knime.core.node.NodeLogger; import org.knime.core.node.defaultnodesettings.SettingsModelString; import org.knime.python3.BundledPythonCommand; +import org.knime.python3.PythonCommand; /** * The BundledCondaEnvironmentConfig is a PythonEnvironmentConfig that points to a bundled conda environment which is @@ -106,7 +107,7 @@ public boolean isAvailable() { } @Override - public BundledPythonCommand getPythonCommand() { + public PythonCommand getPythonCommand() { final var condaEnv = CondaEnvironmentRegistry.getEnvironment(m_bundledCondaEnvironment.getStringValue()); if (condaEnv == null) { final var errorMsg = "You have selected the 'Bundled' option in KNIME Python preferences, " diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/Python3ScriptingPreferences.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/Python3ScriptingPreferences.java index 046407ca6..73a24a4e2 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/Python3ScriptingPreferences.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/Python3ScriptingPreferences.java @@ -127,7 +127,7 @@ public static PythonCommand getPythonCommandPreference() { * @return The {@link PythonCommand} for the installed bundled environment. */ public static BundledPythonCommand getBundledPythonCommand() { - return getBundledCondaEnvironmentConfig().getPythonCommand(); + return (BundledPythonCommand)getBundledCondaEnvironmentConfig().getPythonCommand(); } private static BundledCondaEnvironmentConfig getBundledCondaEnvironmentConfig() { diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/script/PythonScriptNodeFactory.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/script/PythonScriptNodeFactory.java index ebf8d9a10..496e9cc17 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/script/PythonScriptNodeFactory.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/script/PythonScriptNodeFactory.java @@ -63,7 +63,8 @@ import org.knime.core.node.context.NodeCreationConfiguration; import org.knime.core.node.port.PortType; import org.knime.core.node.port.image.ImagePortObject; -import org.knime.pixi.port.PixiEnvironmentPortObject; + +import org.knime.pixi.port.PythonEnvironmentPortObject; import org.knime.python2.port.PickledObjectFileStorePortObject; import org.knime.python2.ports.InputPort; import org.knime.python2.ports.OutputPort; @@ -82,17 +83,13 @@ protected Optional createPortsConfigBuilder() { b.addExtendableInputPortGroup("Input object (pickled)", PickledObjectFileStorePortObject.TYPE); b.addExtendableInputPortGroupWithDefault("Input table", new PortType[0], new PortType[]{BufferedDataTable.TYPE}, BufferedDataTable.TYPE); - boolean pixiPortAdded = false; try { - final Class pixiClass = PixiEnvironmentPortObject.class; - final PortType pixiPortType = PixiEnvironmentPortObject.TYPE_OPTIONAL; - b.addOptionalInputPortGroup("Pixi environment", pixiPortType); - pixiPortAdded = true; - LOGGER.info("Successfully added optional Pixi environment port"); + b.addOptionalInputPortGroup("Python environment", PythonEnvironmentPortObject.TYPE_OPTIONAL); + LOGGER.info("Successfully added optional Python environment port"); } catch (NoClassDefFoundError e) { - LOGGER.warn("Could not add Pixi environment port - pixi bundle not available: " + e.getMessage()); + LOGGER.warn("Could not add Python environment port - bundle not available: " + e.getMessage()); } catch (Exception e) { - LOGGER.error("Unexpected error adding Pixi environment port", e); + LOGGER.error("Unexpected error adding Python environment port", e); } b.addExtendableOutputPortGroupWithDefault("Output table", new PortType[0], new PortType[]{BufferedDataTable.TYPE}, BufferedDataTable.TYPE); diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/view/PythonViewNodeFactory.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/view/PythonViewNodeFactory.java index bb33637c2..bb932986c 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/view/PythonViewNodeFactory.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/view/PythonViewNodeFactory.java @@ -64,7 +64,8 @@ import org.knime.core.node.port.PortType; import org.knime.core.node.port.image.ImagePortObject; import org.knime.core.webui.node.view.NodeViewFactory; -import org.knime.pixi.port.PixiEnvironmentPortObject; + +import org.knime.pixi.port.PythonEnvironmentPortObject; import org.knime.python2.port.PickledObjectFileStorePortObject; import org.knime.python2.ports.ImageOutputPort; import org.knime.python2.ports.OutputPort; @@ -87,14 +88,12 @@ protected Optional createPortsConfigBuilder() { b.addExtendableInputPortGroupWithDefault("Input table", new PortType[0], new PortType[]{BufferedDataTable.TYPE}, BufferedDataTable.TYPE); try { - final Class pixiClass = PixiEnvironmentPortObject.class; - final PortType pixiPortType = PixiEnvironmentPortObject.TYPE_OPTIONAL; - b.addOptionalInputPortGroup("Pixi environment", pixiPortType); - LOGGER.info("Successfully added optional Pixi environment port"); + b.addOptionalInputPortGroup("Python environment", PythonEnvironmentPortObject.TYPE_OPTIONAL); + LOGGER.info("Successfully added optional Python environment port"); } catch (NoClassDefFoundError e) { - LOGGER.warn("Could not add Pixi environment port - pixi bundle not available: " + e.getMessage()); + LOGGER.warn("Could not add Python environment port - bundle not available: " + e.getMessage()); } catch (Exception e) { - LOGGER.error("Unexpected error adding Pixi environment port", e); + LOGGER.error("Unexpected error adding Python environment port", e); } b.addOptionalOutputPortGroup("Output image", ImagePortObject.TYPE); return Optional.of(b); diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonIOUtils.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonIOUtils.java index 0779375ea..6b784dd9c 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonIOUtils.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonIOUtils.java @@ -79,7 +79,7 @@ import org.knime.core.node.port.image.ImagePortObjectSpec; import org.knime.core.node.port.inactive.InactiveBranchPortObject; import org.knime.core.util.PathUtils; -import org.knime.pixi.port.PixiEnvironmentPortObject; +import org.knime.pixi.port.PythonEnvironmentPortObject; import org.knime.python2.port.PickledObjectFile; import org.knime.python2.port.PickledObjectFileStorePortObject; import org.knime.python3.PythonDataSource; @@ -101,12 +101,12 @@ private PythonIOUtils() { /** * Create an array of Python data sources for the given input ports. The input ports can be either a - * {@link BufferedDataTable}, a {@link PickledObjectFileStorePortObject}, or a {@link PixiEnvironmentPortObject}. - * Note that {@link PixiEnvironmentPortObject}s are filtered out as they are only used for environment + * {@link BufferedDataTable}, a {@link PickledObjectFileStorePortObject}, or {@link PythonEnvironmentPortObject}. + * Note that {@link PythonEnvironmentPortObject}s are filtered out as they are only used for environment * configuration and not passed to Python as data. * * @param data a list of port objects. Only {@link BufferedDataTable}, {@link PickledObjectFileStorePortObject}, - * and {@link PixiEnvironmentPortObject} are supported. + * and {@link PythonEnvironmentPortObject} are supported. * @param tableConverter a table converter that is used to convert the {@link BufferedDataTable}s to Python sources * @param exec for progress reporting and cancellation * @return an array of Python data sources @@ -123,12 +123,16 @@ static PythonDataSource[] createSources(final PortObject[] data, final PythonArr final var tablePortObjects = Arrays.stream(data) // .filter(BufferedDataTable.class::isInstance) // .toArray(BufferedDataTable[]::new); - final var pixiPortObjects = Arrays.stream(data) // - .filter(PixiEnvironmentPortObject.class::isInstance) // - .toArray(PixiEnvironmentPortObject[]::new); + final var pythonEnvPortObjects = Arrays.stream(data) // + .filter(PythonEnvironmentPortObject.class::isInstance) // + .toArray(PythonEnvironmentPortObject[]::new); - // Make sure that all ports are tables, pickled port objects, or pixi environment ports - if (pickledPortObjects.length + tablePortObjects.length + pixiPortObjects.length < data.length) { + NodeLogger.getLogger(PythonIOUtils.class).debugWithFormat( + "Creating sources from %d input ports: %d tables, %d pickled objects, %d PythonEnvironment ports (filtered out)", + data.length, tablePortObjects.length, pickledPortObjects.length, pythonEnvPortObjects.length); + + // Make sure that all ports are tables, pickled port objects, or environment ports + if (pickledPortObjects.length + tablePortObjects.length + pythonEnvPortObjects.length < data.length) { throw new IllegalArgumentException("Unsupported port type connected. This is an implementation error."); } diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptNodeModel.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptNodeModel.java index a3f84126b..266a2afb1 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptNodeModel.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptNodeModel.java @@ -88,7 +88,8 @@ import org.knime.core.node.workflow.VariableTypeRegistry; import org.knime.core.util.asynclose.AsynchronousCloseableTracker; import org.knime.core.webui.node.dialog.scripting.ScriptingService.ConsoleText; -import org.knime.pixi.port.PixiEnvironmentPortObject; + +import org.knime.pixi.port.PythonEnvironmentPortObject; import org.knime.python3.AbstractCondaPythonCommand; import org.knime.python3.PixiPythonCommand; import org.knime.python3.PythonCommand; @@ -188,19 +189,18 @@ protected PortObject[] execute(final PortObject[] inObjects, final ExecutionCont LOGGER.debug("Checking for Pixi environment port"); // The Pixi port is after all regular input ports final int pixiPortIndex = inObjects.length - 1; - PythonCommand pixiCommand = null; try { - pixiCommand = extractPythonCommandFromPixiPort(inObjects[pixiPortIndex]); + final PythonCommand pixiCommand = extractPythonCommandFromPixiPort(inObjects[pixiPortIndex]); + if (pixiCommand != null) { + LOGGER.debug("Using Python from Pixi environment"); + pythonCommand = pixiCommand; + // TODO: Consider if flow variable should take precedence over Pixi port + } else { + LOGGER.debug("Pixi port not connected, using configured Python command"); + pythonCommand = ExecutableSelectionUtils.getPythonCommand(m_settings.getExecutableSelection()); + } } catch (InvalidSettingsException ex) { - // TODO Auto-generated catch block - } - if (pixiCommand != null) { - LOGGER.debug("Using Python from Pixi environment"); - pythonCommand = pixiCommand; - // TODO: Consider if flow variable should take precedence over Pixi port - } else { - LOGGER.debug("Pixi port not connected, using configured Python command"); - pythonCommand = ExecutableSelectionUtils.getPythonCommand(m_settings.getExecutableSelection()); + throw new KNIMEException("Failed to extract Python command from environment port: " + ex.getMessage(), ex); } } else { pythonCommand = ExecutableSelectionUtils.getPythonCommand(m_settings.getExecutableSelection()); @@ -314,11 +314,11 @@ private void pushNewFlowVariable(final FlowVariable variable) { } /** - * Extract the Python command from a PixiEnvironmentPortObject. + * Extract the Python command from a PythonEnvironmentPortObject. * * @param portObject the port object (may be null if optional port is not connected) * @return the Python command, or null if the port is not connected or doesn't contain a valid Python executable - * @throws InvalidSettingsException if the Python executable path from the Pixi environment doesn't exist + * @throws InvalidSettingsException if the Python executable path from the environment doesn't exist */ private static PythonCommand extractPythonCommandFromPixiPort(final PortObject portObject) throws InvalidSettingsException { @@ -327,29 +327,26 @@ private static PythonCommand extractPythonCommandFromPixiPort(final PortObject p } try { - // Check if this is a Pixi environment port object - if (!(portObject instanceof PixiEnvironmentPortObject)) { - return null; + // Check if this is a PythonEnvironmentPortObject (new unified type) + if (portObject instanceof PythonEnvironmentPortObject) { + final PythonEnvironmentPortObject pythonEnvPort = (PythonEnvironmentPortObject)portObject; + try { + // PythonEnvironmentPortObject.getPythonCommand() returns org.knime.pixi.port.PythonCommand, + // but we need org.knime.python3.PythonCommand. Extract the pixi.toml path and create a new instance. + final Path pixiToml = pythonEnvPort.getPixiEnvironmentPath().resolve("pixi.toml"); + final PythonCommand pythonCommand = new PixiPythonCommand(pixiToml); + LOGGER.debug("Using Python from PythonEnvironmentPortObject: " + pythonCommand); + return pythonCommand; + } catch (IOException e) { + throw new InvalidSettingsException("Failed to get Python command from environment: " + e.getMessage(), e); + } } - final PixiEnvironmentPortObject pixiPort = (PixiEnvironmentPortObject)portObject; - - // Create PixiPythonCommand from the pixi.toml path - final Path pixiTomlPath = pixiPort.getPixiTomlPath(); - final PythonCommand pythonCommand = new PixiPythonCommand(pixiTomlPath); - // Verify that the Python executable exists - final Path pythonExecPath = pythonCommand.getPythonExecutablePath(); - if (!Files.exists(pythonExecPath)) { - throw new InvalidSettingsException("The Python executable from the Pixi environment does not exist: " - + pythonExecPath + ". Please check that the Pixi environment is valid."); - } - - LOGGER.debug("Using Python from Pixi environment via pixi run: " + pythonCommand); - return pythonCommand; + return null; } catch (NoClassDefFoundError e) { - // Pixi bundle not available - this should not happen if the port was added successfully - LOGGER.debug("PixiEnvironmentPortObject class not available", e); + // Environment port bundle not available - this should not happen if the port was added successfully + LOGGER.debug("Environment port class not available", e); return null; } } diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptPortsConfiguration.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptPortsConfiguration.java index 6b223c68e..b6db83322 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptPortsConfiguration.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptPortsConfiguration.java @@ -55,7 +55,8 @@ import org.knime.core.node.context.ports.PortsConfiguration; import org.knime.core.node.port.image.ImagePortObject; import org.knime.core.node.workflow.NodeContext; -import org.knime.pixi.port.PixiEnvironmentPortObject; + +import org.knime.pixi.port.PythonEnvironmentPortObject; import org.knime.python2.port.PickledObjectFileStorePortObject; /** @@ -80,8 +81,8 @@ public final class PythonScriptPortsConfiguration { /** Name of the object output port */ public static final String PORTGR_ID_OUT_OBJECT = "Output object (pickled)"; - /** Name of the Pixi environment port */ - public static final String PORTGR_ID_PIXI_ENV = "Pixi environment"; + /** Name of the Python environment port (accepts PythonEnvironmentPortObject) */ + public static final String PORTGR_ID_PYTHON_ENV = "Python environment"; private final int m_numInTables; @@ -95,7 +96,7 @@ public final class PythonScriptPortsConfiguration { private final boolean m_hasView; - private final boolean m_hasPixiPort; + private final boolean m_hasEnvironmentPort; /** * Create a new {@link PythonScriptPortsConfiguration} from the given {@link PortsConfiguration}. @@ -109,14 +110,15 @@ static PythonScriptPortsConfiguration fromPortsConfiguration(final PortsConfigur final Map inPortsLocation = portsConfig.getInputPortLocation(); final var numInTables = ArrayUtils.getLength(inPortsLocation.get(PORTGR_ID_INP_TABLE)); final var numInObjects = ArrayUtils.getLength(inPortsLocation.get(PORTGR_ID_INP_OBJECT)); - final var hasPixiPort = inPortsLocation.containsKey(PORTGR_ID_PIXI_ENV) - && ArrayUtils.getLength(inPortsLocation.get(PORTGR_ID_PIXI_ENV)) > 0; + // Check for environment port (accepts PythonEnvironmentPortObject) + final var hasEnvironmentPort = inPortsLocation.containsKey(PORTGR_ID_PYTHON_ENV) + && ArrayUtils.getLength(inPortsLocation.get(PORTGR_ID_PYTHON_ENV)) > 0; final Map outPortsLocation = portsConfig.getOutputPortLocation(); final var numOutTables = ArrayUtils.getLength(outPortsLocation.get(PORTGR_ID_OUT_TABLE)); final var numOutImages = ArrayUtils.getLength(outPortsLocation.get(PORTGR_ID_OUT_IMAGE)); final var numOutObjects = ArrayUtils.getLength(outPortsLocation.get(PORTGR_ID_OUT_OBJECT)); - return new PythonScriptPortsConfiguration(numInTables, numInObjects, numOutTables, numOutImages, numOutObjects, hasView, hasPixiPort); + return new PythonScriptPortsConfiguration(numInTables, numInObjects, numOutTables, numOutImages, numOutObjects, hasView, hasEnvironmentPort); } /** @@ -142,7 +144,7 @@ static PythonScriptPortsConfiguration fromCurrentNodeContext() { // Count the number of the different ports (skip the flow var port) var numInTables = 0; var numInObjects = 0; - var hasPixiPort = false; + var hasEnvironmentPort = false; for (int i = 1; i < nodeContainer.getNrInPorts(); i++) { var portType = nodeContainer.getInPort(i).getPortType(); if (BufferedDataTable.TYPE.equals(portType)) { @@ -150,15 +152,15 @@ static PythonScriptPortsConfiguration fromCurrentNodeContext() { } else if (PickledObjectFileStorePortObject.TYPE.equals(portType)) { numInObjects++; } else { - // Check if it's a Pixi environment port + // Check if it's a Python environment port try { - if (PixiEnvironmentPortObject.TYPE.equals(portType) - || PixiEnvironmentPortObject.TYPE_OPTIONAL.equals(portType)) { - hasPixiPort = true; + if (PythonEnvironmentPortObject.TYPE.equals(portType) + || PythonEnvironmentPortObject.TYPE_OPTIONAL.equals(portType)) { + hasEnvironmentPort = true; continue; // Don't count as error } } catch (NoClassDefFoundError e) { - // Pixi bundle not available, ignore + // Python environment bundle not available, ignore } throw new IllegalStateException("Unsupported input port configured. This is an implementation error."); } @@ -181,18 +183,18 @@ static PythonScriptPortsConfiguration fromCurrentNodeContext() { } var hasView = nodeContainer.getNrViews() > 0; - return new PythonScriptPortsConfiguration(numInTables, numInObjects, numOutTables, numOutImages, numOutObjects, hasView, hasPixiPort); + return new PythonScriptPortsConfiguration(numInTables, numInObjects, numOutTables, numOutImages, numOutObjects, hasView, hasEnvironmentPort); } private PythonScriptPortsConfiguration(final int numInTables, final int numInObjects, final int numOutTables, - final int numOutImages, final int numOutObjects, final boolean hasView, final boolean hasPixiPort) { + final int numOutImages, final int numOutObjects, final boolean hasView, final boolean hasEnvironmentPort) { m_numInTables = numInTables; m_numInObjects = numInObjects; m_numOutTables = numOutTables; m_numOutImages = numOutImages; m_numOutObjects = numOutObjects; m_hasView = hasView; - m_hasPixiPort = hasPixiPort; + m_hasEnvironmentPort = hasEnvironmentPort; } /** @@ -237,9 +239,27 @@ public boolean hasView() { return m_hasView; } /** - * @return if the node has a Pixi environment port + * @return if the node has a Python environment port (accepts PythonEnvironmentPortObject) + */ + public boolean hasEnvironmentPort() { + return m_hasEnvironmentPort; + } + + /** + * @deprecated Use {@link #hasEnvironmentPort()} instead + * @return if the node has a Python environment port */ + @Deprecated(since = "5.10", forRemoval = true) public boolean hasPixiPort() { - return m_hasPixiPort; + return m_hasEnvironmentPort; + } + + /** + * @deprecated Use {@link #hasEnvironmentPort()} instead + * @return if the node has a Python environment port + */ + @Deprecated(since = "5.10", forRemoval = true) + public boolean hasPythonEnvPort() { + return m_hasEnvironmentPort; } } diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingInputOutputModelUtils.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingInputOutputModelUtils.java index b28e62330..c502e38c3 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingInputOutputModelUtils.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingInputOutputModelUtils.java @@ -58,13 +58,14 @@ import org.knime.core.data.DataTableSpec; import org.knime.core.data.DataType; import org.knime.core.node.BufferedDataTable; +import org.knime.core.node.NodeLogger; import org.knime.core.node.port.PortType; import org.knime.core.node.port.flowvariable.FlowVariablePortObject; import org.knime.core.node.port.image.ImagePortObject; import org.knime.core.node.workflow.FlowVariable; import org.knime.core.webui.node.dialog.scripting.InputOutputModel; import org.knime.core.webui.node.dialog.scripting.WorkflowControl.InputPortInfo; -import org.knime.pixi.port.PixiEnvironmentPortObject; +import org.knime.pixi.port.PythonEnvironmentPortObject; import org.knime.python2.port.PickledObjectFileStorePortObject; /** @@ -74,6 +75,8 @@ */ final class PythonScriptingInputOutputModelUtils { + private static final NodeLogger LOGGER = NodeLogger.getLogger(PythonScriptingInputOutputModelUtils.class); + /** * The type string used for tables */ @@ -131,6 +134,7 @@ static List getInputObjects(final InputPortInfo[] inputPorts) // Skip Pixi environment ports - they are not data ports if (isPixiEnvironmentPort(type)) { + LOGGER.debugWithFormat("Skipping environment port at index %d (type: %s) - not exposed to Python script", i, type.getName()); continue; } @@ -213,9 +217,13 @@ private static boolean isNoFlowVariablePort(final PortType portType) { private static boolean isPixiEnvironmentPort(final PortType portType) { try { - return portType.acceptsPortObjectClass(PixiEnvironmentPortObject.class); + final boolean isPythonEnvPort = portType.acceptsPortObjectClass(PythonEnvironmentPortObject.class); + LOGGER.debugWithFormat("Checking if port type '%s' is environment port: %s", + portType.getName(), isPythonEnvPort); + return isPythonEnvPort; } catch (NoClassDefFoundError e) { // Pixi nodes bundle not available + LOGGER.debugWithFormat("Pixi nodes bundle not available for port type '%s'", portType.getName()); return false; } } diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingService.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingService.java index b3b5dfae0..67c876c78 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingService.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingService.java @@ -77,7 +77,8 @@ import org.knime.core.webui.node.dialog.scripting.CodeGenerationRequest; import org.knime.core.webui.node.dialog.scripting.InputOutputModel; import org.knime.core.webui.node.dialog.scripting.ScriptingService; -import org.knime.pixi.port.PixiEnvironmentPortObject; + +import org.knime.pixi.port.PythonEnvironmentPortObject; import org.knime.python3.PixiPythonCommand; import org.knime.python3.PythonCommand; import org.knime.python3.scripting.nodes2.PythonScriptingService.ExecutableOption.ExecutableOptionType; @@ -531,11 +532,11 @@ public void close() { } /** - * Extract the Python command from a PixiEnvironmentPortObject. + * Extract the Python command from a PythonEnvironmentPortObject. * * @param portObject the port object (may be null if optional port is not connected) * @return the Python command, or null if the port is not connected or doesn't contain a valid Python executable - * @throws InvalidSettingsException if the Python executable path from the Pixi environment doesn't exist + * @throws InvalidSettingsException if the Python executable path from the environment doesn't exist */ private static PythonCommand extractPythonCommandFromPixiPort(final PortObject portObject) throws InvalidSettingsException { @@ -544,29 +545,26 @@ private static PythonCommand extractPythonCommandFromPixiPort(final PortObject p } try { - // Check if this is a Pixi environment port object - if (!(portObject instanceof PixiEnvironmentPortObject)) { - return null; + // Check if this is a PythonEnvironmentPortObject (new unified type) + if (portObject instanceof PythonEnvironmentPortObject) { + final PythonEnvironmentPortObject pythonEnvPort = (PythonEnvironmentPortObject)portObject; + try { + // PythonEnvironmentPortObject.getPythonCommand() returns org.knime.pixi.port.PythonCommand, + // but we need org.knime.python3.PythonCommand. Extract the pixi.toml path and create a new instance. + final Path pixiToml = pythonEnvPort.getPixiEnvironmentPath().resolve("pixi.toml"); + final PythonCommand pythonCommand = new PixiPythonCommand(pixiToml); + LOGGER.debug("Using Python from PythonEnvironmentPortObject: " + pythonCommand); + return pythonCommand; + } catch (IOException e) { + throw new InvalidSettingsException("Failed to get Python command from environment: " + e.getMessage(), e); + } } - final PixiEnvironmentPortObject pixiPort = (PixiEnvironmentPortObject)portObject; - - // Create PixiPythonCommand from the pixi.toml path - final Path pixiTomlPath = pixiPort.getPixiTomlPath(); - final PythonCommand pythonCommand = new PixiPythonCommand(pixiTomlPath); - // Verify that the Python executable exists - final Path pythonExecPath = pythonCommand.getPythonExecutablePath(); - if (!Files.exists(pythonExecPath)) { - throw new InvalidSettingsException("The Python executable from the Pixi environment does not exist: " - + pythonExecPath + ". Please check that the Pixi environment is valid."); - } - - LOGGER.debug("Using Python from Pixi environment via pixi run: " + pythonCommand); - return pythonCommand; + return null; } catch (NoClassDefFoundError e) { - // Pixi bundle not available - this should not happen if the port was added successfully - LOGGER.debug("PixiEnvironmentPortObject class not available", e); + // Environment port bundle not available - this should not happen if the port was added successfully + LOGGER.debug("Environment port class not available", e); return null; } } diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingSession.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingSession.java index e520c4351..fd1c79a3d 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingSession.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingSession.java @@ -425,8 +425,10 @@ private static PythonGateway createGateway(final Pyth throw new IOException(CondaEnvironmentIdentifier.NOT_EXECUTED_PATH_PLACEHOLDER); } + // Wrap internal PythonCommand in adapter for public API + final var pythonCommandAdapter = new PythonCommandAdapter(pythonCommand); final var gatewayDescriptionBuilder = - PythonGatewayDescription.builder(pythonCommand, LAUNCHER.toAbsolutePath(), PythonScriptingEntryPoint.class); + PythonGatewayDescription.builder(pythonCommandAdapter, LAUNCHER.toAbsolutePath(), PythonScriptingEntryPoint.class); gatewayDescriptionBuilder.withPreloaded(PythonArrowExtension.INSTANCE); gatewayDescriptionBuilder.withPreloaded(PythonViewsExtension.INSTANCE); @@ -523,4 +525,47 @@ public interface FileStoreHandlerSupplier extends AutoCloseable { @Override void close(); } + + /** + * Adapter that wraps org.knime.pixi.port.PythonCommand to implement org.knime.python3.PythonCommand. + * This is needed because PythonGatewayDescription.builder() requires the public API type. + */ + private static final class PythonCommandAdapter implements org.knime.python3.PythonCommand { + private final org.knime.python3.PythonCommand m_delegate; + + PythonCommandAdapter(final org.knime.python3.PythonCommand delegate) { + m_delegate = delegate; + } + + @Override + public ProcessBuilder createProcessBuilder() { + return m_delegate.createProcessBuilder(); + } + + @Override + public Path getPythonExecutablePath() { + return m_delegate.getPythonExecutablePath(); + } + + @Override + public int hashCode() { + return m_delegate.hashCode(); + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof PythonCommandAdapter other) { + return m_delegate.equals(other.m_delegate); + } + return false; + } + + @Override + public String toString() { + return m_delegate.toString(); + } + } } diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/script/PythonScriptNodeFactory.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/script/PythonScriptNodeFactory.java index b13c0b79e..02f3cab23 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/script/PythonScriptNodeFactory.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/script/PythonScriptNodeFactory.java @@ -67,7 +67,7 @@ import org.knime.core.webui.node.dialog.NodeDialog; import org.knime.core.webui.node.dialog.NodeDialogFactory; import org.knime.core.webui.node.dialog.NodeDialogManager; -import org.knime.pixi.port.PixiEnvironmentPortObject; +import org.knime.pixi.port.PythonEnvironmentPortObject; import org.knime.python2.port.PickledObjectFileStorePortObject; import org.knime.python3.scripting.nodes2.PythonScriptNodeDialog; import org.knime.python3.scripting.nodes2.PythonScriptNodeModel; @@ -83,7 +83,7 @@ public final class PythonScriptNodeFactory extends ConfigurableNodeFactory createPortsConfigBuilder() { b.addExtendableInputPortGroup(PORTGR_ID_INP_OBJECT, PickledObjectFileStorePortObject.TYPE); b.addExtendableInputPortGroupWithDefault(PORTGR_ID_INP_TABLE, new PortType[0], new PortType[]{BufferedDataTable.TYPE}, BufferedDataTable.TYPE); - boolean pixiPortAdded = false; + + // Add Python environment port try { - final Class pixiClass = PixiEnvironmentPortObject.class; - final PortType pixiPortType = PixiEnvironmentPortObject.TYPE_OPTIONAL; - b.addOptionalInputPortGroup(PORTGR_ID_PIXI_ENV, pixiPortType); - pixiPortAdded = true; - LOGGER.info("Successfully added optional Pixi environment port"); + final Class pythonEnvClass = PythonEnvironmentPortObject.class; + b.addOptionalInputPortGroup(PORTGR_ID_PYTHON_ENV, PythonEnvironmentPortObject.TYPE_OPTIONAL); + LOGGER.info("Successfully added Python environment port"); } catch (NoClassDefFoundError e) { - LOGGER.warn("Could not add Pixi environment port - pixi bundle not available: " + e.getMessage()); - } catch (Exception e) { - LOGGER.error("Unexpected error adding Pixi environment port", e); + LOGGER.debug("PythonEnvironmentPortObject not available: " + e.getMessage()); } + b.addExtendableOutputPortGroupWithDefault(PORTGR_ID_OUT_TABLE, new PortType[0], new PortType[]{BufferedDataTable.TYPE}, BufferedDataTable.TYPE); b.addExtendableOutputPortGroup(PORTGR_ID_OUT_IMAGE, ImagePortObject.TYPE); diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/view/PythonViewNodeFactory.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/view/PythonViewNodeFactory.java index c5cf3efa1..29c844762 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/view/PythonViewNodeFactory.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/view/PythonViewNodeFactory.java @@ -66,7 +66,7 @@ import org.knime.core.webui.node.dialog.NodeDialogManager; import org.knime.core.webui.node.view.NodeView; import org.knime.core.webui.node.view.NodeViewFactory; -import org.knime.pixi.port.PixiEnvironmentPortObject; +import org.knime.pixi.port.PythonEnvironmentPortObject; import org.knime.python2.port.PickledObjectFileStorePortObject; import org.knime.python3.scripting.nodes2.PythonScriptNodeDialog; import org.knime.python3.scripting.nodes2.PythonScriptNodeModel; @@ -83,7 +83,7 @@ public class PythonViewNodeFactory extends ConfigurableNodeFactory createPortsConfigBuilder() { b.addExtendableInputPortGroup(PORTGR_ID_INP_OBJECT, PickledObjectFileStorePortObject.TYPE); b.addExtendableInputPortGroupWithDefault(PORTGR_ID_INP_TABLE, new PortType[0], new PortType[]{BufferedDataTable.TYPE}, BufferedDataTable.TYPE); + + // Add Python environment port try { - final Class pixiClass = PixiEnvironmentPortObject.class; - final PortType pixiPortType = PixiEnvironmentPortObject.TYPE_OPTIONAL; - b.addOptionalInputPortGroup(PORTGR_ID_PIXI_ENV, pixiPortType); - LOGGER.info("Successfully added optional Pixi environment port"); + final Class pythonEnvClass = PythonEnvironmentPortObject.class; + b.addOptionalInputPortGroup(PORTGR_ID_PYTHON_ENV, PythonEnvironmentPortObject.TYPE_OPTIONAL); + LOGGER.info("Successfully added Python environment port"); } catch (NoClassDefFoundError e) { - LOGGER.warn("Could not add Pixi environment port - pixi bundle not available: " + e.getMessage()); - } catch (Exception e) { - LOGGER.error("Unexpected error adding Pixi environment port", e); + LOGGER.debug("PythonEnvironmentPortObject not available: " + e.getMessage()); } + b.addOptionalOutputPortGroup(PORTGR_ID_OUT_IMAGE, ImagePortObject.TYPE); return Optional.of(b); } diff --git a/org.knime.python3/META-INF/MANIFEST.MF b/org.knime.python3/META-INF/MANIFEST.MF index e62ab5e67..eaadbff62 100644 --- a/org.knime.python3/META-INF/MANIFEST.MF +++ b/org.knime.python3/META-INF/MANIFEST.MF @@ -16,7 +16,8 @@ Require-Bundle: com.google.guava;bundle-version="[31.0.1,32.0.0)", org.knime.conda;bundle-version="[5.9.0,6.0.0)", org.knime.python3.types;bundle-version="[5.9.0,6.0.0)", org.eclipse.equinox.p2.core;bundle-version="[2.0.0,3.0.0)";visibility:=reexport, - org.eclipse.equinox.p2.engine;bundle-version="[2.0.0,3.0.0)";visibility:=reexport + org.eclipse.equinox.p2.engine;bundle-version="[2.0.0,3.0.0)";visibility:=reexport, + org.knime.pixi.port;bundle-version="[5.10.0,6.0.0)" Export-Package: org.knime.python3, org.knime.python3.utils Automatic-Module-Name: org.knime.python3 From 42072d1e1aeed461ec8257af2d0d302b7aebb9cb Mon Sep 17 00:00:00 2001 From: Marc Lehner Date: Wed, 21 Jan 2026 14:52:03 +0100 Subject: [PATCH 07/34] AP-25245: add progress for the python port installation and make it cancelable AP-25245 () --- .../nodes/PortsConfigurationUtils.java | 60 +++++++++++++++++++ .../nodes2/PythonScriptNodeModel.java | 23 +++++++ 2 files changed, 83 insertions(+) diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/PortsConfigurationUtils.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/PortsConfigurationUtils.java index ccfe6766a..e3a6c2ff2 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/PortsConfigurationUtils.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/PortsConfigurationUtils.java @@ -48,10 +48,16 @@ */ package org.knime.python3.scripting.nodes; +import java.io.IOException; + import org.knime.core.node.BufferedDataTable; +import org.knime.core.node.CanceledExecutionException; +import org.knime.core.node.ExecutionMonitor; import org.knime.core.node.context.ports.PortsConfiguration; +import org.knime.core.node.port.PortObject; import org.knime.core.node.port.PortType; import org.knime.core.node.port.image.ImagePortObject; +import org.knime.pixi.port.PixiInstallationProgressReporter; import org.knime.pixi.port.PythonEnvironmentPortObject; import org.knime.python2.port.PickledObjectFileStorePortObject; import org.knime.python2.ports.DataTableInputPort; @@ -179,4 +185,58 @@ public static OutputPort[] createOutputPorts(final PortsConfiguration config) { public static OutputPort createPickledObjectOutputPort(final int outObjectSuffix) { return new PickledObjectOutputPort("knio.output_objects[" + outObjectSuffix + "]"); } + + /** + * Extract the Python environment port object from the input port objects, if present. + * + * @param config the ports configuration + * @param inObjects the input port objects + * @return the Python environment port object, or null if not present + */ + public static PythonEnvironmentPortObject extractPythonEnvironmentPort( + final PortsConfiguration config, final PortObject[] inObjects) { + final PortType[] inTypes = config.getInputPorts(); + for (int i = 0; i < inTypes.length; i++) { + if (isPixiPort(inTypes[i]) && inObjects[i] instanceof PythonEnvironmentPortObject) { + return (PythonEnvironmentPortObject) inObjects[i]; + } + } + return null; + } + + /** + * Install the Python environment port if present, with progress reporting. + * This should be called early in node execution to avoid installation timeout issues. + * Installation is thread-safe and will only happen once even if called multiple times. + * + * @param config the ports configuration + * @param inObjects the input port objects + * @param exec the execution monitor for progress reporting and cancellation + * @throws IOException if installation fails + * @throws CanceledExecutionException if the operation is canceled + */ + public static void installPythonEnvironmentIfPresent( + final PortsConfiguration config, final PortObject[] inObjects, final ExecutionMonitor exec) + throws IOException, CanceledExecutionException { + final PythonEnvironmentPortObject envPort = extractPythonEnvironmentPort(config, inObjects); + if (envPort != null) { + exec.setMessage("Installing Python environment..."); + // Create simulated progress reporter that maps internal progress to node progress + final PixiInstallationProgressReporter progressReporter = new PixiInstallationProgressReporter() { + @Override + public void setProgress(final double fraction, final String message) { + exec.setProgress(fraction, message); + } + + @Override + public void checkCanceled() throws CanceledExecutionException { + exec.checkCanceled(); + } + }; + + // Use simulated progress since we don't yet capture pixi output + envPort.installPixiEnvironment(exec, + PixiInstallationProgressReporter.createSimulated(progressReporter)); + } + } } diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptNodeModel.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptNodeModel.java index 266a2afb1..b631670a0 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptNodeModel.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptNodeModel.java @@ -94,6 +94,7 @@ import org.knime.python3.PixiPythonCommand; import org.knime.python3.PythonCommand; import org.knime.python3.PythonProcessTerminatedException; +import org.knime.python3.scripting.nodes.PortsConfigurationUtils; import org.knime.python3.scripting.nodes2.ConsoleOutputUtils.ConsoleOutputStorage; import org.knime.python3.scripting.nodes2.PythonScriptingSession.ExecutionInfo; import org.knime.python3.scripting.nodes2.PythonScriptingSession.ExecutionStatus; @@ -123,6 +124,8 @@ public final class PythonScriptNodeModel extends NodeModel { private final PythonScriptNodeSettings m_settings; private final PythonScriptPortsConfiguration m_ports; + + private final PortsConfiguration m_portsConfiguration; private final AsynchronousCloseableTracker m_sessionShutdownTracker = new AsynchronousCloseableTracker<>(t -> LOGGER.debug("Kernel shutdown failed.", t)); @@ -153,10 +156,18 @@ public final class PythonScriptNodeModel extends NodeModel { public PythonScriptNodeModel(final PortsConfiguration portsConfiguration, final boolean hasView) { super(portsConfiguration.getInputPorts(), portsConfiguration.getOutputPorts()); m_hasView = hasView; + m_portsConfiguration = portsConfiguration; m_ports = PythonScriptPortsConfiguration.fromPortsConfiguration(portsConfiguration, hasView); m_settings = new PythonScriptNodeSettings(m_ports); m_view = Optional.empty(); } + + /** + * @return the ports configuration + */ + private PortsConfiguration getPortsConfiguration() { + return m_portsConfiguration; + } /** * @return the path to the HTML view file @@ -183,6 +194,18 @@ protected PortObjectSpec[] configure(final PortObjectSpec[] inSpecs) throws Inva @Override protected PortObject[] execute(final PortObject[] inObjects, final ExecutionContext exec) throws IOException, InterruptedException, CanceledExecutionException, KNIMEException { + + // Install Python environment early to avoid timeout issues during gateway connection + // This must happen before creating the PythonScriptingSession + if (m_ports.hasPixiPort()) { + try { + PortsConfigurationUtils.installPythonEnvironmentIfPresent( + getPortsConfiguration(), inObjects, exec); + } catch (IOException | CanceledExecutionException ex) { + throw ex; // Re-throw as-is + } + } + // Check if Pixi port is connected and use it, otherwise use configured Python command final PythonCommand pythonCommand; if (m_ports.hasPixiPort()) { From a840697e8390884b4e5cf8821a6eba8600b35e15 Mon Sep 17 00:00:00 2001 From: Marc Lehner Date: Mon, 26 Jan 2026 13:15:12 +0100 Subject: [PATCH 08/34] AP-25245: deprecate PythonCommand and use PythonProcessProvider from knime-conda instead AP-25245 () --- .../META-INF/MANIFEST.MF | 1 + .../org/knime/python3/arrow/TestUtils.java | 4 ++-- .../META-INF/MANIFEST.MF | 1 + .../types/KnimeArrowExtensionTypesTest.java | 4 ++-- .../nodes/PythonExtensionPreferences.java | 8 ++++---- .../nodes/PythonNodeGatewayFactory.java | 8 ++++---- .../META-INF/MANIFEST.MF | 1 + .../AbstractPythonScriptingNodeModel.java | 19 +++++++++---------- .../prefs/BundledCondaEnvironmentConfig.java | 4 ++-- .../nodes/prefs/CondaEnvironmentConfig.java | 4 ++-- .../nodes/prefs/ManualEnvironmentConfig.java | 4 ++-- .../prefs/Python3ScriptingPreferences.java | 6 +++--- .../nodes/prefs/PythonEnvironmentConfig.java | 4 ++-- .../nodes/prefs/PythonKernelTester.java | 12 ++++++------ .../nodes2/ExecutableSelectionUtils.java | 12 ++++++------ .../nodes2/PythonScriptNodeModel.java | 10 +++++----- .../nodes2/PythonScriptingService.java | 8 ++++---- .../nodes2/PythonScriptingSession.java | 12 ++++++------ .../META-INF/MANIFEST.MF | 1 + .../Python3KernelBackendProxyTest.java | 1 + .../META-INF/MANIFEST.MF | 1 + .../python3/testing/Python3TestUtils.java | 4 ++-- org.knime.python3/META-INF/MANIFEST.MF | 3 ++- .../python3/AbstractPixiPythonCommand.java | 12 ++++++------ .../knime/python3/AbstractPythonCommand.java | 3 ++- .../knime/python3/BundledPythonCommand.java | 4 ++-- .../org/knime/python3/CondaPythonCommand.java | 4 ++-- .../org/knime/python3/PixiPythonCommand.java | 6 +++--- .../java/org/knime/python3/PythonCommand.java | 15 +++++++++++++-- .../knime/python3/PythonGatewayFactory.java | 11 ++++++----- .../python3/QueuedPythonGatewayFactory.java | 13 +++++++------ .../knime/python3/SimplePythonCommand.java | 2 +- 32 files changed, 111 insertions(+), 91 deletions(-) diff --git a/org.knime.python3.arrow.tests/META-INF/MANIFEST.MF b/org.knime.python3.arrow.tests/META-INF/MANIFEST.MF index 36dc71524..f71a116d7 100644 --- a/org.knime.python3.arrow.tests/META-INF/MANIFEST.MF +++ b/org.knime.python3.arrow.tests/META-INF/MANIFEST.MF @@ -14,3 +14,4 @@ Require-Bundle: org.junit;bundle-version="[4.13.0,5.0.0)", org.knime.core.data.columnar;bundle-version="[5.6.0,6.0.0)", org.knime.python3.testing;bundle-version="[5.6.0,6.0.0)" Automatic-Module-Name: org.knime.python3.arrow.tests +Import-Package: org.knime.python3.processprovider diff --git a/org.knime.python3.arrow.tests/src/test/java/org/knime/python3/arrow/TestUtils.java b/org.knime.python3.arrow.tests/src/test/java/org/knime/python3/arrow/TestUtils.java index 81c5497d7..946513029 100644 --- a/org.knime.python3.arrow.tests/src/test/java/org/knime/python3/arrow/TestUtils.java +++ b/org.knime.python3.arrow.tests/src/test/java/org/knime/python3/arrow/TestUtils.java @@ -55,7 +55,7 @@ import org.knime.python3.DefaultPythonGateway; import org.knime.python3.Python3SourceDirectory; -import org.knime.python3.PythonCommand; +import org.knime.python3.processprovider.PythonProcessProvider; import org.knime.python3.PythonDataSink; import org.knime.python3.PythonDataSource; import org.knime.python3.PythonEntryPoint; @@ -83,7 +83,7 @@ private TestUtils() { * @throws InterruptedException */ public static PythonGateway openPythonGateway() throws IOException, InterruptedException { - final PythonCommand command = Python3TestUtils.getPythonCommand(); + final PythonProcessProvider command = Python3TestUtils.getPythonCommand(); final String launcherPath = Paths.get(System.getProperty("user.dir"), "src/test/python", "tests_launcher.py").toString(); final PythonPath pythonPath = (new PythonPathBuilder()) // diff --git a/org.knime.python3.arrow.types.tests/META-INF/MANIFEST.MF b/org.knime.python3.arrow.types.tests/META-INF/MANIFEST.MF index c98d5943f..427a9be70 100644 --- a/org.knime.python3.arrow.types.tests/META-INF/MANIFEST.MF +++ b/org.knime.python3.arrow.types.tests/META-INF/MANIFEST.MF @@ -19,3 +19,4 @@ Require-Bundle: org.knime.core.table;bundle-version="[5.6.0,6.0.0)", org.knime.python3.testing;bundle-version="[5.6.0,6.0.0)" Automatic-Module-Name: org.knime.python3.arrow.types.tests Export-Package: org.knime.python3.arrow.types +Import-Package: org.knime.python3.processprovider diff --git a/org.knime.python3.arrow.types.tests/src/test/java/org/knime/python3/arrow/types/KnimeArrowExtensionTypesTest.java b/org.knime.python3.arrow.types.tests/src/test/java/org/knime/python3/arrow/types/KnimeArrowExtensionTypesTest.java index 55706f90f..6d8799345 100644 --- a/org.knime.python3.arrow.types.tests/src/test/java/org/knime/python3/arrow/types/KnimeArrowExtensionTypesTest.java +++ b/org.knime.python3.arrow.types.tests/src/test/java/org/knime/python3/arrow/types/KnimeArrowExtensionTypesTest.java @@ -172,7 +172,7 @@ import org.knime.filehandling.core.data.location.cell.SimpleFSLocationCellFactory; import org.knime.python3.DefaultPythonGateway; import org.knime.python3.Python3SourceDirectory; -import org.knime.python3.PythonCommand; +import org.knime.python3.processprovider.PythonProcessProvider; import org.knime.python3.PythonDataSink; import org.knime.python3.PythonDataSource; import org.knime.python3.PythonEntryPoint; @@ -893,7 +893,7 @@ interface TriConsumer { private static PythonGateway openPythonGateway(final Class entryPointClass, final String launcherModule, final PythonModule... modules) throws IOException, InterruptedException { - final PythonCommand command = Python3TestUtils.getPythonCommand(); + final PythonProcessProvider command = Python3TestUtils.getPythonCommand(); final String launcherPath = Paths.get(System.getProperty("user.dir"), "src/test/python", launcherModule) .toString(); final PythonPathBuilder builder = PythonPath.builder()// diff --git a/org.knime.python3.nodes/src/main/java/org/knime/python3/nodes/PythonExtensionPreferences.java b/org.knime.python3.nodes/src/main/java/org/knime/python3/nodes/PythonExtensionPreferences.java index 6aad57b37..e13dc9ce4 100644 --- a/org.knime.python3.nodes/src/main/java/org/knime/python3/nodes/PythonExtensionPreferences.java +++ b/org.knime.python3.nodes/src/main/java/org/knime/python3/nodes/PythonExtensionPreferences.java @@ -59,11 +59,11 @@ import java.util.Optional; import java.util.stream.Stream; -import org.knime.python3.CondaPythonCommand; import org.knime.conda.prefs.CondaPreferences; import org.knime.core.node.NodeLogger; -import org.knime.python3.PythonCommand; +import org.knime.python3.CondaPythonCommand; import org.knime.python3.SimplePythonCommand; +import org.knime.python3.processprovider.PythonProcessProvider; import org.yaml.snakeyaml.Yaml; /** @@ -91,7 +91,7 @@ static Stream getPathsToCustomExtensions() { .map(Optional::get); } - static Optional getCustomPythonCommand(final String extensionId) { + static Optional getCustomPythonCommand(final String extensionId) { return loadConfigs()// .filter(e -> extensionId.equals(e.m_id))// .findFirst()// @@ -307,7 +307,7 @@ Optional getSrcPath() { } } - Optional getCommand() { + Optional getCommand() { if (m_condaEnvPath != null) { if (m_pythonExecutable != null) { LOGGER.warnWithFormat("Both conda_env_path and python_executable are provided for extension '%s'." diff --git a/org.knime.python3.nodes/src/main/java/org/knime/python3/nodes/PythonNodeGatewayFactory.java b/org.knime.python3.nodes/src/main/java/org/knime/python3/nodes/PythonNodeGatewayFactory.java index c5c0cdf6a..49e1f68f3 100644 --- a/org.knime.python3.nodes/src/main/java/org/knime/python3/nodes/PythonNodeGatewayFactory.java +++ b/org.knime.python3.nodes/src/main/java/org/knime/python3/nodes/PythonNodeGatewayFactory.java @@ -52,11 +52,10 @@ import java.nio.file.Path; import java.util.Objects; -import org.knime.python3.BundledPythonCommand; import org.knime.conda.envbundling.environment.CondaEnvironmentRegistry; import org.knime.python3.Activator; +import org.knime.python3.BundledPythonCommand; import org.knime.python3.FreshPythonGatewayFactory; -import org.knime.python3.PythonCommand; import org.knime.python3.Python3SourceDirectory; import org.knime.python3.PythonEntryPointUtils; import org.knime.python3.PythonGateway; @@ -65,6 +64,7 @@ import org.knime.python3.PythonGatewayFactory.PythonGatewayDescription; import org.knime.python3.arrow.Python3ArrowSourceDirectory; import org.knime.python3.arrow.PythonArrowExtension; +import org.knime.python3.processprovider.PythonProcessProvider; import org.knime.python3.types.PythonValueFactoryModule; import org.knime.python3.types.PythonValueFactoryRegistry; import org.knime.python3.views.Python3ViewsSourceDirectory; @@ -141,12 +141,12 @@ public PythonGateway create() throws IOException, InterruptedE return gateway; } - private static PythonCommand createCommand(final String extensionId, final String environmentName) { + private static PythonProcessProvider createCommand(final String extensionId, final String environmentName) { return PythonExtensionPreferences.getCustomPythonCommand(extensionId)// .orElseGet(() -> getPythonCommandForEnvironment(environmentName)); } - private static PythonCommand getPythonCommandForEnvironment(final String environmentName) { + private static PythonProcessProvider getPythonCommandForEnvironment(final String environmentName) { final var environment = CondaEnvironmentRegistry.getEnvironment(environmentName); if (environment == null) { throw new IllegalStateException("Conda environment '" + environmentName + "' not found. " diff --git a/org.knime.python3.scripting.nodes/META-INF/MANIFEST.MF b/org.knime.python3.scripting.nodes/META-INF/MANIFEST.MF index 1ad1db24f..839c7237c 100644 --- a/org.knime.python3.scripting.nodes/META-INF/MANIFEST.MF +++ b/org.knime.python3.scripting.nodes/META-INF/MANIFEST.MF @@ -44,3 +44,4 @@ Automatic-Module-Name: org.knime.python3.scripting.nodes Export-Package: org.knime.python3.scripting.nodes.prefs Eclipse-RegisterBuddy: org.knime.ext.py4j Eclipse-BundleShape: dir +Import-Package: org.knime.python3.processprovider diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/AbstractPythonScriptingNodeModel.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/AbstractPythonScriptingNodeModel.java index f30ad5ca4..bd1afdc7a 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/AbstractPythonScriptingNodeModel.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/AbstractPythonScriptingNodeModel.java @@ -81,7 +81,6 @@ import org.knime.core.util.PathUtils; import org.knime.core.util.asynclose.AsynchronousCloseableTracker; import org.knime.core.webui.node.view.NodeView; - import org.knime.pixi.port.PythonEnvironmentPortObject; import org.knime.python2.PythonCommand; import org.knime.python2.PythonModuleSpec; @@ -103,8 +102,8 @@ import org.knime.python2.ports.OutputPort; import org.knime.python2.ports.PickledObjectOutputPort; import org.knime.python2.ports.Port; -import org.knime.python3.AbstractCondaPythonCommand; import org.knime.python3.PixiPythonCommand; +import org.knime.python3.processprovider.PythonProcessProvider; import org.knime.python3.scripting.Python3KernelBackend; import org.knime.python3.scripting.nodes.prefs.Python3ScriptingPreferences; @@ -402,7 +401,7 @@ private static PythonCommand extractPythonCommandFromPixiPort(final PortObject p if (!(portObject instanceof PythonEnvironmentPortObject)) { return null; } - + // Handle PythonEnvironmentPortObject final PythonEnvironmentPortObject pythonEnvPort = (PythonEnvironmentPortObject)portObject; final Path pixiTomlPath; @@ -411,10 +410,10 @@ private static PythonCommand extractPythonCommandFromPixiPort(final PortObject p } catch (IOException e) { throw new InvalidSettingsException("Failed to get pixi.toml path from PythonEnvironmentPortObject: " + e.getMessage(), e); } - + // Create PixiPythonCommand from the pixi.toml path - final org.knime.python3.PythonCommand pythonCommand = new PixiPythonCommand(pixiTomlPath); - + final PythonProcessProvider pythonCommand = new PixiPythonCommand(pixiTomlPath); + // Verify that the Python executable exists final Path pythonExecPath = pythonCommand.getPythonExecutablePath(); if (!Files.exists(pythonExecPath)) { @@ -422,7 +421,7 @@ private static PythonCommand extractPythonCommandFromPixiPort(final PortObject p "The Python executable from the Pixi environment does not exist at path: " + pythonExecPath + ". Please check that the Pixi environment was created successfully."); } - + LOGGER.debug("Using Python from Pixi environment via pixi run: " + pythonCommand); return new LegacyPythonCommand(pythonCommand); @@ -477,14 +476,14 @@ private void pushNewFlowVariable(final FlowVariable variable) { } /** - * Wraps a {@link org.knime.pixi.port.PythonCommand} into the legacy implementation for using it in a + * Wraps a {@link org.knime.pixi.port.PythonProcessProvider} into the legacy implementation for using it in a * {@link PythonKernelBackend}. */ private static final class LegacyPythonCommand implements PythonCommand { - private final org.knime.python3.PythonCommand m_pythonCommand; + private final PythonProcessProvider m_pythonCommand; - private LegacyPythonCommand(final org.knime.python3.PythonCommand pythonCommand) { + private LegacyPythonCommand(final PythonProcessProvider pythonCommand) { m_pythonCommand = pythonCommand; } diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/BundledCondaEnvironmentConfig.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/BundledCondaEnvironmentConfig.java index 15f365b2b..b46dfd8b7 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/BundledCondaEnvironmentConfig.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/BundledCondaEnvironmentConfig.java @@ -52,7 +52,7 @@ import org.knime.core.node.NodeLogger; import org.knime.core.node.defaultnodesettings.SettingsModelString; import org.knime.python3.BundledPythonCommand; -import org.knime.python3.PythonCommand; +import org.knime.python3.processprovider.PythonProcessProvider; /** * The BundledCondaEnvironmentConfig is a PythonEnvironmentConfig that points to a bundled conda environment which is @@ -107,7 +107,7 @@ public boolean isAvailable() { } @Override - public PythonCommand getPythonCommand() { + public PythonProcessProvider getPythonCommand() { final var condaEnv = CondaEnvironmentRegistry.getEnvironment(m_bundledCondaEnvironment.getStringValue()); if (condaEnv == null) { final var errorMsg = "You have selected the 'Bundled' option in KNIME Python preferences, " diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/CondaEnvironmentConfig.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/CondaEnvironmentConfig.java index dcaa8fc27..6a68da195 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/CondaEnvironmentConfig.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/CondaEnvironmentConfig.java @@ -57,7 +57,7 @@ import org.knime.core.node.NodeLogger; import org.knime.core.node.defaultnodesettings.SettingsModelString; import org.knime.python3.CondaPythonCommand; -import org.knime.python3.PythonCommand; +import org.knime.python3.processprovider.PythonProcessProvider; /** * Copied and modified from org.knime.python2.config. @@ -113,7 +113,7 @@ public ObservableValue getAvailableEnvironments() } @Override - public PythonCommand getPythonCommand() { + public PythonProcessProvider getPythonCommand() { return new CondaPythonCommand(CondaPreferences.getCondaInstallationDirectory(), m_environmentDirectory.getStringValue()); } diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/ManualEnvironmentConfig.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/ManualEnvironmentConfig.java index f0fd961b1..97a90f140 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/ManualEnvironmentConfig.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/ManualEnvironmentConfig.java @@ -49,7 +49,7 @@ package org.knime.python3.scripting.nodes.prefs; import org.knime.core.node.defaultnodesettings.SettingsModelString; -import org.knime.python3.PythonCommand; +import org.knime.python3.processprovider.PythonProcessProvider; import org.knime.python3.SimplePythonCommand; /** @@ -79,7 +79,7 @@ public SettingsModelString getExecutablePath() { } @Override - public PythonCommand getPythonCommand() { + public PythonProcessProvider getPythonCommand() { return new SimplePythonCommand(m_pythonPath.getStringValue()); } diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/Python3ScriptingPreferences.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/Python3ScriptingPreferences.java index 73a24a4e2..e8892ba44 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/Python3ScriptingPreferences.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/Python3ScriptingPreferences.java @@ -53,7 +53,7 @@ import org.knime.conda.CondaEnvironmentIdentifier; import org.knime.conda.prefs.CondaPreferences; import org.knime.python3.BundledPythonCommand; -import org.knime.python3.PythonCommand; +import org.knime.python3.processprovider.PythonProcessProvider; /** * Convenience front-end of the preference-based configuration of the Python integration. @@ -110,7 +110,7 @@ public static PythonEnvironmentType getEnvironmentTypePreference() { /** * @return The currently selected default Python command. */ - public static PythonCommand getPythonCommandPreference() { + public static PythonProcessProvider getPythonCommandPreference() { final var envType = getEnvironmentTypePreference(); PythonEnvironmentsConfig environmentsConfig; @@ -124,7 +124,7 @@ public static PythonCommand getPythonCommandPreference() { } /** - * @return The {@link PythonCommand} for the installed bundled environment. + * @return The {@link PythonProcessProvider} for the installed bundled environment. */ public static BundledPythonCommand getBundledPythonCommand() { return (BundledPythonCommand)getBundledCondaEnvironmentConfig().getPythonCommand(); diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/PythonEnvironmentConfig.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/PythonEnvironmentConfig.java index 6999dce74..d7dbb0321 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/PythonEnvironmentConfig.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/PythonEnvironmentConfig.java @@ -50,7 +50,7 @@ import org.knime.core.node.defaultnodesettings.SettingsModelBoolean; import org.knime.core.node.defaultnodesettings.SettingsModelString; -import org.knime.python3.PythonCommand; +import org.knime.python3.processprovider.PythonProcessProvider; /** * Copied from org.knime.python2.config. @@ -63,7 +63,7 @@ interface PythonEnvironmentConfig extends PythonConfig { /** * @return The command that executes Python in the Python environment configured by this instance. */ - PythonCommand getPythonCommand(); + PythonProcessProvider getPythonCommand(); /** * @return If the Python environment configured by this instance is currently the default environment. Not meant for diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/PythonKernelTester.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/PythonKernelTester.java index fa1b261a4..a002f0a94 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/PythonKernelTester.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/PythonKernelTester.java @@ -66,7 +66,7 @@ import org.knime.core.node.NodeLogger; import org.knime.core.util.FileUtil; import org.knime.core.util.Pair; -import org.knime.python3.PythonCommand; +import org.knime.python3.processprovider.PythonProcessProvider; /** * Copied from org.knime.python2. @@ -94,7 +94,7 @@ final class PythonKernelTester { * Caches previous test results. Mapping from the Python command that was tested to a list of additional required * modules that were tested along the command and the test results of these combinations of command and modules. */ - private static final Map, PythonKernelTestResult>>> TEST_RESULTS = + private static final Map, PythonKernelTestResult>>> TEST_RESULTS = new ConcurrentHashMap<>(); private static String getPythonKernelTesterPath() throws IOException { @@ -119,7 +119,7 @@ private PythonKernelTester() { * @param force Force the test to be rerun again even if the same configuration was successfully tested before. * @return The results of the installation test. */ - public static PythonKernelTestResult testPython3Installation(final PythonCommand python3Command, + public static PythonKernelTestResult testPython3Installation(final PythonProcessProvider python3Command, final Collection additionalRequiredModules, final boolean force) { return testPythonInstallation(python3Command, PYTHON_MAJOR_VERSION_3, PYTHON_MINIMUM_VERSION_3, additionalRequiredModules, Collections.emptyList(), force); @@ -128,7 +128,7 @@ public static PythonKernelTestResult testPython3Installation(final PythonCommand /** * @param minimumVersion May be {@code null} in the case where no minimum version is required. */ - private static synchronized PythonKernelTestResult testPythonInstallation(final PythonCommand pythonCommand, + private static synchronized PythonKernelTestResult testPythonInstallation(final PythonProcessProvider pythonCommand, final String majorVersion, final String minimumVersion, final Collection additionalRequiredModules, final Collection additionalOptionalModules, final boolean force) { @@ -189,7 +189,7 @@ private static synchronized PythonKernelTestResult testPythonInstallation(final return testResults; } - private static PythonKernelTestResult getPreviousTestResultsIfApplicable(final PythonCommand pythonCommand, + private static PythonKernelTestResult getPreviousTestResultsIfApplicable(final PythonProcessProvider pythonCommand, final Set additionalRequiredModules) { // If a previous, appropriate Python test already succeeded, we will not have to run it again and return the // old results here (except if we're forced to). @@ -209,7 +209,7 @@ private static PythonKernelTestResult getPreviousTestResultsIfApplicable(final P return null; } - private static Process runPythonKernelTester(final PythonCommand pythonCommand, final String majorVersion, + private static Process runPythonKernelTester(final PythonProcessProvider pythonCommand, final String majorVersion, final String minimumVersion, final Collection additionalRequiredModules, final Collection additionalOptionalModules, final StringBuilder testLogger) throws IOException { diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/ExecutableSelectionUtils.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/ExecutableSelectionUtils.java index 8551fa4d4..b60fa4a18 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/ExecutableSelectionUtils.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/ExecutableSelectionUtils.java @@ -60,7 +60,7 @@ import org.knime.conda.prefs.CondaPreferences; import org.knime.core.node.workflow.FlowObjectStack; import org.knime.python3.CondaPythonCommand; -import org.knime.python3.PythonCommand; +import org.knime.python3.processprovider.PythonProcessProvider; import org.knime.python3.SimplePythonCommand; import org.knime.python3.scripting.nodes.prefs.Python3ScriptingPreferences; import org.knime.python3.scripting.nodes2.PythonScriptingService.ExecutableOption; @@ -83,7 +83,7 @@ static Map getExecutableOptions(final FlowObjectStack } /** Get the PythonCommand from the selected option */ - static PythonCommand getPythonCommand(final ExecutableOption option) { + static PythonProcessProvider getPythonCommand(final ExecutableOption option) { switch (option.type) { case CONDA_ENV_VAR: return commandForConda(option.condaEnvDir); @@ -106,7 +106,7 @@ static PythonCommand getPythonCommand(final ExecutableOption option) { } /** Get the PythonCommand from the given settings String */ - static PythonCommand getPythonCommand(final String commandString) { + static PythonProcessProvider getPythonCommand(final String commandString) { if (commandString == null || EXEC_SELECTION_PREF_ID.equals(commandString)) { // Nothing configured -> Use preferences return commandForPreferences(); @@ -161,15 +161,15 @@ private static Stream getCondaFlowVariableOptions(final FlowOb } } - private static PythonCommand commandForConda(final String condaEnvDir) { + private static PythonProcessProvider commandForConda(final String condaEnvDir) { return new CondaPythonCommand(CondaPreferences.getCondaInstallationDirectory(), condaEnvDir); } - private static PythonCommand commandForString(final String pythonExecutable) { + private static PythonProcessProvider commandForString(final String pythonExecutable) { return new SimplePythonCommand(pythonExecutable); } - private static PythonCommand commandForPreferences() { + private static PythonProcessProvider commandForPreferences() { return Python3ScriptingPreferences.getPythonCommandPreference(); } } diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptNodeModel.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptNodeModel.java index b631670a0..8ff48d689 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptNodeModel.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptNodeModel.java @@ -92,7 +92,7 @@ import org.knime.pixi.port.PythonEnvironmentPortObject; import org.knime.python3.AbstractCondaPythonCommand; import org.knime.python3.PixiPythonCommand; -import org.knime.python3.PythonCommand; +import org.knime.python3.processprovider.PythonProcessProvider; import org.knime.python3.PythonProcessTerminatedException; import org.knime.python3.scripting.nodes.PortsConfigurationUtils; import org.knime.python3.scripting.nodes2.ConsoleOutputUtils.ConsoleOutputStorage; @@ -207,13 +207,13 @@ protected PortObject[] execute(final PortObject[] inObjects, final ExecutionCont } // Check if Pixi port is connected and use it, otherwise use configured Python command - final PythonCommand pythonCommand; + final PythonProcessProvider pythonCommand; if (m_ports.hasPixiPort()) { LOGGER.debug("Checking for Pixi environment port"); // The Pixi port is after all regular input ports final int pixiPortIndex = inObjects.length - 1; try { - final PythonCommand pixiCommand = extractPythonCommandFromPixiPort(inObjects[pixiPortIndex]); + final PythonProcessProvider pixiCommand = extractPythonCommandFromPixiPort(inObjects[pixiPortIndex]); if (pixiCommand != null) { LOGGER.debug("Using Python from Pixi environment"); pythonCommand = pixiCommand; @@ -343,7 +343,7 @@ private void pushNewFlowVariable(final FlowVariable variable) { * @return the Python command, or null if the port is not connected or doesn't contain a valid Python executable * @throws InvalidSettingsException if the Python executable path from the environment doesn't exist */ - private static PythonCommand extractPythonCommandFromPixiPort(final PortObject portObject) + private static PythonProcessProvider extractPythonCommandFromPixiPort(final PortObject portObject) throws InvalidSettingsException { if (portObject == null) { return null; @@ -357,7 +357,7 @@ private static PythonCommand extractPythonCommandFromPixiPort(final PortObject p // PythonEnvironmentPortObject.getPythonCommand() returns org.knime.pixi.port.PythonCommand, // but we need org.knime.python3.PythonCommand. Extract the pixi.toml path and create a new instance. final Path pixiToml = pythonEnvPort.getPixiEnvironmentPath().resolve("pixi.toml"); - final PythonCommand pythonCommand = new PixiPythonCommand(pixiToml); + final PythonProcessProvider pythonCommand = new PixiPythonCommand(pixiToml); LOGGER.debug("Using Python from PythonEnvironmentPortObject: " + pythonCommand); return pythonCommand; } catch (IOException e) { diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingService.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingService.java index 67c876c78..e908cec74 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingService.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingService.java @@ -80,7 +80,7 @@ import org.knime.pixi.port.PythonEnvironmentPortObject; import org.knime.python3.PixiPythonCommand; -import org.knime.python3.PythonCommand; +import org.knime.python3.processprovider.PythonProcessProvider; import org.knime.python3.scripting.nodes2.PythonScriptingService.ExecutableOption.ExecutableOptionType; import org.knime.python3.scripting.nodes2.PythonScriptingSession.ExecutionInfo; import org.knime.python3.scripting.nodes2.PythonScriptingSession.ExecutionStatus; @@ -255,7 +255,7 @@ private void startNewInteractiveSession() throws IOException, InterruptedExcepti final var inputData = workflowControl.getInputData(); // Check if Pixi port is connected (it's the last port if present) - PythonCommand pythonCommand = null; + PythonProcessProvider pythonCommand = null; PortObject[] dataPortObjects = inputData; // By default, all inputs are data ports if (m_ports.hasPixiPort() && inputData != null && inputData.length > 0) { @@ -538,7 +538,7 @@ public void close() { * @return the Python command, or null if the port is not connected or doesn't contain a valid Python executable * @throws InvalidSettingsException if the Python executable path from the environment doesn't exist */ - private static PythonCommand extractPythonCommandFromPixiPort(final PortObject portObject) + private static PythonProcessProvider extractPythonCommandFromPixiPort(final PortObject portObject) throws InvalidSettingsException { if (portObject == null) { return null; @@ -552,7 +552,7 @@ private static PythonCommand extractPythonCommandFromPixiPort(final PortObject p // PythonEnvironmentPortObject.getPythonCommand() returns org.knime.pixi.port.PythonCommand, // but we need org.knime.python3.PythonCommand. Extract the pixi.toml path and create a new instance. final Path pixiToml = pythonEnvPort.getPixiEnvironmentPath().resolve("pixi.toml"); - final PythonCommand pythonCommand = new PixiPythonCommand(pixiToml); + final PythonProcessProvider pythonCommand = new PixiPythonCommand(pixiToml); LOGGER.debug("Using Python from PythonEnvironmentPortObject: " + pythonCommand); return pythonCommand; } catch (IOException e) { diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingSession.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingSession.java index fd1c79a3d..005d410b5 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingSession.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingSession.java @@ -87,7 +87,6 @@ import org.knime.core.webui.node.dialog.scripting.ScriptingService.ConsoleText; import org.knime.python3.Activator; import org.knime.python3.Python3SourceDirectory; -import org.knime.python3.PythonCommand; import org.knime.python3.PythonEntryPointUtils; import org.knime.python3.PythonFileStoreUtils; import org.knime.python3.PythonGateway; @@ -100,6 +99,7 @@ import org.knime.python3.arrow.PythonArrowDataUtils; import org.knime.python3.arrow.PythonArrowExtension; import org.knime.python3.arrow.PythonArrowTableConverter; +import org.knime.python3.processprovider.PythonProcessProvider; import org.knime.python3.types.PythonValueFactoryModule; import org.knime.python3.types.PythonValueFactoryRegistry; import org.knime.python3.utils.FlowVariableUtils; @@ -155,7 +155,7 @@ final class PythonScriptingSession implements AsynchronousCloseable private int m_numOutObjects; - PythonScriptingSession(final PythonCommand pythonCommand, final Consumer consoleTextConsumer, + PythonScriptingSession(final PythonProcessProvider pythonCommand, final Consumer consoleTextConsumer, final FileStoreHandlerSupplier fileStoreHandlerSupplier) throws IOException, InterruptedException { m_consoleTextConsumer = consoleTextConsumer; m_fileStoreHandlerSupplier = fileStoreHandlerSupplier; @@ -418,7 +418,7 @@ Optional getOutputView() throws IOException { } } - private static PythonGateway createGateway(final PythonCommand pythonCommand) + private static PythonGateway createGateway(final PythonProcessProvider pythonCommand) throws IOException, InterruptedException { if (pythonCommand.getPythonExecutablePath() .startsWith(CondaEnvironmentIdentifier.NOT_EXECUTED_PATH_PLACEHOLDER)) { @@ -530,10 +530,10 @@ public interface FileStoreHandlerSupplier extends AutoCloseable { * Adapter that wraps org.knime.pixi.port.PythonCommand to implement org.knime.python3.PythonCommand. * This is needed because PythonGatewayDescription.builder() requires the public API type. */ - private static final class PythonCommandAdapter implements org.knime.python3.PythonCommand { - private final org.knime.python3.PythonCommand m_delegate; + private static final class PythonCommandAdapter implements PythonProcessProvider { + private final PythonProcessProvider m_delegate; - PythonCommandAdapter(final org.knime.python3.PythonCommand delegate) { + PythonCommandAdapter(final PythonProcessProvider delegate) { m_delegate = delegate; } diff --git a/org.knime.python3.scripting.tests/META-INF/MANIFEST.MF b/org.knime.python3.scripting.tests/META-INF/MANIFEST.MF index 5a1b1e50a..b064208dd 100644 --- a/org.knime.python3.scripting.tests/META-INF/MANIFEST.MF +++ b/org.knime.python3.scripting.tests/META-INF/MANIFEST.MF @@ -12,3 +12,4 @@ Export-Package: org.knime.python3.scripting Require-Bundle: org.junit;bundle-version="[4.13.0,5.0.0)", org.apache.arrow.memory-core;bundle-version="[18.1.0,19.0.0)", org.knime.python3.testing;bundle-version="[5.6.0,6.0.0)" +Import-Package: org.knime.python3.processprovider diff --git a/org.knime.python3.scripting.tests/src/test/java/org/knime/python3/scripting/Python3KernelBackendProxyTest.java b/org.knime.python3.scripting.tests/src/test/java/org/knime/python3/scripting/Python3KernelBackendProxyTest.java index aeef61a92..7b4e54b2d 100644 --- a/org.knime.python3.scripting.tests/src/test/java/org/knime/python3/scripting/Python3KernelBackendProxyTest.java +++ b/org.knime.python3.scripting.tests/src/test/java/org/knime/python3/scripting/Python3KernelBackendProxyTest.java @@ -93,6 +93,7 @@ import org.knime.python3.arrow.PythonArrowDataSource; import org.knime.python3.arrow.PythonArrowDataUtils; import org.knime.python3.arrow.PythonArrowExtension; +import org.knime.python3.processprovider.PythonProcessProvider; import org.knime.python3.testing.Python3ArrowTestUtils; import org.knime.python3.testing.Python3TestUtils; import org.knime.python3.views.Python3ViewsSourceDirectory; diff --git a/org.knime.python3.testing/META-INF/MANIFEST.MF b/org.knime.python3.testing/META-INF/MANIFEST.MF index 28ca50d58..7e1e42910 100644 --- a/org.knime.python3.testing/META-INF/MANIFEST.MF +++ b/org.knime.python3.testing/META-INF/MANIFEST.MF @@ -11,3 +11,4 @@ Require-Bundle: org.apache.commons.lang3;bundle-version="[3.9.0,4.0.0)", org.knime.core.columnar;bundle-version="[5.6.0,6.0.0)" Automatic-Module-Name: org.knime.python3.testing Export-Package: org.knime.python3.testing +Import-Package: org.knime.python3.processprovider diff --git a/org.knime.python3.testing/src/main/java/org/knime/python3/testing/Python3TestUtils.java b/org.knime.python3.testing/src/main/java/org/knime/python3/testing/Python3TestUtils.java index 7db4d2d13..85567ed16 100644 --- a/org.knime.python3.testing/src/main/java/org/knime/python3/testing/Python3TestUtils.java +++ b/org.knime.python3.testing/src/main/java/org/knime/python3/testing/Python3TestUtils.java @@ -51,8 +51,8 @@ import java.io.IOException; import org.apache.commons.lang3.SystemUtils; -import org.knime.python3.PythonCommand; import org.knime.python3.SimplePythonCommand; +import org.knime.python3.processprovider.PythonProcessProvider; /** * Contains utilities shared by multiple test fragments in knime-python. @@ -74,7 +74,7 @@ private Python3TestUtils() { * @return The command created from environment variable. * @throws IOException If none of the environment variables is set. */ - public static PythonCommand getPythonCommand() throws IOException { + public static PythonProcessProvider getPythonCommand() throws IOException { final String osSuffix; if (SystemUtils.IS_OS_LINUX) { osSuffix = "LINUX"; diff --git a/org.knime.python3/META-INF/MANIFEST.MF b/org.knime.python3/META-INF/MANIFEST.MF index eaadbff62..30c5e22ce 100644 --- a/org.knime.python3/META-INF/MANIFEST.MF +++ b/org.knime.python3/META-INF/MANIFEST.MF @@ -24,4 +24,5 @@ Automatic-Module-Name: org.knime.python3 Eclipse-RegisterBuddy: org.knime.ext.py4j Eclipse-BundleShape: dir Bundle-Activator: org.knime.python3.Activator -Import-Package: org.knime.conda.envinstall.pixi +Import-Package: org.knime.conda.envinstall.pixi, + org.knime.python3.processprovider diff --git a/org.knime.python3/src/main/java/org/knime/python3/AbstractPixiPythonCommand.java b/org.knime.python3/src/main/java/org/knime/python3/AbstractPixiPythonCommand.java index 5c1d3ff01..54d957eee 100644 --- a/org.knime.python3/src/main/java/org/knime/python3/AbstractPixiPythonCommand.java +++ b/org.knime.python3/src/main/java/org/knime/python3/AbstractPixiPythonCommand.java @@ -48,7 +48,6 @@ */ package org.knime.python3; -import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; @@ -56,6 +55,7 @@ import org.knime.conda.envinstall.pixi.PixiBinary; import org.knime.conda.envinstall.pixi.PixiBinary.PixiBinaryLocationException; +import org.knime.python3.processprovider.PythonProcessProvider; /** * Abstract base class for Python commands that use Pixi environments. Executes Python via {@code pixi run python} @@ -66,7 +66,7 @@ * * @author Marc Lehner, KNIME GmbH, Zurich, Switzerland */ -abstract class AbstractPixiPythonCommand implements PythonCommand { +abstract class AbstractPixiPythonCommand implements PythonProcessProvider { private final Path m_pixiTomlPath; @@ -115,10 +115,10 @@ public Path getPythonExecutablePath() { final Path projectDir = m_pixiTomlPath.getParent(); final Path envDir = projectDir.resolve(".pixi").resolve("envs").resolve(m_pixiEnvironmentName); final boolean isWindows = System.getProperty("os.name").toLowerCase().contains("win"); - final Path pythonPath = isWindows - ? envDir.resolve("python.exe") + final Path pythonPath = isWindows + ? envDir.resolve("python.exe") : envDir.resolve("bin").resolve("python"); - + // Return the path even if it doesn't exist yet - the environment might not be installed // The caller is responsible for checking existence if needed return pythonPath; @@ -158,7 +158,7 @@ public boolean equals(final Object obj) { @Override public String toString() { - return "pixi run --manifest-path " + m_pixiTomlPath + " --environment " + m_pixiEnvironmentName + return "pixi run --manifest-path " + m_pixiTomlPath + " --environment " + m_pixiEnvironmentName + " --no-progress python"; } } diff --git a/org.knime.python3/src/main/java/org/knime/python3/AbstractPythonCommand.java b/org.knime.python3/src/main/java/org/knime/python3/AbstractPythonCommand.java index 64d2d2e5f..342073c8f 100644 --- a/org.knime.python3/src/main/java/org/knime/python3/AbstractPythonCommand.java +++ b/org.knime.python3/src/main/java/org/knime/python3/AbstractPythonCommand.java @@ -53,13 +53,14 @@ import java.util.Collections; import java.util.List; import java.util.Objects; +import org.knime.python3.processprovider.PythonProcessProvider; /** * @author Marcel Wiedenmann, KNIME GmbH, Konstanz, Germany * @author Christian Dietz, KNIME GmbH, Konstanz, Germany * @author Benjamin Wilhelm, KNIME GmbH, Konstanz, Germany */ -abstract class AbstractPythonCommand implements PythonCommand { +abstract class AbstractPythonCommand implements PythonProcessProvider { /** The Python command and possible arguments */ protected final List m_command; diff --git a/org.knime.python3/src/main/java/org/knime/python3/BundledPythonCommand.java b/org.knime.python3/src/main/java/org/knime/python3/BundledPythonCommand.java index 2000811f6..d5a084f54 100644 --- a/org.knime.python3/src/main/java/org/knime/python3/BundledPythonCommand.java +++ b/org.knime.python3/src/main/java/org/knime/python3/BundledPythonCommand.java @@ -51,7 +51,7 @@ import org.knime.conda.CondaEnvironmentDirectory; /** - * Conda-specific implementation of {@link PythonCommand} that works with bundled Python environments. Allows to build + * Conda-specific implementation of {@link PythonProcessProvider} that works with bundled Python environments. Allows to build * Python processes for a given Conda environment. Takes care of resolving PATH-related issues on Windows. * * @author Marcel Wiedenmann, KNIME GmbH, Konstanz, Germany @@ -60,7 +60,7 @@ public final class BundledPythonCommand extends AbstractCondaPythonCommand { /** - * Constructs a {@link PythonCommand} that describes a Python process that is run in the bundled Conda environment + * Constructs a {@link PythonProcessProvider} that describes a Python process that is run in the bundled Conda environment * identified by the given Conda environment directory. The validity of the given argument is not tested. * * @param environmentDirectoryPath The path to the directory of the bundled Conda environment. diff --git a/org.knime.python3/src/main/java/org/knime/python3/CondaPythonCommand.java b/org.knime.python3/src/main/java/org/knime/python3/CondaPythonCommand.java index 37879c787..04722b5fe 100644 --- a/org.knime.python3/src/main/java/org/knime/python3/CondaPythonCommand.java +++ b/org.knime.python3/src/main/java/org/knime/python3/CondaPythonCommand.java @@ -51,7 +51,7 @@ import org.knime.conda.CondaEnvironmentDirectory; /** - * Conda-specific implementation of {@link PythonCommand}. Allows to build Python processes for a given Conda + * Conda-specific implementation of {@link PythonProcessProvider}. Allows to build Python processes for a given Conda * installation and environment. Takes care of resolving PATH-related issues on Windows. * * @author Marcel Wiedenmann, KNIME GmbH, Konstanz, Germany @@ -60,7 +60,7 @@ public final class CondaPythonCommand extends AbstractCondaPythonCommand { /** - * Constructs a {@link PythonCommand} that describes a Python process that is run in the Conda environment + * Constructs a {@link PythonProcessProvider} that describes a Python process that is run in the Conda environment * identified by the given Conda installation directory and the given Conda environment directory.
* The validity of the given arguments is not tested. * diff --git a/org.knime.python3/src/main/java/org/knime/python3/PixiPythonCommand.java b/org.knime.python3/src/main/java/org/knime/python3/PixiPythonCommand.java index cb9f0eed0..ab2c4857c 100644 --- a/org.knime.python3/src/main/java/org/knime/python3/PixiPythonCommand.java +++ b/org.knime.python3/src/main/java/org/knime/python3/PixiPythonCommand.java @@ -51,7 +51,7 @@ import java.nio.file.Path; /** - * Pixi-specific implementation of {@link PythonCommand}. Executes Python processes via {@code pixi run python} + * Pixi-specific implementation of {@link PythonProcessProvider}. Executes Python processes via {@code pixi run python} * to ensure proper environment activation and variable setup. *

* This command resolves the pixi binary and constructs a command line that invokes Python through pixi's @@ -62,7 +62,7 @@ public final class PixiPythonCommand extends AbstractPixiPythonCommand { /** - * Constructs a {@link PythonCommand} that describes a Python process run via pixi in the environment + * Constructs a {@link PythonProcessProvider} that describes a Python process run via pixi in the environment * identified by the given pixi.toml manifest file.
* The validity of the given arguments is not tested. * @@ -74,7 +74,7 @@ public PixiPythonCommand(final Path pixiTomlPath, final String environmentName) } /** - * Constructs a {@link PythonCommand} that describes a Python process run via pixi in the default environment + * Constructs a {@link PythonProcessProvider} that describes a Python process run via pixi in the default environment * identified by the given pixi.toml manifest file.
* The validity of the given arguments is not tested. * diff --git a/org.knime.python3/src/main/java/org/knime/python3/PythonCommand.java b/org.knime.python3/src/main/java/org/knime/python3/PythonCommand.java index 6bb909f7d..f320bf1d7 100644 --- a/org.knime.python3/src/main/java/org/knime/python3/PythonCommand.java +++ b/org.knime.python3/src/main/java/org/knime/python3/PythonCommand.java @@ -50,6 +50,8 @@ import java.nio.file.Path; +import org.knime.python3.processprovider.PythonProcessProvider; + /** * Describes an external Python process. The process can be started via the {@link ProcessBuilder} returned by * {@link #createProcessBuilder()}. @@ -60,27 +62,36 @@ * @author Marcel Wiedenmann, KNIME GmbH, Konstanz, Germany * @author Christian Dietz, KNIME GmbH, Konstanz, Germany * @author Benjamin Wilhelm, KNIME GmbH, Konstanz, Germany + * @deprecated Use {@link PythonProcessProvider} instead. This interface is kept for backward compatibility. */ -public interface PythonCommand { +@Deprecated(since = "5.10") +public interface PythonCommand extends PythonProcessProvider { /** * @return A {@link ProcessBuilder} that can be used to parameterize and start the Python process represented by * this command instance. */ + @Deprecated + @Override ProcessBuilder createProcessBuilder(); /** * @return The path to the Python executable. Should only be used to gather information about the Python environment * without running the Python executable. Use {@link #createProcessBuilder()} to start Python processes. */ + @Deprecated + @Override Path getPythonExecutablePath(); + @Deprecated @Override int hashCode(); + @Deprecated @Override boolean equals(Object obj); + @Deprecated @Override String toString(); -} +} \ No newline at end of file diff --git a/org.knime.python3/src/main/java/org/knime/python3/PythonGatewayFactory.java b/org.knime.python3/src/main/java/org/knime/python3/PythonGatewayFactory.java index 79552f363..4a11eccf8 100644 --- a/org.knime.python3/src/main/java/org/knime/python3/PythonGatewayFactory.java +++ b/org.knime.python3/src/main/java/org/knime/python3/PythonGatewayFactory.java @@ -55,6 +55,7 @@ import java.util.Objects; import org.knime.python3.PythonPath.PythonPathBuilder; +import org.knime.python3.processprovider.PythonProcessProvider; /** * An object that provides {@link PythonGateway PythonGateways} for a particular combination of environment, launcher @@ -105,7 +106,7 @@ interface EntryPointCustomizer { */ final class PythonGatewayDescription { - private final PythonCommand m_command; + private final PythonProcessProvider m_command; private final Path m_launcherPath; @@ -130,7 +131,7 @@ Path getLauncherPath() { return m_launcherPath; } - PythonCommand getCommand() { + PythonProcessProvider getCommand() { return m_command; } @@ -188,7 +189,7 @@ public boolean equals(final Object obj) { * @param entryPointClass the type of entry point * @return a builder for a PythonGatewayDescription */ - public static Builder builder(final PythonCommand pythonCommand, + public static Builder builder(final PythonProcessProvider pythonCommand, final Path launcherPath, final Class entryPointClass) { return new Builder<>(pythonCommand, launcherPath, entryPointClass); } @@ -203,7 +204,7 @@ public static final class Builder { private final Path m_launcherPath; - private final PythonCommand m_pythonCommand; + private final PythonProcessProvider m_pythonCommand; private final Class m_entryPointClass; @@ -213,7 +214,7 @@ public static final class Builder { private final List> m_entryPointCustomizers = new ArrayList<>(); - private Builder(final PythonCommand pythonCommand, final Path launcherPath, + private Builder(final PythonProcessProvider pythonCommand, final Path launcherPath, final Class entryPointClass) { m_launcherPath = launcherPath; m_pythonCommand = pythonCommand; diff --git a/org.knime.python3/src/main/java/org/knime/python3/QueuedPythonGatewayFactory.java b/org.knime.python3/src/main/java/org/knime/python3/QueuedPythonGatewayFactory.java index ad7ccf403..89e6e2fde 100644 --- a/org.knime.python3/src/main/java/org/knime/python3/QueuedPythonGatewayFactory.java +++ b/org.knime.python3/src/main/java/org/knime/python3/QueuedPythonGatewayFactory.java @@ -73,6 +73,7 @@ import org.knime.core.node.NodeLogger; import org.knime.python3.PythonGatewayCreationGate.PythonGatewayCreationGateListener; +import org.knime.python3.processprovider.PythonProcessProvider; import com.google.common.util.concurrent.ThreadFactoryBuilder; @@ -153,7 +154,7 @@ public synchronized void reconfigureQueue(final int maxNumberOfIdlingGateways, * * @param command The Python command whose corresponding gateways to remove from the queue */ - public synchronized void clearQueuedGateways(final PythonCommand command) { + public synchronized void clearQueuedGateways(final PythonProcessProvider command) { if (m_queue != null) { m_queue.clearQueuedGateways(command); } @@ -316,7 +317,7 @@ private BlockingQueue getGatewayQueue(final PythonGatewayDescript } @Override - public synchronized void clearQueuedGateways(final PythonCommand command) { + public synchronized void clearQueuedGateways(final PythonProcessProvider command) { final List gatewaysToEvict = new ArrayList<>(); for (final var entry : m_gateways.entrySet()) { if (entry.getKey().getCommand().equals(command)) { @@ -467,7 +468,7 @@ public PythonGatewayDummyQueue(final int maxNumberOfIdlingGateways, final int ex } @Override - public void clearQueuedGateways(final PythonCommand command) { + public void clearQueuedGateways(final PythonProcessProvider command) { // Nothing to do. } @@ -496,11 +497,11 @@ public AbstractPythonGatewayQueue(final int maxNumberOfIdlingGateways, final int getNextGateway(PythonGatewayDescription description) throws IOException, InterruptedException; /** - * Clears all queued gateways that were created with the specified {@link PythonCommand}. + * Clears all queued gateways that were created with the specified {@link PythonProcessProvider}. * - * @param command The {@link PythonCommand} + * @param command The {@link PythonProcessProvider} */ - public abstract void clearQueuedGateways(PythonCommand command); + public abstract void clearQueuedGateways(PythonProcessProvider command); @Override public abstract void close(); diff --git a/org.knime.python3/src/main/java/org/knime/python3/SimplePythonCommand.java b/org.knime.python3/src/main/java/org/knime/python3/SimplePythonCommand.java index 0475d7fa9..2c5da808f 100644 --- a/org.knime.python3/src/main/java/org/knime/python3/SimplePythonCommand.java +++ b/org.knime.python3/src/main/java/org/knime/python3/SimplePythonCommand.java @@ -51,7 +51,7 @@ import java.util.List; /** - * A simple implementation of {@link PythonCommand}. Runs a command that is given by a list of strings. + * A simple implementation of {@link PythonProcessProvider}. Runs a command that is given by a list of strings. * * @author Marcel Wiedenmann, KNIME GmbH, Konstanz, Germany * @author Christian Dietz, KNIME GmbH, Konstanz, Germany From dce5538182d18e477925f52dea5018fd262b4584 Mon Sep 17 00:00:00 2001 From: Marc Lehner Date: Mon, 26 Jan 2026 13:23:17 +0100 Subject: [PATCH 09/34] AP-25245: only use import PixiPythonCommand from knime-conda (import org.knime.pixi.port.PixiPythonCommand;) AP-25245 () --- org.knime.python3.nodes/META-INF/MANIFEST.MF | 1 + .../AbstractPythonScriptingNodeModel.java | 2 +- .../nodes2/PythonScriptNodeModel.java | 17 +- .../nodes2/PythonScriptingService.java | 11 +- .../python3/AbstractPixiPythonCommand.java | 164 ------------------ .../org/knime/python3/PixiPythonCommand.java | 86 --------- 6 files changed, 14 insertions(+), 267 deletions(-) delete mode 100644 org.knime.python3/src/main/java/org/knime/python3/AbstractPixiPythonCommand.java delete mode 100644 org.knime.python3/src/main/java/org/knime/python3/PixiPythonCommand.java diff --git a/org.knime.python3.nodes/META-INF/MANIFEST.MF b/org.knime.python3.nodes/META-INF/MANIFEST.MF index 4429ecb86..9637887dc 100644 --- a/org.knime.python3.nodes/META-INF/MANIFEST.MF +++ b/org.knime.python3.nodes/META-INF/MANIFEST.MF @@ -41,3 +41,4 @@ Automatic-Module-Name: org.knime.python3.nodes Eclipse-RegisterBuddy: org.knime.ext.py4j Eclipse-BundleShape: dir Bundle-Activator: org.knime.python3.nodes.Activator +Import-Package: org.knime.python3.processprovider diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/AbstractPythonScriptingNodeModel.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/AbstractPythonScriptingNodeModel.java index bd1afdc7a..129b9b7da 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/AbstractPythonScriptingNodeModel.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/AbstractPythonScriptingNodeModel.java @@ -81,6 +81,7 @@ import org.knime.core.util.PathUtils; import org.knime.core.util.asynclose.AsynchronousCloseableTracker; import org.knime.core.webui.node.view.NodeView; +import org.knime.pixi.port.PixiPythonCommand; import org.knime.pixi.port.PythonEnvironmentPortObject; import org.knime.python2.PythonCommand; import org.knime.python2.PythonModuleSpec; @@ -102,7 +103,6 @@ import org.knime.python2.ports.OutputPort; import org.knime.python2.ports.PickledObjectOutputPort; import org.knime.python2.ports.Port; -import org.knime.python3.PixiPythonCommand; import org.knime.python3.processprovider.PythonProcessProvider; import org.knime.python3.scripting.Python3KernelBackend; import org.knime.python3.scripting.nodes.prefs.Python3ScriptingPreferences; diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptNodeModel.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptNodeModel.java index 8ff48d689..18ca84d9e 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptNodeModel.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptNodeModel.java @@ -51,7 +51,6 @@ import java.io.File; import java.io.IOException; import java.net.ConnectException; -import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; import java.util.Collection; @@ -88,12 +87,10 @@ import org.knime.core.node.workflow.VariableTypeRegistry; import org.knime.core.util.asynclose.AsynchronousCloseableTracker; import org.knime.core.webui.node.dialog.scripting.ScriptingService.ConsoleText; - +import org.knime.pixi.port.PixiPythonCommand; import org.knime.pixi.port.PythonEnvironmentPortObject; -import org.knime.python3.AbstractCondaPythonCommand; -import org.knime.python3.PixiPythonCommand; -import org.knime.python3.processprovider.PythonProcessProvider; import org.knime.python3.PythonProcessTerminatedException; +import org.knime.python3.processprovider.PythonProcessProvider; import org.knime.python3.scripting.nodes.PortsConfigurationUtils; import org.knime.python3.scripting.nodes2.ConsoleOutputUtils.ConsoleOutputStorage; import org.knime.python3.scripting.nodes2.PythonScriptingSession.ExecutionInfo; @@ -124,7 +121,7 @@ public final class PythonScriptNodeModel extends NodeModel { private final PythonScriptNodeSettings m_settings; private final PythonScriptPortsConfiguration m_ports; - + private final PortsConfiguration m_portsConfiguration; private final AsynchronousCloseableTracker m_sessionShutdownTracker = @@ -161,7 +158,7 @@ public PythonScriptNodeModel(final PortsConfiguration portsConfiguration, final m_settings = new PythonScriptNodeSettings(m_ports); m_view = Optional.empty(); } - + /** * @return the ports configuration */ @@ -194,7 +191,7 @@ protected PortObjectSpec[] configure(final PortObjectSpec[] inSpecs) throws Inva @Override protected PortObject[] execute(final PortObject[] inObjects, final ExecutionContext exec) throws IOException, InterruptedException, CanceledExecutionException, KNIMEException { - + // Install Python environment early to avoid timeout issues during gateway connection // This must happen before creating the PythonScriptingSession if (m_ports.hasPixiPort()) { @@ -205,7 +202,7 @@ protected PortObject[] execute(final PortObject[] inObjects, final ExecutionCont throw ex; // Re-throw as-is } } - + // Check if Pixi port is connected and use it, otherwise use configured Python command final PythonProcessProvider pythonCommand; if (m_ports.hasPixiPort()) { @@ -365,7 +362,7 @@ private static PythonProcessProvider extractPythonCommandFromPixiPort(final Port } } - + return null; } catch (NoClassDefFoundError e) { // Environment port bundle not available - this should not happen if the port was added successfully diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingService.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingService.java index e908cec74..6903875ef 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingService.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingService.java @@ -77,9 +77,8 @@ import org.knime.core.webui.node.dialog.scripting.CodeGenerationRequest; import org.knime.core.webui.node.dialog.scripting.InputOutputModel; import org.knime.core.webui.node.dialog.scripting.ScriptingService; - +import org.knime.pixi.port.PixiPythonCommand; import org.knime.pixi.port.PythonEnvironmentPortObject; -import org.knime.python3.PixiPythonCommand; import org.knime.python3.processprovider.PythonProcessProvider; import org.knime.python3.scripting.nodes2.PythonScriptingService.ExecutableOption.ExecutableOptionType; import org.knime.python3.scripting.nodes2.PythonScriptingSession.ExecutionInfo; @@ -253,11 +252,11 @@ private void startNewInteractiveSession() throws IOException, InterruptedExcepti // Start the interactive Python session and setup the IO final var workflowControl = getWorkflowControl(); final var inputData = workflowControl.getInputData(); - + // Check if Pixi port is connected (it's the last port if present) PythonProcessProvider pythonCommand = null; PortObject[] dataPortObjects = inputData; // By default, all inputs are data ports - + if (m_ports.hasPixiPort() && inputData != null && inputData.length > 0) { // The Pixi port is at the end, after all data ports final int pixiPortIndex = inputData.length - 1; @@ -272,7 +271,7 @@ private void startNewInteractiveSession() throws IOException, InterruptedExcepti LOGGER.warn("Failed to extract Python command from Pixi port: " + e.getMessage()); } } - + // Fall back to user selection if no Pixi port or extraction failed if (pythonCommand == null) { pythonCommand = ExecutableSelectionUtils.getPythonCommand(getExecutableOption(m_executableSelection)); @@ -560,7 +559,7 @@ private static PythonProcessProvider extractPythonCommandFromPixiPort(final Port } } - + return null; } catch (NoClassDefFoundError e) { // Environment port bundle not available - this should not happen if the port was added successfully diff --git a/org.knime.python3/src/main/java/org/knime/python3/AbstractPixiPythonCommand.java b/org.knime.python3/src/main/java/org/knime/python3/AbstractPixiPythonCommand.java deleted file mode 100644 index 54d957eee..000000000 --- a/org.knime.python3/src/main/java/org/knime/python3/AbstractPixiPythonCommand.java +++ /dev/null @@ -1,164 +0,0 @@ -/* - * ------------------------------------------------------------------------ - * - * Copyright by KNIME AG, Zurich, Switzerland - * Website: http://www.knime.com; Email: contact@knime.com - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License, Version 3, as - * published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, see . - * - * Additional permission under GNU GPL version 3 section 7: - * - * KNIME interoperates with ECLIPSE solely via ECLIPSE's plug-in APIs. - * Hence, KNIME and ECLIPSE are both independent programs and are not - * derived from each other. Should, however, the interpretation of the - * GNU GPL Version 3 ("License") under any applicable laws result in - * KNIME and ECLIPSE being a combined program, KNIME AG herewith grants - * you the additional permission to use and propagate KNIME together with - * ECLIPSE with only the license terms in place for ECLIPSE applying to - * ECLIPSE and the GNU GPL Version 3 applying for KNIME, provided the - * license terms of ECLIPSE themselves allow for the respective use and - * propagation of ECLIPSE together with KNIME. - * - * Additional permission relating to nodes for KNIME that extend the Node - * Extension (and in particular that are based on subclasses of NodeModel, - * NodeDialog, and NodeView) and that only interoperate with KNIME through - * standard APIs ("Nodes"): - * Nodes are deemed to be separate and independent programs and to not be - * covered works. Notwithstanding anything to the contrary in the - * License, the License does not apply to Nodes, you are not required to - * license Nodes under the License, and you are granted a license to - * prepare and propagate Nodes, in each case even if such Nodes are - * propagated with or for interoperation with KNIME. The owner of a Node - * may freely choose the license terms applicable to such Node, including - * when such Node is propagated with or for interoperation with KNIME. - * --------------------------------------------------------------------- - * - * History - * Jan 13, 2026 (Marc Lehner): created - */ -package org.knime.python3; - -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; - -import org.knime.conda.envinstall.pixi.PixiBinary; -import org.knime.conda.envinstall.pixi.PixiBinary.PixiBinaryLocationException; -import org.knime.python3.processprovider.PythonProcessProvider; - -/** - * Abstract base class for Python commands that use Pixi environments. Executes Python via {@code pixi run python} - * to ensure proper environment activation and variable setup. - *

- * Implementation note: Implementors must provide value-based implementations of {@link #hashCode()}, - * {@link #equals(Object)}, and {@link #toString()}. - * - * @author Marc Lehner, KNIME GmbH, Zurich, Switzerland - */ -abstract class AbstractPixiPythonCommand implements PythonProcessProvider { - - private final Path m_pixiTomlPath; - - private final String m_pixiEnvironmentName; - - /** - * @param pixiTomlPath The path to the pixi.toml manifest file that describes the environment - * @param environmentName The name of the environment within the pixi project (typically "default") - */ - protected AbstractPixiPythonCommand(final Path pixiTomlPath, final String environmentName) { - m_pixiTomlPath = Objects.requireNonNull(pixiTomlPath, "pixiTomlPath must not be null"); - m_pixiEnvironmentName = Objects.requireNonNull(environmentName, "environmentName must not be null"); - } - - /** - * @param pixiTomlPath The path to the pixi.toml manifest file that describes the environment - */ - protected AbstractPixiPythonCommand(final Path pixiTomlPath) { - this(pixiTomlPath, "default"); - } - - @Override - public ProcessBuilder createProcessBuilder() { - try { - final String pixiBinaryPath = PixiBinary.getPixiBinaryPath(); - final List command = new ArrayList<>(); - command.add(pixiBinaryPath); - command.add("run"); - command.add("--manifest-path"); - command.add(m_pixiTomlPath.toString()); - command.add("--environment"); - command.add(m_pixiEnvironmentName); - command.add("--no-progress"); - command.add("python"); - return new ProcessBuilder(command); - } catch (PixiBinaryLocationException ex) { - throw new IllegalStateException( - "Could not locate pixi binary. Please ensure the pixi bundle is properly installed.", ex); - } - } - - @Override - public Path getPythonExecutablePath() { - // Resolve the actual Python executable path within the environment - // This is used for informational purposes only, not for execution - final Path projectDir = m_pixiTomlPath.getParent(); - final Path envDir = projectDir.resolve(".pixi").resolve("envs").resolve(m_pixiEnvironmentName); - final boolean isWindows = System.getProperty("os.name").toLowerCase().contains("win"); - final Path pythonPath = isWindows - ? envDir.resolve("python.exe") - : envDir.resolve("bin").resolve("python"); - - // Return the path even if it doesn't exist yet - the environment might not be installed - // The caller is responsible for checking existence if needed - return pythonPath; - } - - /** - * @return The path to the pixi.toml manifest file - */ - protected Path getPixiTomlPath() { - return m_pixiTomlPath; - } - - /** - * @return The environment name - */ - protected String getEnvironmentName() { - return m_pixiEnvironmentName; - } - - @Override - public int hashCode() { - return Objects.hash(m_pixiTomlPath, m_pixiEnvironmentName); - } - - @Override - public boolean equals(final Object obj) { - if (this == obj) { - return true; - } - if (obj == null || getClass() != obj.getClass()) { - return false; - } - final AbstractPixiPythonCommand other = (AbstractPixiPythonCommand)obj; - return Objects.equals(m_pixiTomlPath, other.m_pixiTomlPath) - && Objects.equals(m_pixiEnvironmentName, other.m_pixiEnvironmentName); - } - - @Override - public String toString() { - return "pixi run --manifest-path " + m_pixiTomlPath + " --environment " + m_pixiEnvironmentName - + " --no-progress python"; - } -} diff --git a/org.knime.python3/src/main/java/org/knime/python3/PixiPythonCommand.java b/org.knime.python3/src/main/java/org/knime/python3/PixiPythonCommand.java deleted file mode 100644 index ab2c4857c..000000000 --- a/org.knime.python3/src/main/java/org/knime/python3/PixiPythonCommand.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * ------------------------------------------------------------------------ - * - * Copyright by KNIME AG, Zurich, Switzerland - * Website: http://www.knime.com; Email: contact@knime.com - * - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License, Version 3, as - * published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, see . - * - * Additional permission under GNU GPL version 3 section 7: - * - * KNIME interoperates with ECLIPSE solely via ECLIPSE's plug-in APIs. - * Hence, KNIME and ECLIPSE are both independent programs and are not - * derived from each other. Should, however, the interpretation of the - * GNU GPL Version 3 ("License") under any applicable laws result in - * KNIME and ECLIPSE being a combined program, KNIME AG herewith grants - * you the additional permission to use and propagate KNIME together with - * ECLIPSE with only the license terms in place for ECLIPSE applying to - * ECLIPSE and the GNU GPL Version 3 applying for KNIME, provided the - * license terms of ECLIPSE themselves allow for the respective use and - * propagation of ECLIPSE together with KNIME. - * - * Additional permission relating to nodes for KNIME that extend the Node - * Extension (and in particular that are based on subclasses of NodeModel, - * NodeDialog, and NodeView) and that only interoperate with KNIME through - * standard APIs ("Nodes"): - * Nodes are deemed to be separate and independent programs and to not be - * covered works. Notwithstanding anything to the contrary in the - * License, the License does not apply to Nodes, you are not required to - * license Nodes under the License, and you are granted a license to - * prepare and propagate Nodes, in each case even if such Nodes are - * propagated with or for interoperation with KNIME. The owner of a Node - * may freely choose the license terms applicable to such Node, including - * when such Node is propagated with or for interoperation with KNIME. - * --------------------------------------------------------------------- - * - * History - * Jan 13, 2026 (Marc Lehner): created - */ -package org.knime.python3; - -import java.nio.file.Path; - -/** - * Pixi-specific implementation of {@link PythonProcessProvider}. Executes Python processes via {@code pixi run python} - * to ensure proper environment activation and variable setup. - *

- * This command resolves the pixi binary and constructs a command line that invokes Python through pixi's - * environment runner, which handles all necessary environment setup automatically. - * - * @author Marc Lehner, KNIME GmbH, Zurich, Switzerland - */ -public final class PixiPythonCommand extends AbstractPixiPythonCommand { - - /** - * Constructs a {@link PythonProcessProvider} that describes a Python process run via pixi in the environment - * identified by the given pixi.toml manifest file.
- * The validity of the given arguments is not tested. - * - * @param pixiTomlPath The path to the pixi.toml manifest file that describes the environment. - * @param environmentName The name of the environment within the pixi project (e.g., "default"). - */ - public PixiPythonCommand(final Path pixiTomlPath, final String environmentName) { - super(pixiTomlPath, environmentName); - } - - /** - * Constructs a {@link PythonProcessProvider} that describes a Python process run via pixi in the default environment - * identified by the given pixi.toml manifest file.
- * The validity of the given arguments is not tested. - * - * @param pixiTomlPath The path to the pixi.toml manifest file that describes the environment. - */ - public PixiPythonCommand(final Path pixiTomlPath) { - super(pixiTomlPath, "default"); - } -} From c8b6f168e08899b0b27f6c0f60d9c4234f5110ba Mon Sep 17 00:00:00 2001 From: Carsten Haubold Date: Mon, 26 Jan 2026 17:54:55 +0000 Subject: [PATCH 10/34] AP-25245: use debug instead of info log for trivial messages. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../python3/scripting/nodes/script/PythonScriptNodeFactory.java | 2 +- .../python3/scripting/nodes/view/PythonViewNodeFactory.java | 2 +- .../knime/python3/scripting/nodes2/PythonScriptingService.java | 2 +- .../scripting/nodes2/script/PythonScriptNodeFactory.java | 2 +- .../python3/scripting/nodes2/view/PythonViewNodeFactory.java | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/script/PythonScriptNodeFactory.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/script/PythonScriptNodeFactory.java index 496e9cc17..b75038d82 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/script/PythonScriptNodeFactory.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/script/PythonScriptNodeFactory.java @@ -85,7 +85,7 @@ protected Optional createPortsConfigBuilder() { BufferedDataTable.TYPE); try { b.addOptionalInputPortGroup("Python environment", PythonEnvironmentPortObject.TYPE_OPTIONAL); - LOGGER.info("Successfully added optional Python environment port"); + LOGGER.debug("Successfully added optional Python environment port"); } catch (NoClassDefFoundError e) { LOGGER.warn("Could not add Python environment port - bundle not available: " + e.getMessage()); } catch (Exception e) { diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/view/PythonViewNodeFactory.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/view/PythonViewNodeFactory.java index bb932986c..df2eb0391 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/view/PythonViewNodeFactory.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/view/PythonViewNodeFactory.java @@ -89,7 +89,7 @@ protected Optional createPortsConfigBuilder() { BufferedDataTable.TYPE); try { b.addOptionalInputPortGroup("Python environment", PythonEnvironmentPortObject.TYPE_OPTIONAL); - LOGGER.info("Successfully added optional Python environment port"); + LOGGER.debug("Successfully added optional Python environment port"); } catch (NoClassDefFoundError e) { LOGGER.warn("Could not add Python environment port - bundle not available: " + e.getMessage()); } catch (Exception e) { diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingService.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingService.java index 6903875ef..ca8d0f66d 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingService.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingService.java @@ -263,7 +263,7 @@ private void startNewInteractiveSession() throws IOException, InterruptedExcepti try { pythonCommand = extractPythonCommandFromPixiPort(inputData[pixiPortIndex]); if (pythonCommand != null) { - LOGGER.info("Using Python environment from connected Pixi port for interactive session"); + LOGGER.debug("Using Python environment from connected Pixi port for interactive session"); // Filter out Pixi port from data ports - it's not a data port for setupIO dataPortObjects = java.util.Arrays.copyOf(inputData, inputData.length - 1); } diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/script/PythonScriptNodeFactory.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/script/PythonScriptNodeFactory.java index 02f3cab23..c44bb260f 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/script/PythonScriptNodeFactory.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/script/PythonScriptNodeFactory.java @@ -129,7 +129,7 @@ protected Optional createPortsConfigBuilder() { try { final Class pythonEnvClass = PythonEnvironmentPortObject.class; b.addOptionalInputPortGroup(PORTGR_ID_PYTHON_ENV, PythonEnvironmentPortObject.TYPE_OPTIONAL); - LOGGER.info("Successfully added Python environment port"); + LOGGER.debug("Successfully added Python environment port"); } catch (NoClassDefFoundError e) { LOGGER.debug("PythonEnvironmentPortObject not available: " + e.getMessage()); } diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/view/PythonViewNodeFactory.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/view/PythonViewNodeFactory.java index 29c844762..76d51e785 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/view/PythonViewNodeFactory.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/view/PythonViewNodeFactory.java @@ -138,7 +138,7 @@ protected Optional createPortsConfigBuilder() { try { final Class pythonEnvClass = PythonEnvironmentPortObject.class; b.addOptionalInputPortGroup(PORTGR_ID_PYTHON_ENV, PythonEnvironmentPortObject.TYPE_OPTIONAL); - LOGGER.info("Successfully added Python environment port"); + LOGGER.debug("Successfully added Python environment port"); } catch (NoClassDefFoundError e) { LOGGER.debug("PythonEnvironmentPortObject not available: " + e.getMessage()); } From 0691a21b153db281fc7af87cbf38b92206e0647b Mon Sep 17 00:00:00 2001 From: Marc Lehner Date: Tue, 10 Feb 2026 16:26:07 +0100 Subject: [PATCH 11/34] AP-25245: update all plugins to 5.11 AP-25245 () --- org.knime.python3.nodes/META-INF/MANIFEST.MF | 4 ++-- org.knime.python3.scripting.nodes/META-INF/MANIFEST.MF | 2 +- org.knime.python3/META-INF/MANIFEST.MF | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/org.knime.python3.nodes/META-INF/MANIFEST.MF b/org.knime.python3.nodes/META-INF/MANIFEST.MF index 9637887dc..11f917ee7 100644 --- a/org.knime.python3.nodes/META-INF/MANIFEST.MF +++ b/org.knime.python3.nodes/META-INF/MANIFEST.MF @@ -36,9 +36,9 @@ Require-Bundle: org.knime.core;bundle-version="[5.10.0,6.0.0)", com.fasterxml.jackson.core.jackson-annotations;bundle-version="[2.13.2,3.0.0)", org.knime.workflowservices;bundle-version="[5.10.0,6.0.0)", org.apache.commons.lang3;bundle-version="[3.9.0,4.0.0)", - org.knime.gateway.impl;bundle-version="[5.10.0,6.0.0)" + org.knime.gateway.impl;bundle-version="[5.10.0,6.0.0)", + org.knime.python3.processprovider;bundle-version="[5.11.0,6.0.0)" Automatic-Module-Name: org.knime.python3.nodes Eclipse-RegisterBuddy: org.knime.ext.py4j Eclipse-BundleShape: dir Bundle-Activator: org.knime.python3.nodes.Activator -Import-Package: org.knime.python3.processprovider diff --git a/org.knime.python3.scripting.nodes/META-INF/MANIFEST.MF b/org.knime.python3.scripting.nodes/META-INF/MANIFEST.MF index 839c7237c..3203142de 100644 --- a/org.knime.python3.scripting.nodes/META-INF/MANIFEST.MF +++ b/org.knime.python3.scripting.nodes/META-INF/MANIFEST.MF @@ -23,7 +23,7 @@ Require-Bundle: org.knime.core;bundle-version="[5.11.0,6.0.0)", org.eclipse.ui;bundle-version="3.119.0", org.knime.conda;bundle-version="[5.11.0,6.0.0)", org.knime.conda.envbundling;bundle-version="[5.10.0,6.0.0)", - org.knime.pixi.port;bundle-version="[5.10.0,6.0.0)";resolution:=optional, + org.knime.pixi.port;bundle-version="[5.11.0,6.0.0)";resolution:=optional, org.knime.core.ui;bundle-version="[5.10.0,6.0.0)", org.knime.workbench.editor;bundle-version="[5.9.0,6.0.0)", org.apache.batik.util;bundle-version="[1.16.0,2.0.0)", diff --git a/org.knime.python3/META-INF/MANIFEST.MF b/org.knime.python3/META-INF/MANIFEST.MF index 30c5e22ce..a261deb8a 100644 --- a/org.knime.python3/META-INF/MANIFEST.MF +++ b/org.knime.python3/META-INF/MANIFEST.MF @@ -17,7 +17,7 @@ Require-Bundle: com.google.guava;bundle-version="[31.0.1,32.0.0)", org.knime.python3.types;bundle-version="[5.9.0,6.0.0)", org.eclipse.equinox.p2.core;bundle-version="[2.0.0,3.0.0)";visibility:=reexport, org.eclipse.equinox.p2.engine;bundle-version="[2.0.0,3.0.0)";visibility:=reexport, - org.knime.pixi.port;bundle-version="[5.10.0,6.0.0)" + org.knime.pixi.port;bundle-version="[5.11.0,6.0.0)" Export-Package: org.knime.python3, org.knime.python3.utils Automatic-Module-Name: org.knime.python3 From 54aa7b52cd598b9b8af73f7bc801d7b5e8152a1c Mon Sep 17 00:00:00 2001 From: Marc Lehner Date: Tue, 10 Feb 2026 16:59:09 +0100 Subject: [PATCH 12/34] AP-25245: move pixi code to python port AP-25245 () --- .../AbstractPythonScriptingNodeModel.java | 6 +-- .../nodes/PortsConfigurationUtils.java | 41 +++---------------- .../nodes2/PythonScriptNodeModel.java | 28 ++++--------- 3 files changed, 14 insertions(+), 61 deletions(-) diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/AbstractPythonScriptingNodeModel.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/AbstractPythonScriptingNodeModel.java index 129b9b7da..4dc60fe01 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/AbstractPythonScriptingNodeModel.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/AbstractPythonScriptingNodeModel.java @@ -193,11 +193,7 @@ private static final PortType[] toPortTypes(final Port[] ports, final boolean ha for (int i = 0; i < ports.length; i++) { portTypes[i] = ports[i].getPortType(); } - try { - portTypes[ports.length] = PythonEnvironmentPortObject.TYPE_OPTIONAL; - } catch (NoClassDefFoundError e) { - throw new IllegalStateException("Could not load PythonEnvironmentPortObject class", e); - } + portTypes[ports.length] = PythonEnvironmentPortObject.TYPE_OPTIONAL; return portTypes; } diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/PortsConfigurationUtils.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/PortsConfigurationUtils.java index e3a6c2ff2..6d8ab68c3 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/PortsConfigurationUtils.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/PortsConfigurationUtils.java @@ -57,7 +57,6 @@ import org.knime.core.node.port.PortObject; import org.knime.core.node.port.PortType; import org.knime.core.node.port.image.ImagePortObject; -import org.knime.pixi.port.PixiInstallationProgressReporter; import org.knime.pixi.port.PythonEnvironmentPortObject; import org.knime.python2.port.PickledObjectFileStorePortObject; import org.knime.python2.ports.DataTableInputPort; @@ -90,7 +89,7 @@ public static boolean hasPixiPort(final PortsConfiguration config) { try { // Check if any input port is a PythonEnvironmentPortObject for (final PortType inType : inTypes) { - if (inType.equals(PythonEnvironmentPortObject.TYPE) || inType.equals(PythonEnvironmentPortObject.TYPE_OPTIONAL)) { + if (isPixiPort(inType)) { return true; } } @@ -185,7 +184,7 @@ public static OutputPort[] createOutputPorts(final PortsConfiguration config) { public static OutputPort createPickledObjectOutputPort(final int outObjectSuffix) { return new PickledObjectOutputPort("knio.output_objects[" + outObjectSuffix + "]"); } - + /** * Extract the Python environment port object from the input port objects, if present. * @@ -203,40 +202,12 @@ public static PythonEnvironmentPortObject extractPythonEnvironmentPort( } return null; } - - /** - * Install the Python environment port if present, with progress reporting. - * This should be called early in node execution to avoid installation timeout issues. - * Installation is thread-safe and will only happen once even if called multiple times. - * - * @param config the ports configuration - * @param inObjects the input port objects - * @param exec the execution monitor for progress reporting and cancellation - * @throws IOException if installation fails - * @throws CanceledExecutionException if the operation is canceled - */ - public static void installPythonEnvironmentIfPresent( - final PortsConfiguration config, final PortObject[] inObjects, final ExecutionMonitor exec) - throws IOException, CanceledExecutionException { + + public static void installPythonEnvironmentIfPresent(final PortsConfiguration config, final PortObject[] inObjects, + final ExecutionMonitor exec) throws IOException, CanceledExecutionException { final PythonEnvironmentPortObject envPort = extractPythonEnvironmentPort(config, inObjects); if (envPort != null) { - exec.setMessage("Installing Python environment..."); - // Create simulated progress reporter that maps internal progress to node progress - final PixiInstallationProgressReporter progressReporter = new PixiInstallationProgressReporter() { - @Override - public void setProgress(final double fraction, final String message) { - exec.setProgress(fraction, message); - } - - @Override - public void checkCanceled() throws CanceledExecutionException { - exec.checkCanceled(); - } - }; - - // Use simulated progress since we don't yet capture pixi output - envPort.installPixiEnvironment(exec, - PixiInstallationProgressReporter.createSimulated(progressReporter)); + PythonEnvironmentPortObject.installPythonEnvironmentWithProgress(config, inObjects, exec); } } } diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptNodeModel.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptNodeModel.java index 18ca84d9e..22e4a83a5 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptNodeModel.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptNodeModel.java @@ -338,36 +338,22 @@ private void pushNewFlowVariable(final FlowVariable variable) { * * @param portObject the port object (may be null if optional port is not connected) * @return the Python command, or null if the port is not connected or doesn't contain a valid Python executable - * @throws InvalidSettingsException if the Python executable path from the environment doesn't exist + * @throws InvalidSettingsException if the Python command cannot be obtained from the environment */ private static PythonProcessProvider extractPythonCommandFromPixiPort(final PortObject portObject) throws InvalidSettingsException { - if (portObject == null) { - return null; - } - try { - // Check if this is a PythonEnvironmentPortObject (new unified type) - if (portObject instanceof PythonEnvironmentPortObject) { - final PythonEnvironmentPortObject pythonEnvPort = (PythonEnvironmentPortObject)portObject; - try { - // PythonEnvironmentPortObject.getPythonCommand() returns org.knime.pixi.port.PythonCommand, - // but we need org.knime.python3.PythonCommand. Extract the pixi.toml path and create a new instance. - final Path pixiToml = pythonEnvPort.getPixiEnvironmentPath().resolve("pixi.toml"); - final PythonProcessProvider pythonCommand = new PixiPythonCommand(pixiToml); - LOGGER.debug("Using Python from PythonEnvironmentPortObject: " + pythonCommand); - return pythonCommand; - } catch (IOException e) { - throw new InvalidSettingsException("Failed to get Python command from environment: " + e.getMessage(), e); - } + final PythonProcessProvider pythonCommand = PythonEnvironmentPortObject.extractPythonCommand(portObject); + if (pythonCommand != null) { + LOGGER.debug("Using Python from PythonEnvironmentPortObject: " + pythonCommand); } - - - return null; + return pythonCommand; } catch (NoClassDefFoundError e) { // Environment port bundle not available - this should not happen if the port was added successfully LOGGER.debug("Environment port class not available", e); return null; + } catch (IOException e) { + throw new InvalidSettingsException("Failed to get Python command from environment: " + e.getMessage(), e); } } From d461935df9cf9414943ff39cf4be480d08b06735 Mon Sep 17 00:00:00 2001 From: Marc Lehner Date: Wed, 11 Feb 2026 09:34:20 +0100 Subject: [PATCH 13/34] AP-25245: consistent nameing of python environment port and methods AP-25245 () --- .../AbstractPythonScriptingNodeModel.java | 92 +++++-------------- .../nodes/PortsConfigurationUtils.java | 61 ++++++++---- .../prefs/Python3ScriptingPreferences.java | 9 +- .../nodes/script/PythonScriptNodeFactory.java | 2 +- .../nodes/script/PythonScriptNodeModel.java | 8 +- .../nodes/view/PythonViewNodeFactory.java | 2 +- .../nodes/view/PythonViewNodeModel.java | 4 +- .../nodes2/PythonScriptNodeModel.java | 87 +++++------------- .../PythonScriptPortsConfiguration.java | 45 ++------- .../PythonScriptingInputOutputModelUtils.java | 10 +- .../nodes2/PythonScriptingService.java | 60 +++--------- .../nodes2/PythonScriptingSession.java | 4 + .../script/PythonScriptNodeFactory.java | 1 - .../nodes2/view/PythonViewNodeFactory.java | 1 - org.knime.python3/META-INF/MANIFEST.MF | 6 +- .../java/org/knime/python3/PythonCommand.java | 12 +-- 16 files changed, 147 insertions(+), 257 deletions(-) diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/AbstractPythonScriptingNodeModel.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/AbstractPythonScriptingNodeModel.java index 4dc60fe01..a6745fc6d 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/AbstractPythonScriptingNodeModel.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/AbstractPythonScriptingNodeModel.java @@ -158,7 +158,7 @@ static void setExpectedOutputView(final PythonKernel kernel, final boolean expec private final boolean m_hasView; - private final boolean m_hasPixiPort; + private final boolean m_hasPythonEnvironmentPort; private String m_script; @@ -170,12 +170,12 @@ static void setExpectedOutputView(final PythonKernel kernel, final boolean expec new AsynchronousCloseableTracker<>(t -> LOGGER.debug("Kernel shutdown failed.", t)); protected AbstractPythonScriptingNodeModel(final InputPort[] inPorts, final OutputPort[] outPorts, - final boolean hasView, final boolean hasPixiPort, final String defaultScript) { - super(toPortTypes(inPorts, hasPixiPort), toPortTypes(outPorts)); + final boolean hasView, final boolean hasPythonEnvironmentPort, final String defaultScript) { + super(toPortTypes(inPorts, hasPythonEnvironmentPort), toPortTypes(outPorts)); m_inPorts = inPorts; m_outPorts = outPorts; m_hasView = hasView; - m_hasPixiPort = hasPixiPort; + m_hasPythonEnvironmentPort = hasPythonEnvironmentPort; m_view = Optional.empty(); m_script = defaultScript; } @@ -184,8 +184,8 @@ private static final PortType[] toPortTypes(final Port[] ports) { return Arrays.stream(ports).map(Port::getPortType).toArray(PortType[]::new); } - private static final PortType[] toPortTypes(final Port[] ports, final boolean hasPixiPort) { - if (!hasPixiPort) { + private static final PortType[] toPortTypes(final Port[] ports, final boolean hasPythonEnvironmentPort) { + if (!hasPythonEnvironmentPort) { return toPortTypes(ports); } // Add the optional Python environment port at the end of the input ports @@ -217,7 +217,7 @@ protected void loadValidatedSettingsFrom(final NodeSettingsRO settings) throws I @Override protected PortObjectSpec[] configure(final PortObjectSpec[] inSpecs) throws InvalidSettingsException { - // The Pixi port (if present) is at the end of the input specs + // The Python environment port (if present) is at the end of the input specs final int numRegularPorts = m_inPorts.length; for (int i = 0; i < numRegularPorts; i++) { m_inPorts[i].configure(inSpecs[i]); @@ -227,13 +227,19 @@ protected PortObjectSpec[] configure(final PortObjectSpec[] inSpecs) throws Inva @Override protected PortObject[] execute(final PortObject[] inObjects, final ExecutionContext exec) throws Exception { - // Extract Pixi environment if present - // The Pixi port (if present) is at the end of the input objects - final PythonCommand pythonCommandFromPixi; - if (m_hasPixiPort && inObjects.length > m_inPorts.length) { - pythonCommandFromPixi = extractPythonCommandFromPixiPort(inObjects[inObjects.length - 1]); + // Extract Python command from environment port if present + final PythonCommand pythonCommandFromEnv; + if (m_hasPythonEnvironmentPort) { + try { + final PythonProcessProvider pythonProvider = PythonEnvironmentPortObject.extractPythonCommand( + PortsConfigurationUtils.extractPythonEnvironmentPort(getPortsConfiguration(), inObjects)); + pythonCommandFromEnv = pythonProvider != null ? new LegacyPythonCommand(pythonProvider) : null; + } catch (NoClassDefFoundError e) { + LOGGER.debug("Environment port class not available", e); + pythonCommandFromEnv = null; + } } else { - pythonCommandFromPixi = null; + pythonCommandFromEnv = null; } double inWeight = 0d; @@ -248,7 +254,7 @@ protected PortObject[] execute(final PortObject[] inObjects, final ExecutionCont final var cancelable = new PythonExecutionMonitorCancelable(exec); try (final PythonKernel kernel = getNextKernelFromQueue(requiredAdditionalModules, Collections.emptySet(), cancelable, - pythonCommandFromPixi)) { + pythonCommandFromEnv)) { final Collection inFlowVariables = getAvailableFlowVariables(Python3KernelBackend.getCompatibleFlowVariableTypes()).values(); kernel.putFlowVariables(null, inFlowVariables); @@ -380,54 +386,6 @@ private static Path persistedViewPath(final File nodeInternDir) { return nodeInternDir.toPath().resolve("view.html"); } - /** - * Extract the Python command from a PythonEnvironmentPortObject. - * - * @param portObject the port object (may be null if optional port is not connected) - * @return the Python command, or null if the port is not connected or doesn't contain a valid Python executable - * @throws InvalidSettingsException if the Python executable path from the Pixi environment doesn't exist - */ - private static PythonCommand extractPythonCommandFromPixiPort(final PortObject portObject) - throws InvalidSettingsException { - if (portObject == null) { - return null; - } - - try { - if (!(portObject instanceof PythonEnvironmentPortObject)) { - return null; - } - - // Handle PythonEnvironmentPortObject - final PythonEnvironmentPortObject pythonEnvPort = (PythonEnvironmentPortObject)portObject; - final Path pixiTomlPath; - try { - pixiTomlPath = pythonEnvPort.getPixiEnvironmentPath().resolve("pixi.toml"); - } catch (IOException e) { - throw new InvalidSettingsException("Failed to get pixi.toml path from PythonEnvironmentPortObject: " + e.getMessage(), e); - } - - // Create PixiPythonCommand from the pixi.toml path - final PythonProcessProvider pythonCommand = new PixiPythonCommand(pixiTomlPath); - - // Verify that the Python executable exists - final Path pythonExecPath = pythonCommand.getPythonExecutablePath(); - if (!Files.exists(pythonExecPath)) { - throw new InvalidSettingsException( - "The Python executable from the Pixi environment does not exist at path: " + pythonExecPath - + ". Please check that the Pixi environment was created successfully."); - } - - LOGGER.debug("Using Python from Pixi environment via pixi run: " + pythonCommand); - return new LegacyPythonCommand(pythonCommand); - - } catch (NoClassDefFoundError e) { - // Python environment bundle is not available - this is fine since it's optional - LOGGER.debug("PythonEnvironmentPortObject class not available - bundle may not be installed", e); - return null; - } - } - protected PythonKernel getNextKernelFromQueue(final Set requiredAdditionalModules, final Set optionalAdditionalModules, final PythonCancelable cancelable) throws PythonCanceledExecutionException, PythonIOException { @@ -436,12 +394,12 @@ protected PythonKernel getNextKernelFromQueue(final Set requir protected PythonKernel getNextKernelFromQueue(final Set requiredAdditionalModules, final Set optionalAdditionalModules, final PythonCancelable cancelable, - final PythonCommand pythonCommandFromPixi) + final PythonCommand pythonCommandFromEnv) throws PythonCanceledExecutionException, PythonIOException { - // Use Python command from Pixi port if available - // TODO: We might want to consider flow variables in addition to the Pixi port in the future + // Use Python command from environment port if available + // TODO: We might want to consider flow variables in addition to the environment port in the future final PythonCommand commandToUse = - pythonCommandFromPixi != null ? pythonCommandFromPixi : m_command.getCommand(); + pythonCommandFromEnv != null ? pythonCommandFromEnv : m_command.getCommand(); return PythonKernelQueue.getNextKernel(commandToUse, PythonKernelBackendType.PYTHON3, requiredAdditionalModules, optionalAdditionalModules, new PythonKernelOptions(), cancelable); @@ -472,7 +430,7 @@ private void pushNewFlowVariable(final FlowVariable variable) { } /** - * Wraps a {@link org.knime.pixi.port.PythonProcessProvider} into the legacy implementation for using it in a + * Wraps a {@link PythonProcessProvider} into the legacy implementation for using it in a * {@link PythonKernelBackend}. */ private static final class LegacyPythonCommand implements PythonCommand { diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/PortsConfigurationUtils.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/PortsConfigurationUtils.java index 6d8ab68c3..d3e58ef55 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/PortsConfigurationUtils.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/PortsConfigurationUtils.java @@ -48,11 +48,7 @@ */ package org.knime.python3.scripting.nodes; -import java.io.IOException; - import org.knime.core.node.BufferedDataTable; -import org.knime.core.node.CanceledExecutionException; -import org.knime.core.node.ExecutionMonitor; import org.knime.core.node.context.ports.PortsConfiguration; import org.knime.core.node.port.PortObject; import org.knime.core.node.port.PortType; @@ -84,12 +80,12 @@ private PortsConfigurationUtils() { * @param config the ports configuration * @return true if a Python environment port is present */ - public static boolean hasPixiPort(final PortsConfiguration config) { + public static boolean hasPythonEnvironmentPort(final PortsConfiguration config) { final PortType[] inTypes = config.getInputPorts(); try { // Check if any input port is a PythonEnvironmentPortObject for (final PortType inType : inTypes) { - if (isPixiPort(inType)) { + if (isPythonEnvironmentPort(inType)) { return true; } } @@ -109,19 +105,19 @@ public static InputPort[] createInputPorts(final PortsConfiguration config) { final PortType[] inTypes = config.getInputPorts(); int inTableIndex = 0; int inObjectIndex = 0; - // Count non-Pixi ports for the result array - int numNonPixiPorts = 0; + // Count non-environment ports for the result array + int numNonEnvironmentPorts = 0; for (final PortType inType : inTypes) { - if (!isPixiPort(inType)) { - numNonPixiPorts++; + if (!isPythonEnvironmentPort(inType)) { + numNonEnvironmentPorts++; } } - final var inPorts = new InputPort[numNonPixiPorts]; + final var inPorts = new InputPort[numNonEnvironmentPorts]; int portIndex = 0; for (int i = 0; i < inTypes.length; i++) { final PortType inType = inTypes[i]; - // Skip Pixi ports - they are not InputPorts in the traditional sense - if (isPixiPort(inType)) { + // Skip Python environment ports - they are not InputPorts in the traditional sense + if (isPythonEnvironmentPort(inType)) { continue; } final InputPort inPort; @@ -137,7 +133,13 @@ public static InputPort[] createInputPorts(final PortsConfiguration config) { return inPorts; } - private static boolean isPixiPort(final PortType inType) { + /** + * Check if a port type is a Python environment port. + * + * @param inType the port type to check + * @return true if the port type is a Python environment port + */ + public static boolean isPythonEnvironmentPort(final PortType inType) { try { return inType.equals(PythonEnvironmentPortObject.TYPE) || inType.equals(PythonEnvironmentPortObject.TYPE_OPTIONAL); } catch (NoClassDefFoundError e) { @@ -196,18 +198,43 @@ public static PythonEnvironmentPortObject extractPythonEnvironmentPort( final PortsConfiguration config, final PortObject[] inObjects) { final PortType[] inTypes = config.getInputPorts(); for (int i = 0; i < inTypes.length; i++) { - if (isPixiPort(inTypes[i]) && inObjects[i] instanceof PythonEnvironmentPortObject) { + if (isPythonEnvironmentPort(inTypes[i]) && PythonEnvironmentPortObject.isPythonEnvironmentPortObject(inObjects[i])) { return (PythonEnvironmentPortObject) inObjects[i]; } } return null; } - public static void installPythonEnvironmentIfPresent(final PortsConfiguration config, final PortObject[] inObjects, + /*public static void installPythonEnvironmentIfPresent(final PortsConfiguration config, final PortObject[] inObjects, final ExecutionMonitor exec) throws IOException, CanceledExecutionException { final PythonEnvironmentPortObject envPort = extractPythonEnvironmentPort(config, inObjects); if (envPort != null) { PythonEnvironmentPortObject.installPythonEnvironmentWithProgress(config, inObjects, exec); } + }*/ + + /** + * Filter out the Python environment port from the input port objects array. + * The environment port is not a data port and should not be passed to the session. + * + * @param config the ports configuration + * @param inObjects the input port objects + * @return the filtered array without the Python environment port + */ + public static PortObject[] filterEnvironmentPort(final PortsConfiguration config, final PortObject[] inObjects) { + if (!hasPythonEnvironmentPort(config)) { + return inObjects; + } + + // Find and exclude the environment port + final PortType[] inTypes = config.getInputPorts(); + final PortObject[] filtered = new PortObject[inObjects.length - 1]; + int filteredIndex = 0; + for (int i = 0; i < inTypes.length && i < inObjects.length; i++) { + if (!isPythonEnvironmentPort(inTypes[i])) { + filtered[filteredIndex++] = inObjects[i]; + } + } + return filtered; } -} +} \ No newline at end of file diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/Python3ScriptingPreferences.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/Python3ScriptingPreferences.java index e8892ba44..8cf6524df 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/Python3ScriptingPreferences.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/Python3ScriptingPreferences.java @@ -127,7 +127,14 @@ public static PythonProcessProvider getPythonCommandPreference() { * @return The {@link PythonProcessProvider} for the installed bundled environment. */ public static BundledPythonCommand getBundledPythonCommand() { - return (BundledPythonCommand)getBundledCondaEnvironmentConfig().getPythonCommand(); + final var pythonCommand = getBundledCondaEnvironmentConfig().getPythonCommand(); + if (!(pythonCommand instanceof BundledPythonCommand)) { + throw new IllegalStateException( + "Bundled Python environment '" + BUNDLED_PYTHON_ENV_ID + + "' does not provide a BundledPythonCommand (got: " + + (pythonCommand == null ? "null" : pythonCommand.getClass().getName()) + ")"); + } + return (BundledPythonCommand)pythonCommand; } private static BundledCondaEnvironmentConfig getBundledCondaEnvironmentConfig() { diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/script/PythonScriptNodeFactory.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/script/PythonScriptNodeFactory.java index b75038d82..85550b5bd 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/script/PythonScriptNodeFactory.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/script/PythonScriptNodeFactory.java @@ -106,7 +106,7 @@ protected PythonScriptNodeModel createNodeModel(final NodeCreationConfiguration return PythonScriptNodeModel.createDnDNodeModel(urlConfig.get().getUrl()); } return new PythonScriptNodeModel(createInputPorts(config), createOutputPorts(config), - PortsConfigurationUtils.hasPixiPort(config)); + PortsConfigurationUtils.hasPythonEnvironmentPort(config)); } @Override diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/script/PythonScriptNodeModel.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/script/PythonScriptNodeModel.java index 20bb426a2..ca4381d59 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/script/PythonScriptNodeModel.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/script/PythonScriptNodeModel.java @@ -66,13 +66,13 @@ */ final class PythonScriptNodeModel extends AbstractPythonScriptingNodeModel { - public PythonScriptNodeModel(final InputPort[] inPorts, final OutputPort[] outPorts, final boolean hasPixiPort) { - super(inPorts, outPorts, false, hasPixiPort, createDefaultScript(inPorts, outPorts)); + public PythonScriptNodeModel(final InputPort[] inPorts, final OutputPort[] outPorts, final boolean hasPythonEnvironmentPort) { + super(inPorts, outPorts, false, hasPythonEnvironmentPort, createDefaultScript(inPorts, outPorts)); } - PythonScriptNodeModel(final InputPort[] inPorts, final OutputPort[] outPorts, final boolean hasPixiPort, + PythonScriptNodeModel(final InputPort[] inPorts, final OutputPort[] outPorts, final boolean hasPythonEnvironmentPort, final String defaultScript) { - super(inPorts, outPorts, false, hasPixiPort, defaultScript); + super(inPorts, outPorts, false, hasPythonEnvironmentPort, defaultScript); } diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/view/PythonViewNodeFactory.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/view/PythonViewNodeFactory.java index df2eb0391..d6bfc067e 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/view/PythonViewNodeFactory.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/view/PythonViewNodeFactory.java @@ -103,7 +103,7 @@ protected Optional createPortsConfigBuilder() { protected PythonViewNodeModel createNodeModel(final NodeCreationConfiguration creationConfig) { final var config = creationConfig.getPortConfig().get(); // NOSONAR return new PythonViewNodeModel(createInputPorts(config), createOutputPorts(config), - PortsConfigurationUtils.hasPixiPort(config)); + PortsConfigurationUtils.hasPythonEnvironmentPort(config)); } @Override diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/view/PythonViewNodeModel.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/view/PythonViewNodeModel.java index 39237e1c2..bba354707 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/view/PythonViewNodeModel.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/view/PythonViewNodeModel.java @@ -61,8 +61,8 @@ */ final class PythonViewNodeModel extends AbstractPythonScriptingNodeModel { - public PythonViewNodeModel(final InputPort[] inPorts, final OutputPort[] outPorts, final boolean hasPixiPort) { - super(inPorts, outPorts, true, hasPixiPort, createDefaultScript(inPorts)); + public PythonViewNodeModel(final InputPort[] inPorts, final OutputPort[] outPorts, final boolean hasPythonEnvironmentPort) { + super(inPorts, outPorts, true, hasPythonEnvironmentPort, createDefaultScript(inPorts)); } Path getPathToHtml() { diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptNodeModel.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptNodeModel.java index 22e4a83a5..62a4ca640 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptNodeModel.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptNodeModel.java @@ -52,7 +52,6 @@ import java.io.IOException; import java.net.ConnectException; import java.nio.file.Path; -import java.util.Arrays; import java.util.Collection; import java.util.Map; import java.util.Objects; @@ -87,7 +86,6 @@ import org.knime.core.node.workflow.VariableTypeRegistry; import org.knime.core.util.asynclose.AsynchronousCloseableTracker; import org.knime.core.webui.node.dialog.scripting.ScriptingService.ConsoleText; -import org.knime.pixi.port.PixiPythonCommand; import org.knime.pixi.port.PythonEnvironmentPortObject; import org.knime.python3.PythonProcessTerminatedException; import org.knime.python3.processprovider.PythonProcessProvider; @@ -193,35 +191,25 @@ protected PortObject[] execute(final PortObject[] inObjects, final ExecutionCont throws IOException, InterruptedException, CanceledExecutionException, KNIMEException { // Install Python environment early to avoid timeout issues during gateway connection - // This must happen before creating the PythonScriptingSession - if (m_ports.hasPixiPort()) { - try { - PortsConfigurationUtils.installPythonEnvironmentIfPresent( - getPortsConfiguration(), inObjects, exec); - } catch (IOException | CanceledExecutionException ex) { - throw ex; // Re-throw as-is - } + final ExecutionMonitor restOfTheProgress; + if (m_ports.hasPythonEnvironmentPort()) { + //PortsConfigurationUtils.installPythonEnvironmentIfPresent(getPortsConfiguration(), inObjects, + // exec.createSubProgress(0.2)); + PythonEnvironmentPortObject.installPythonEnvironmentWithProgress(getPortsConfiguration(), inObjects, + exec.createSubProgress(0.2)); + restOfTheProgress = exec.createSubProgress(0.8); + } else { + restOfTheProgress = exec; } - // Check if Pixi port is connected and use it, otherwise use configured Python command + // Extract Python command from environment port if connected, otherwise use configured command final PythonProcessProvider pythonCommand; - if (m_ports.hasPixiPort()) { - LOGGER.debug("Checking for Pixi environment port"); - // The Pixi port is after all regular input ports - final int pixiPortIndex = inObjects.length - 1; - try { - final PythonProcessProvider pixiCommand = extractPythonCommandFromPixiPort(inObjects[pixiPortIndex]); - if (pixiCommand != null) { - LOGGER.debug("Using Python from Pixi environment"); - pythonCommand = pixiCommand; - // TODO: Consider if flow variable should take precedence over Pixi port - } else { - LOGGER.debug("Pixi port not connected, using configured Python command"); - pythonCommand = ExecutableSelectionUtils.getPythonCommand(m_settings.getExecutableSelection()); - } - } catch (InvalidSettingsException ex) { - throw new KNIMEException("Failed to extract Python command from environment port: " + ex.getMessage(), ex); - } + final PythonProcessProvider pythonCommandFromEnv = PythonEnvironmentPortObject.extractPythonCommand( + PortsConfigurationUtils.extractPythonEnvironmentPort(getPortsConfiguration(), inObjects)); + if (pythonCommandFromEnv != null) { + LOGGER.debug("Using Python from environment port"); + pythonCommand = pythonCommandFromEnv; + // TODO: Consider if flow variable should take precedence over environment port } else { pythonCommand = ExecutableSelectionUtils.getPythonCommand(m_settings.getExecutableSelection()); } @@ -231,24 +219,19 @@ protected PortObject[] execute(final PortObject[] inObjects, final ExecutionCont try (final var session = new PythonScriptingSession(pythonCommand, consoleConsumer, new ModelFileStoreHandlerSupplier())) { - // Filter out Pixi port from inObjects - it's not a data port - final PortObject[] dataPortObjects; - if (m_ports.hasPixiPort()) { - // Pixi port is at the end, so exclude it - dataPortObjects = Arrays.copyOf(inObjects, inObjects.length - 1); - } else { - dataPortObjects = inObjects; - } + // Filter out environment port from inObjects - it's not a data port + final PortObject[] dataPortObjects = + PortsConfigurationUtils.filterEnvironmentPort(getPortsConfiguration(), inObjects); - exec.setProgress(0.0, "Setting up inputs"); + restOfTheProgress.setProgress(0.0, "Setting up inputs"); session.setupIO(dataPortObjects, getAvailableFlowVariables(KNOWN_FLOW_VARIABLE_TYPES).values(), m_ports.getNumOutTables(), m_ports.getNumOutImages(), m_ports.getNumOutObjects(), m_hasView, - exec.createSubProgress(0.3)); - exec.setProgress(0.3, "Running script"); + restOfTheProgress.createSubProgress(0.3)); + restOfTheProgress.setProgress(0.3, "Running script"); runUserScript(session); - exec.setProgress(0.7, "Processing output"); + restOfTheProgress.setProgress(0.7, "Processing output"); final var outputs = session.getOutputs(exec.createSubExecutionContext(0.3)); final var flowVars = session.getFlowVariables(); addNewFlowVariables(flowVars); @@ -333,29 +316,7 @@ private void pushNewFlowVariable(final FlowVariable variable) { variable.getValue(variable.getVariableType())); } - /** - * Extract the Python command from a PythonEnvironmentPortObject. - * - * @param portObject the port object (may be null if optional port is not connected) - * @return the Python command, or null if the port is not connected or doesn't contain a valid Python executable - * @throws InvalidSettingsException if the Python command cannot be obtained from the environment - */ - private static PythonProcessProvider extractPythonCommandFromPixiPort(final PortObject portObject) - throws InvalidSettingsException { - try { - final PythonProcessProvider pythonCommand = PythonEnvironmentPortObject.extractPythonCommand(portObject); - if (pythonCommand != null) { - LOGGER.debug("Using Python from PythonEnvironmentPortObject: " + pythonCommand); - } - return pythonCommand; - } catch (NoClassDefFoundError e) { - // Environment port bundle not available - this should not happen if the port was added successfully - LOGGER.debug("Environment port class not available", e); - return null; - } catch (IOException e) { - throw new InvalidSettingsException("Failed to get Python command from environment: " + e.getMessage(), e); - } - } + /** Get the output view from the session if the node has a view and remember the path */ private void collectViewFromSession(final PythonScriptingSession session) throws IOException, KNIMEException { diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptPortsConfiguration.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptPortsConfiguration.java index b6db83322..79d07345f 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptPortsConfiguration.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptPortsConfiguration.java @@ -55,9 +55,8 @@ import org.knime.core.node.context.ports.PortsConfiguration; import org.knime.core.node.port.image.ImagePortObject; import org.knime.core.node.workflow.NodeContext; - -import org.knime.pixi.port.PythonEnvironmentPortObject; import org.knime.python2.port.PickledObjectFileStorePortObject; +import org.knime.python3.scripting.nodes.PortsConfigurationUtils; /** * Information about the configured ports of a Python scripting node. @@ -96,7 +95,7 @@ public final class PythonScriptPortsConfiguration { private final boolean m_hasView; - private final boolean m_hasEnvironmentPort; + private final boolean m_hasPythonEnvironmentPort; /** * Create a new {@link PythonScriptPortsConfiguration} from the given {@link PortsConfiguration}. @@ -111,7 +110,7 @@ static PythonScriptPortsConfiguration fromPortsConfiguration(final PortsConfigur final var numInTables = ArrayUtils.getLength(inPortsLocation.get(PORTGR_ID_INP_TABLE)); final var numInObjects = ArrayUtils.getLength(inPortsLocation.get(PORTGR_ID_INP_OBJECT)); // Check for environment port (accepts PythonEnvironmentPortObject) - final var hasEnvironmentPort = inPortsLocation.containsKey(PORTGR_ID_PYTHON_ENV) + final var hasEnvironmentPort = inPortsLocation.containsKey(PORTGR_ID_PYTHON_ENV) && ArrayUtils.getLength(inPortsLocation.get(PORTGR_ID_PYTHON_ENV)) > 0; final Map outPortsLocation = portsConfig.getOutputPortLocation(); @@ -151,17 +150,9 @@ static PythonScriptPortsConfiguration fromCurrentNodeContext() { numInTables++; } else if (PickledObjectFileStorePortObject.TYPE.equals(portType)) { numInObjects++; + } else if (PortsConfigurationUtils.isPythonEnvironmentPort(portType)) { + hasEnvironmentPort = true; } else { - // Check if it's a Python environment port - try { - if (PythonEnvironmentPortObject.TYPE.equals(portType) - || PythonEnvironmentPortObject.TYPE_OPTIONAL.equals(portType)) { - hasEnvironmentPort = true; - continue; // Don't count as error - } - } catch (NoClassDefFoundError e) { - // Python environment bundle not available, ignore - } throw new IllegalStateException("Unsupported input port configured. This is an implementation error."); } } @@ -187,14 +178,14 @@ static PythonScriptPortsConfiguration fromCurrentNodeContext() { } private PythonScriptPortsConfiguration(final int numInTables, final int numInObjects, final int numOutTables, - final int numOutImages, final int numOutObjects, final boolean hasView, final boolean hasEnvironmentPort) { + final int numOutImages, final int numOutObjects, final boolean hasView, final boolean hasPythonEnvironmentPort) { m_numInTables = numInTables; m_numInObjects = numInObjects; m_numOutTables = numOutTables; m_numOutImages = numOutImages; m_numOutObjects = numOutObjects; m_hasView = hasView; - m_hasEnvironmentPort = hasEnvironmentPort; + m_hasPythonEnvironmentPort = hasPythonEnvironmentPort; } /** @@ -241,25 +232,7 @@ public boolean hasView() { /** * @return if the node has a Python environment port (accepts PythonEnvironmentPortObject) */ - public boolean hasEnvironmentPort() { - return m_hasEnvironmentPort; - } - - /** - * @deprecated Use {@link #hasEnvironmentPort()} instead - * @return if the node has a Python environment port - */ - @Deprecated(since = "5.10", forRemoval = true) - public boolean hasPixiPort() { - return m_hasEnvironmentPort; - } - - /** - * @deprecated Use {@link #hasEnvironmentPort()} instead - * @return if the node has a Python environment port - */ - @Deprecated(since = "5.10", forRemoval = true) - public boolean hasPythonEnvPort() { - return m_hasEnvironmentPort; + public boolean hasPythonEnvironmentPort() { + return m_hasPythonEnvironmentPort; } } diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingInputOutputModelUtils.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingInputOutputModelUtils.java index c502e38c3..9290efdf0 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingInputOutputModelUtils.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingInputOutputModelUtils.java @@ -132,8 +132,8 @@ static List getInputObjects(final InputPortInfo[] inputPorts) for (int i = 0; i < inputPorts.length; i++) { final var type = inputPorts[i].portType(); - // Skip Pixi environment ports - they are not data ports - if (isPixiEnvironmentPort(type)) { + // Skip Python environment ports - they are not data ports + if (isPythonEnvironmentPort(type)) { LOGGER.debugWithFormat("Skipping environment port at index %d (type: %s) - not exposed to Python script", i, type.getName()); continue; } @@ -215,15 +215,15 @@ private static boolean isNoFlowVariablePort(final PortType portType) { return !portType.acceptsPortObjectClass(FlowVariablePortObject.class); } - private static boolean isPixiEnvironmentPort(final PortType portType) { + private static boolean isPythonEnvironmentPort(final PortType portType) { try { final boolean isPythonEnvPort = portType.acceptsPortObjectClass(PythonEnvironmentPortObject.class); LOGGER.debugWithFormat("Checking if port type '%s' is environment port: %s", portType.getName(), isPythonEnvPort); return isPythonEnvPort; } catch (NoClassDefFoundError e) { - // Pixi nodes bundle not available - LOGGER.debugWithFormat("Pixi nodes bundle not available for port type '%s'", portType.getName()); + // Python environment bundle not available + LOGGER.debugWithFormat("Python environment bundle not available for port type '%s'", portType.getName()); return false; } } diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingService.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingService.java index ca8d0f66d..245a3954f 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingService.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingService.java @@ -53,6 +53,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Arrays; import java.util.Collections; import java.util.Map; import java.util.Optional; @@ -67,7 +68,6 @@ import org.knime.core.data.filestore.internal.NotInWorkflowWriteFileStoreHandler; import org.knime.core.node.CanceledExecutionException; import org.knime.core.node.ExecutionMonitor; -import org.knime.core.node.InvalidSettingsException; import org.knime.core.node.NodeLogger; import org.knime.core.node.port.PortObject; import org.knime.core.node.workflow.FlowObjectStack; @@ -77,7 +77,6 @@ import org.knime.core.webui.node.dialog.scripting.CodeGenerationRequest; import org.knime.core.webui.node.dialog.scripting.InputOutputModel; import org.knime.core.webui.node.dialog.scripting.ScriptingService; -import org.knime.pixi.port.PixiPythonCommand; import org.knime.pixi.port.PythonEnvironmentPortObject; import org.knime.python3.processprovider.PythonProcessProvider; import org.knime.python3.scripting.nodes2.PythonScriptingService.ExecutableOption.ExecutableOptionType; @@ -253,26 +252,26 @@ private void startNewInteractiveSession() throws IOException, InterruptedExcepti final var workflowControl = getWorkflowControl(); final var inputData = workflowControl.getInputData(); - // Check if Pixi port is connected (it's the last port if present) + // Check if environment port is connected and extract Python command PythonProcessProvider pythonCommand = null; PortObject[] dataPortObjects = inputData; // By default, all inputs are data ports - if (m_ports.hasPixiPort() && inputData != null && inputData.length > 0) { - // The Pixi port is at the end, after all data ports - final int pixiPortIndex = inputData.length - 1; + if (m_ports.hasPythonEnvironmentPort() && inputData != null && inputData.length > 0) { try { - pythonCommand = extractPythonCommandFromPixiPort(inputData[pixiPortIndex]); + // Environment port is always the last port when present + final PortObject lastPort = inputData[inputData.length - 1]; + pythonCommand = PythonEnvironmentPortObject.extractPythonCommand(lastPort); if (pythonCommand != null) { - LOGGER.debug("Using Python environment from connected Pixi port for interactive session"); - // Filter out Pixi port from data ports - it's not a data port for setupIO - dataPortObjects = java.util.Arrays.copyOf(inputData, inputData.length - 1); + LOGGER.debug("Using Python environment from connected port for interactive session"); + // Filter out environment port from data ports (it's the last one) + dataPortObjects = Arrays.copyOf(inputData, inputData.length - 1); } } catch (Exception e) { - LOGGER.warn("Failed to extract Python command from Pixi port: " + e.getMessage()); + LOGGER.warn("Failed to extract Python command from environment port: " + e.getMessage()); } } - // Fall back to user selection if no Pixi port or extraction failed + // Fall back to user selection if no environment port or extraction failed if (pythonCommand == null) { pythonCommand = ExecutableSelectionUtils.getPythonCommand(getExecutableOption(m_executableSelection)); } @@ -530,41 +529,4 @@ public void close() { } } - /** - * Extract the Python command from a PythonEnvironmentPortObject. - * - * @param portObject the port object (may be null if optional port is not connected) - * @return the Python command, or null if the port is not connected or doesn't contain a valid Python executable - * @throws InvalidSettingsException if the Python executable path from the environment doesn't exist - */ - private static PythonProcessProvider extractPythonCommandFromPixiPort(final PortObject portObject) - throws InvalidSettingsException { - if (portObject == null) { - return null; - } - - try { - // Check if this is a PythonEnvironmentPortObject (new unified type) - if (portObject instanceof PythonEnvironmentPortObject) { - final PythonEnvironmentPortObject pythonEnvPort = (PythonEnvironmentPortObject)portObject; - try { - // PythonEnvironmentPortObject.getPythonCommand() returns org.knime.pixi.port.PythonCommand, - // but we need org.knime.python3.PythonCommand. Extract the pixi.toml path and create a new instance. - final Path pixiToml = pythonEnvPort.getPixiEnvironmentPath().resolve("pixi.toml"); - final PythonProcessProvider pythonCommand = new PixiPythonCommand(pixiToml); - LOGGER.debug("Using Python from PythonEnvironmentPortObject: " + pythonCommand); - return pythonCommand; - } catch (IOException e) { - throw new InvalidSettingsException("Failed to get Python command from environment: " + e.getMessage(), e); - } - } - - - return null; - } catch (NoClassDefFoundError e) { - // Environment port bundle not available - this should not happen if the port was added successfully - LOGGER.debug("Environment port class not available", e); - return null; - } - } } diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingSession.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingSession.java index 005d410b5..f70cb3913 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingSession.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingSession.java @@ -560,6 +560,10 @@ public boolean equals(final Object obj) { if (obj instanceof PythonCommandAdapter other) { return m_delegate.equals(other.m_delegate); } + // Add symmetric equality with delegate type for gateway queue cleanup + if (obj instanceof PythonProcessProvider) { + return m_delegate.equals(obj); + } return false; } diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/script/PythonScriptNodeFactory.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/script/PythonScriptNodeFactory.java index c44bb260f..5a025dff7 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/script/PythonScriptNodeFactory.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/script/PythonScriptNodeFactory.java @@ -127,7 +127,6 @@ protected Optional createPortsConfigBuilder() { // Add Python environment port try { - final Class pythonEnvClass = PythonEnvironmentPortObject.class; b.addOptionalInputPortGroup(PORTGR_ID_PYTHON_ENV, PythonEnvironmentPortObject.TYPE_OPTIONAL); LOGGER.debug("Successfully added Python environment port"); } catch (NoClassDefFoundError e) { diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/view/PythonViewNodeFactory.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/view/PythonViewNodeFactory.java index 76d51e785..908d2d1fa 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/view/PythonViewNodeFactory.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/view/PythonViewNodeFactory.java @@ -136,7 +136,6 @@ protected Optional createPortsConfigBuilder() { // Add Python environment port try { - final Class pythonEnvClass = PythonEnvironmentPortObject.class; b.addOptionalInputPortGroup(PORTGR_ID_PYTHON_ENV, PythonEnvironmentPortObject.TYPE_OPTIONAL); LOGGER.debug("Successfully added Python environment port"); } catch (NoClassDefFoundError e) { diff --git a/org.knime.python3/META-INF/MANIFEST.MF b/org.knime.python3/META-INF/MANIFEST.MF index a261deb8a..6f8eae53c 100644 --- a/org.knime.python3/META-INF/MANIFEST.MF +++ b/org.knime.python3/META-INF/MANIFEST.MF @@ -17,12 +17,12 @@ Require-Bundle: com.google.guava;bundle-version="[31.0.1,32.0.0)", org.knime.python3.types;bundle-version="[5.9.0,6.0.0)", org.eclipse.equinox.p2.core;bundle-version="[2.0.0,3.0.0)";visibility:=reexport, org.eclipse.equinox.p2.engine;bundle-version="[2.0.0,3.0.0)";visibility:=reexport, - org.knime.pixi.port;bundle-version="[5.11.0,6.0.0)" + org.knime.pixi.port;bundle-version="[5.11.0,6.0.0)";resolution:=optional, + org.knime.conda.envinstall;bundle-version="[5.10.0,6.0.0)";resolution:=optional, + org.knime.python3.processprovider;bundle-version="[5.11.0,6.0.0)";resolution:=optional Export-Package: org.knime.python3, org.knime.python3.utils Automatic-Module-Name: org.knime.python3 Eclipse-RegisterBuddy: org.knime.ext.py4j Eclipse-BundleShape: dir Bundle-Activator: org.knime.python3.Activator -Import-Package: org.knime.conda.envinstall.pixi, - org.knime.python3.processprovider diff --git a/org.knime.python3/src/main/java/org/knime/python3/PythonCommand.java b/org.knime.python3/src/main/java/org/knime/python3/PythonCommand.java index f320bf1d7..7e7ba96c7 100644 --- a/org.knime.python3/src/main/java/org/knime/python3/PythonCommand.java +++ b/org.knime.python3/src/main/java/org/knime/python3/PythonCommand.java @@ -64,14 +64,14 @@ * @author Benjamin Wilhelm, KNIME GmbH, Konstanz, Germany * @deprecated Use {@link PythonProcessProvider} instead. This interface is kept for backward compatibility. */ -@Deprecated(since = "5.10") +@Deprecated(since = "5.11", forRemoval = true) public interface PythonCommand extends PythonProcessProvider { /** * @return A {@link ProcessBuilder} that can be used to parameterize and start the Python process represented by * this command instance. */ - @Deprecated + @Deprecated(since = "5.11", forRemoval = true) @Override ProcessBuilder createProcessBuilder(); @@ -79,19 +79,19 @@ public interface PythonCommand extends PythonProcessProvider { * @return The path to the Python executable. Should only be used to gather information about the Python environment * without running the Python executable. Use {@link #createProcessBuilder()} to start Python processes. */ - @Deprecated + @Deprecated(since = "5.11", forRemoval = true) @Override Path getPythonExecutablePath(); - @Deprecated + @Deprecated(since = "5.11", forRemoval = true) @Override int hashCode(); - @Deprecated + @Deprecated(since = "5.11", forRemoval = true) @Override boolean equals(Object obj); - @Deprecated + @Deprecated(since = "5.11", forRemoval = true) @Override String toString(); } \ No newline at end of file From b2afed751d0e62067ee4021bea32474386a70f7a Mon Sep 17 00:00:00 2001 From: Marc Lehner Date: Thu, 12 Feb 2026 11:38:22 +0100 Subject: [PATCH 14/34] AP-25245: rename pythonprocessprovider to externalprocessprovider AP-25245 () --- .../META-INF/MANIFEST.MF | 2 +- .../org/knime/python3/arrow/TestUtils.java | 4 ++-- .../META-INF/MANIFEST.MF | 2 +- .../types/KnimeArrowExtensionTypesTest.java | 4 ++-- org.knime.python3.nodes/META-INF/MANIFEST.MF | 2 +- .../nodes/PythonExtensionPreferences.java | 6 +++--- .../nodes/PythonNodeGatewayFactory.java | 6 +++--- .../META-INF/MANIFEST.MF | 2 +- .../AbstractPythonScriptingNodeModel.java | 10 +++++----- .../prefs/BundledCondaEnvironmentConfig.java | 4 ++-- .../nodes/prefs/CondaEnvironmentConfig.java | 4 ++-- .../nodes/prefs/ManualEnvironmentConfig.java | 4 ++-- .../prefs/Python3ScriptingPreferences.java | 6 +++--- .../nodes/prefs/PythonEnvironmentConfig.java | 4 ++-- .../nodes/prefs/PythonKernelTester.java | 12 +++++------ .../nodes2/ExecutableSelectionUtils.java | 14 ++++++------- .../nodes2/PythonScriptNodeModel.java | 20 +++++++++---------- .../nodes2/PythonScriptingService.java | 6 +++--- .../nodes2/PythonScriptingSession.java | 20 +++++++++---------- .../META-INF/MANIFEST.MF | 2 +- .../Python3KernelBackendProxyTest.java | 2 +- .../META-INF/MANIFEST.MF | 2 +- .../python3/testing/Python3TestUtils.java | 4 ++-- org.knime.python3/META-INF/MANIFEST.MF | 2 +- .../knime/python3/AbstractPythonCommand.java | 7 ++++--- .../knime/python3/BundledPythonCommand.java | 4 ++-- .../org/knime/python3/CondaPythonCommand.java | 4 ++-- .../java/org/knime/python3/PythonCommand.java | 8 ++++---- .../knime/python3/PythonGatewayFactory.java | 12 +++++------ .../python3/QueuedPythonGatewayFactory.java | 14 ++++++------- .../knime/python3/SimplePythonCommand.java | 2 +- 31 files changed, 98 insertions(+), 97 deletions(-) diff --git a/org.knime.python3.arrow.tests/META-INF/MANIFEST.MF b/org.knime.python3.arrow.tests/META-INF/MANIFEST.MF index f71a116d7..47c7be9eb 100644 --- a/org.knime.python3.arrow.tests/META-INF/MANIFEST.MF +++ b/org.knime.python3.arrow.tests/META-INF/MANIFEST.MF @@ -14,4 +14,4 @@ Require-Bundle: org.junit;bundle-version="[4.13.0,5.0.0)", org.knime.core.data.columnar;bundle-version="[5.6.0,6.0.0)", org.knime.python3.testing;bundle-version="[5.6.0,6.0.0)" Automatic-Module-Name: org.knime.python3.arrow.tests -Import-Package: org.knime.python3.processprovider +Import-Package: org.knime.externalprocessprovider diff --git a/org.knime.python3.arrow.tests/src/test/java/org/knime/python3/arrow/TestUtils.java b/org.knime.python3.arrow.tests/src/test/java/org/knime/python3/arrow/TestUtils.java index 946513029..a5dfb57eb 100644 --- a/org.knime.python3.arrow.tests/src/test/java/org/knime/python3/arrow/TestUtils.java +++ b/org.knime.python3.arrow.tests/src/test/java/org/knime/python3/arrow/TestUtils.java @@ -53,9 +53,9 @@ import java.util.Collections; import java.util.List; +import org.knime.externalprocessprovider.ExternalProcessProvider; import org.knime.python3.DefaultPythonGateway; import org.knime.python3.Python3SourceDirectory; -import org.knime.python3.processprovider.PythonProcessProvider; import org.knime.python3.PythonDataSink; import org.knime.python3.PythonDataSource; import org.knime.python3.PythonEntryPoint; @@ -83,7 +83,7 @@ private TestUtils() { * @throws InterruptedException */ public static PythonGateway openPythonGateway() throws IOException, InterruptedException { - final PythonProcessProvider command = Python3TestUtils.getPythonCommand(); + final ExternalProcessProvider command = Python3TestUtils.getPythonCommand(); final String launcherPath = Paths.get(System.getProperty("user.dir"), "src/test/python", "tests_launcher.py").toString(); final PythonPath pythonPath = (new PythonPathBuilder()) // diff --git a/org.knime.python3.arrow.types.tests/META-INF/MANIFEST.MF b/org.knime.python3.arrow.types.tests/META-INF/MANIFEST.MF index 427a9be70..69c49056f 100644 --- a/org.knime.python3.arrow.types.tests/META-INF/MANIFEST.MF +++ b/org.knime.python3.arrow.types.tests/META-INF/MANIFEST.MF @@ -19,4 +19,4 @@ Require-Bundle: org.knime.core.table;bundle-version="[5.6.0,6.0.0)", org.knime.python3.testing;bundle-version="[5.6.0,6.0.0)" Automatic-Module-Name: org.knime.python3.arrow.types.tests Export-Package: org.knime.python3.arrow.types -Import-Package: org.knime.python3.processprovider +Import-Package: org.knime.externalprocessprovider diff --git a/org.knime.python3.arrow.types.tests/src/test/java/org/knime/python3/arrow/types/KnimeArrowExtensionTypesTest.java b/org.knime.python3.arrow.types.tests/src/test/java/org/knime/python3/arrow/types/KnimeArrowExtensionTypesTest.java index 6d8799345..90aed17c6 100644 --- a/org.knime.python3.arrow.types.tests/src/test/java/org/knime/python3/arrow/types/KnimeArrowExtensionTypesTest.java +++ b/org.knime.python3.arrow.types.tests/src/test/java/org/knime/python3/arrow/types/KnimeArrowExtensionTypesTest.java @@ -164,6 +164,7 @@ import org.knime.core.table.schema.DataSpecs.DataSpecWithTraits; import org.knime.core.table.schema.DefaultColumnarSchema; import org.knime.core.table.schema.traits.LogicalTypeTrait; +import org.knime.externalprocessprovider.ExternalProcessProvider; import org.knime.filehandling.core.connections.FSCategory; import org.knime.filehandling.core.connections.FSLocation; import org.knime.filehandling.core.data.location.FSLocationValue; @@ -172,7 +173,6 @@ import org.knime.filehandling.core.data.location.cell.SimpleFSLocationCellFactory; import org.knime.python3.DefaultPythonGateway; import org.knime.python3.Python3SourceDirectory; -import org.knime.python3.processprovider.PythonProcessProvider; import org.knime.python3.PythonDataSink; import org.knime.python3.PythonDataSource; import org.knime.python3.PythonEntryPoint; @@ -893,7 +893,7 @@ interface TriConsumer { private static PythonGateway openPythonGateway(final Class entryPointClass, final String launcherModule, final PythonModule... modules) throws IOException, InterruptedException { - final PythonProcessProvider command = Python3TestUtils.getPythonCommand(); + final ExternalProcessProvider command = Python3TestUtils.getPythonCommand(); final String launcherPath = Paths.get(System.getProperty("user.dir"), "src/test/python", launcherModule) .toString(); final PythonPathBuilder builder = PythonPath.builder()// diff --git a/org.knime.python3.nodes/META-INF/MANIFEST.MF b/org.knime.python3.nodes/META-INF/MANIFEST.MF index 11f917ee7..556be0697 100644 --- a/org.knime.python3.nodes/META-INF/MANIFEST.MF +++ b/org.knime.python3.nodes/META-INF/MANIFEST.MF @@ -37,7 +37,7 @@ Require-Bundle: org.knime.core;bundle-version="[5.10.0,6.0.0)", org.knime.workflowservices;bundle-version="[5.10.0,6.0.0)", org.apache.commons.lang3;bundle-version="[3.9.0,4.0.0)", org.knime.gateway.impl;bundle-version="[5.10.0,6.0.0)", - org.knime.python3.processprovider;bundle-version="[5.11.0,6.0.0)" + org.knime.externalprocessprovider;bundle-version="[5.11.0,6.0.0)" Automatic-Module-Name: org.knime.python3.nodes Eclipse-RegisterBuddy: org.knime.ext.py4j Eclipse-BundleShape: dir diff --git a/org.knime.python3.nodes/src/main/java/org/knime/python3/nodes/PythonExtensionPreferences.java b/org.knime.python3.nodes/src/main/java/org/knime/python3/nodes/PythonExtensionPreferences.java index e13dc9ce4..0505a75c0 100644 --- a/org.knime.python3.nodes/src/main/java/org/knime/python3/nodes/PythonExtensionPreferences.java +++ b/org.knime.python3.nodes/src/main/java/org/knime/python3/nodes/PythonExtensionPreferences.java @@ -61,9 +61,9 @@ import org.knime.conda.prefs.CondaPreferences; import org.knime.core.node.NodeLogger; +import org.knime.externalprocessprovider.ExternalProcessProvider; import org.knime.python3.CondaPythonCommand; import org.knime.python3.SimplePythonCommand; -import org.knime.python3.processprovider.PythonProcessProvider; import org.yaml.snakeyaml.Yaml; /** @@ -91,7 +91,7 @@ static Stream getPathsToCustomExtensions() { .map(Optional::get); } - static Optional getCustomPythonCommand(final String extensionId) { + static Optional getCustomPythonCommand(final String extensionId) { return loadConfigs()// .filter(e -> extensionId.equals(e.m_id))// .findFirst()// @@ -307,7 +307,7 @@ Optional getSrcPath() { } } - Optional getCommand() { + Optional getCommand() { if (m_condaEnvPath != null) { if (m_pythonExecutable != null) { LOGGER.warnWithFormat("Both conda_env_path and python_executable are provided for extension '%s'." diff --git a/org.knime.python3.nodes/src/main/java/org/knime/python3/nodes/PythonNodeGatewayFactory.java b/org.knime.python3.nodes/src/main/java/org/knime/python3/nodes/PythonNodeGatewayFactory.java index 49e1f68f3..2a614bdab 100644 --- a/org.knime.python3.nodes/src/main/java/org/knime/python3/nodes/PythonNodeGatewayFactory.java +++ b/org.knime.python3.nodes/src/main/java/org/knime/python3/nodes/PythonNodeGatewayFactory.java @@ -53,6 +53,7 @@ import java.util.Objects; import org.knime.conda.envbundling.environment.CondaEnvironmentRegistry; +import org.knime.externalprocessprovider.ExternalProcessProvider; import org.knime.python3.Activator; import org.knime.python3.BundledPythonCommand; import org.knime.python3.FreshPythonGatewayFactory; @@ -64,7 +65,6 @@ import org.knime.python3.PythonGatewayFactory.PythonGatewayDescription; import org.knime.python3.arrow.Python3ArrowSourceDirectory; import org.knime.python3.arrow.PythonArrowExtension; -import org.knime.python3.processprovider.PythonProcessProvider; import org.knime.python3.types.PythonValueFactoryModule; import org.knime.python3.types.PythonValueFactoryRegistry; import org.knime.python3.views.Python3ViewsSourceDirectory; @@ -141,12 +141,12 @@ public PythonGateway create() throws IOException, InterruptedE return gateway; } - private static PythonProcessProvider createCommand(final String extensionId, final String environmentName) { + private static ExternalProcessProvider createCommand(final String extensionId, final String environmentName) { return PythonExtensionPreferences.getCustomPythonCommand(extensionId)// .orElseGet(() -> getPythonCommandForEnvironment(environmentName)); } - private static PythonProcessProvider getPythonCommandForEnvironment(final String environmentName) { + private static ExternalProcessProvider getPythonCommandForEnvironment(final String environmentName) { final var environment = CondaEnvironmentRegistry.getEnvironment(environmentName); if (environment == null) { throw new IllegalStateException("Conda environment '" + environmentName + "' not found. " diff --git a/org.knime.python3.scripting.nodes/META-INF/MANIFEST.MF b/org.knime.python3.scripting.nodes/META-INF/MANIFEST.MF index 3203142de..d7598f311 100644 --- a/org.knime.python3.scripting.nodes/META-INF/MANIFEST.MF +++ b/org.knime.python3.scripting.nodes/META-INF/MANIFEST.MF @@ -44,4 +44,4 @@ Automatic-Module-Name: org.knime.python3.scripting.nodes Export-Package: org.knime.python3.scripting.nodes.prefs Eclipse-RegisterBuddy: org.knime.ext.py4j Eclipse-BundleShape: dir -Import-Package: org.knime.python3.processprovider +Import-Package: org.knime.externalprocessprovider diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/AbstractPythonScriptingNodeModel.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/AbstractPythonScriptingNodeModel.java index a6745fc6d..b67166b79 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/AbstractPythonScriptingNodeModel.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/AbstractPythonScriptingNodeModel.java @@ -81,6 +81,7 @@ import org.knime.core.util.PathUtils; import org.knime.core.util.asynclose.AsynchronousCloseableTracker; import org.knime.core.webui.node.view.NodeView; +import org.knime.externalprocessprovider.ExternalProcessProvider; import org.knime.pixi.port.PixiPythonCommand; import org.knime.pixi.port.PythonEnvironmentPortObject; import org.knime.python2.PythonCommand; @@ -103,7 +104,6 @@ import org.knime.python2.ports.OutputPort; import org.knime.python2.ports.PickledObjectOutputPort; import org.knime.python2.ports.Port; -import org.knime.python3.processprovider.PythonProcessProvider; import org.knime.python3.scripting.Python3KernelBackend; import org.knime.python3.scripting.nodes.prefs.Python3ScriptingPreferences; @@ -231,7 +231,7 @@ protected PortObject[] execute(final PortObject[] inObjects, final ExecutionCont final PythonCommand pythonCommandFromEnv; if (m_hasPythonEnvironmentPort) { try { - final PythonProcessProvider pythonProvider = PythonEnvironmentPortObject.extractPythonCommand( + final ExternalProcessProvider pythonProvider = PythonEnvironmentPortObject.extractPythonCommand( PortsConfigurationUtils.extractPythonEnvironmentPort(getPortsConfiguration(), inObjects)); pythonCommandFromEnv = pythonProvider != null ? new LegacyPythonCommand(pythonProvider) : null; } catch (NoClassDefFoundError e) { @@ -430,14 +430,14 @@ private void pushNewFlowVariable(final FlowVariable variable) { } /** - * Wraps a {@link PythonProcessProvider} into the legacy implementation for using it in a + * Wraps a {@link ExternalProcessProvider} into the legacy implementation for using it in a * {@link PythonKernelBackend}. */ private static final class LegacyPythonCommand implements PythonCommand { - private final PythonProcessProvider m_pythonCommand; + private final ExternalProcessProvider m_pythonCommand; - private LegacyPythonCommand(final PythonProcessProvider pythonCommand) { + private LegacyPythonCommand(final ExternalProcessProvider pythonCommand) { m_pythonCommand = pythonCommand; } diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/BundledCondaEnvironmentConfig.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/BundledCondaEnvironmentConfig.java index b46dfd8b7..bdbbd6533 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/BundledCondaEnvironmentConfig.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/BundledCondaEnvironmentConfig.java @@ -51,8 +51,8 @@ import org.knime.conda.envbundling.environment.CondaEnvironmentRegistry; import org.knime.core.node.NodeLogger; import org.knime.core.node.defaultnodesettings.SettingsModelString; +import org.knime.externalprocessprovider.ExternalProcessProvider; import org.knime.python3.BundledPythonCommand; -import org.knime.python3.processprovider.PythonProcessProvider; /** * The BundledCondaEnvironmentConfig is a PythonEnvironmentConfig that points to a bundled conda environment which is @@ -107,7 +107,7 @@ public boolean isAvailable() { } @Override - public PythonProcessProvider getPythonCommand() { + public ExternalProcessProvider getPythonCommand() { final var condaEnv = CondaEnvironmentRegistry.getEnvironment(m_bundledCondaEnvironment.getStringValue()); if (condaEnv == null) { final var errorMsg = "You have selected the 'Bundled' option in KNIME Python preferences, " diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/CondaEnvironmentConfig.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/CondaEnvironmentConfig.java index 6a68da195..bd92f7504 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/CondaEnvironmentConfig.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/CondaEnvironmentConfig.java @@ -56,8 +56,8 @@ import org.knime.conda.prefs.CondaPreferences; import org.knime.core.node.NodeLogger; import org.knime.core.node.defaultnodesettings.SettingsModelString; +import org.knime.externalprocessprovider.ExternalProcessProvider; import org.knime.python3.CondaPythonCommand; -import org.knime.python3.processprovider.PythonProcessProvider; /** * Copied and modified from org.knime.python2.config. @@ -113,7 +113,7 @@ public ObservableValue getAvailableEnvironments() } @Override - public PythonProcessProvider getPythonCommand() { + public ExternalProcessProvider getPythonCommand() { return new CondaPythonCommand(CondaPreferences.getCondaInstallationDirectory(), m_environmentDirectory.getStringValue()); } diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/ManualEnvironmentConfig.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/ManualEnvironmentConfig.java index 97a90f140..e814dc2cc 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/ManualEnvironmentConfig.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/ManualEnvironmentConfig.java @@ -49,7 +49,7 @@ package org.knime.python3.scripting.nodes.prefs; import org.knime.core.node.defaultnodesettings.SettingsModelString; -import org.knime.python3.processprovider.PythonProcessProvider; +import org.knime.externalprocessprovider.ExternalProcessProvider; import org.knime.python3.SimplePythonCommand; /** @@ -79,7 +79,7 @@ public SettingsModelString getExecutablePath() { } @Override - public PythonProcessProvider getPythonCommand() { + public ExternalProcessProvider getPythonCommand() { return new SimplePythonCommand(m_pythonPath.getStringValue()); } diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/Python3ScriptingPreferences.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/Python3ScriptingPreferences.java index 8cf6524df..153de151e 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/Python3ScriptingPreferences.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/Python3ScriptingPreferences.java @@ -52,8 +52,8 @@ import org.eclipse.core.runtime.preferences.InstanceScope; import org.knime.conda.CondaEnvironmentIdentifier; import org.knime.conda.prefs.CondaPreferences; +import org.knime.externalprocessprovider.ExternalProcessProvider; import org.knime.python3.BundledPythonCommand; -import org.knime.python3.processprovider.PythonProcessProvider; /** * Convenience front-end of the preference-based configuration of the Python integration. @@ -110,7 +110,7 @@ public static PythonEnvironmentType getEnvironmentTypePreference() { /** * @return The currently selected default Python command. */ - public static PythonProcessProvider getPythonCommandPreference() { + public static ExternalProcessProvider getPythonCommandPreference() { final var envType = getEnvironmentTypePreference(); PythonEnvironmentsConfig environmentsConfig; @@ -124,7 +124,7 @@ public static PythonProcessProvider getPythonCommandPreference() { } /** - * @return The {@link PythonProcessProvider} for the installed bundled environment. + * @return The {@link ExternalProcessProvider} for the installed bundled environment. */ public static BundledPythonCommand getBundledPythonCommand() { final var pythonCommand = getBundledCondaEnvironmentConfig().getPythonCommand(); diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/PythonEnvironmentConfig.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/PythonEnvironmentConfig.java index d7dbb0321..ef945ed64 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/PythonEnvironmentConfig.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/PythonEnvironmentConfig.java @@ -50,7 +50,7 @@ import org.knime.core.node.defaultnodesettings.SettingsModelBoolean; import org.knime.core.node.defaultnodesettings.SettingsModelString; -import org.knime.python3.processprovider.PythonProcessProvider; +import org.knime.externalprocessprovider.ExternalProcessProvider; /** * Copied from org.knime.python2.config. @@ -63,7 +63,7 @@ interface PythonEnvironmentConfig extends PythonConfig { /** * @return The command that executes Python in the Python environment configured by this instance. */ - PythonProcessProvider getPythonCommand(); + ExternalProcessProvider getPythonCommand(); /** * @return If the Python environment configured by this instance is currently the default environment. Not meant for diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/PythonKernelTester.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/PythonKernelTester.java index a002f0a94..e1276b936 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/PythonKernelTester.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/PythonKernelTester.java @@ -66,7 +66,7 @@ import org.knime.core.node.NodeLogger; import org.knime.core.util.FileUtil; import org.knime.core.util.Pair; -import org.knime.python3.processprovider.PythonProcessProvider; +import org.knime.externalprocessprovider.ExternalProcessProvider; /** * Copied from org.knime.python2. @@ -94,7 +94,7 @@ final class PythonKernelTester { * Caches previous test results. Mapping from the Python command that was tested to a list of additional required * modules that were tested along the command and the test results of these combinations of command and modules. */ - private static final Map, PythonKernelTestResult>>> TEST_RESULTS = + private static final Map, PythonKernelTestResult>>> TEST_RESULTS = new ConcurrentHashMap<>(); private static String getPythonKernelTesterPath() throws IOException { @@ -119,7 +119,7 @@ private PythonKernelTester() { * @param force Force the test to be rerun again even if the same configuration was successfully tested before. * @return The results of the installation test. */ - public static PythonKernelTestResult testPython3Installation(final PythonProcessProvider python3Command, + public static PythonKernelTestResult testPython3Installation(final ExternalProcessProvider python3Command, final Collection additionalRequiredModules, final boolean force) { return testPythonInstallation(python3Command, PYTHON_MAJOR_VERSION_3, PYTHON_MINIMUM_VERSION_3, additionalRequiredModules, Collections.emptyList(), force); @@ -128,7 +128,7 @@ public static PythonKernelTestResult testPython3Installation(final PythonProcess /** * @param minimumVersion May be {@code null} in the case where no minimum version is required. */ - private static synchronized PythonKernelTestResult testPythonInstallation(final PythonProcessProvider pythonCommand, + private static synchronized PythonKernelTestResult testPythonInstallation(final ExternalProcessProvider pythonCommand, final String majorVersion, final String minimumVersion, final Collection additionalRequiredModules, final Collection additionalOptionalModules, final boolean force) { @@ -189,7 +189,7 @@ private static synchronized PythonKernelTestResult testPythonInstallation(final return testResults; } - private static PythonKernelTestResult getPreviousTestResultsIfApplicable(final PythonProcessProvider pythonCommand, + private static PythonKernelTestResult getPreviousTestResultsIfApplicable(final ExternalProcessProvider pythonCommand, final Set additionalRequiredModules) { // If a previous, appropriate Python test already succeeded, we will not have to run it again and return the // old results here (except if we're forced to). @@ -209,7 +209,7 @@ private static PythonKernelTestResult getPreviousTestResultsIfApplicable(final P return null; } - private static Process runPythonKernelTester(final PythonProcessProvider pythonCommand, final String majorVersion, + private static Process runPythonKernelTester(final ExternalProcessProvider pythonCommand, final String majorVersion, final String minimumVersion, final Collection additionalRequiredModules, final Collection additionalOptionalModules, final StringBuilder testLogger) throws IOException { diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/ExecutableSelectionUtils.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/ExecutableSelectionUtils.java index b60fa4a18..f5461f666 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/ExecutableSelectionUtils.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/ExecutableSelectionUtils.java @@ -59,8 +59,8 @@ import org.knime.conda.CondaEnvironmentPropagation.CondaEnvironmentType; import org.knime.conda.prefs.CondaPreferences; import org.knime.core.node.workflow.FlowObjectStack; +import org.knime.externalprocessprovider.ExternalProcessProvider; import org.knime.python3.CondaPythonCommand; -import org.knime.python3.processprovider.PythonProcessProvider; import org.knime.python3.SimplePythonCommand; import org.knime.python3.scripting.nodes.prefs.Python3ScriptingPreferences; import org.knime.python3.scripting.nodes2.PythonScriptingService.ExecutableOption; @@ -83,7 +83,7 @@ static Map getExecutableOptions(final FlowObjectStack } /** Get the PythonCommand from the selected option */ - static PythonProcessProvider getPythonCommand(final ExecutableOption option) { + static ExternalProcessProvider getPythonCommand(final ExecutableOption option) { switch (option.type) { case CONDA_ENV_VAR: return commandForConda(option.condaEnvDir); @@ -106,7 +106,7 @@ static PythonProcessProvider getPythonCommand(final ExecutableOption option) { } /** Get the PythonCommand from the given settings String */ - static PythonProcessProvider getPythonCommand(final String commandString) { + static ExternalProcessProvider getPythonCommand(final String commandString) { if (commandString == null || EXEC_SELECTION_PREF_ID.equals(commandString)) { // Nothing configured -> Use preferences return commandForPreferences(); @@ -126,7 +126,7 @@ static boolean isPathToCondaEnv(final String commandString) { private static ExecutableOption getPreferenceOption() { return new ExecutableOption(getPreferenceOptionType(), EXEC_SELECTION_PREF_ID, - commandForPreferences().getPythonExecutablePath().toString(), null, null); + commandForPreferences().getExecutablePath().toString(), null, null); } private static ExecutableOptionType getPreferenceOptionType() { @@ -161,15 +161,15 @@ private static Stream getCondaFlowVariableOptions(final FlowOb } } - private static PythonProcessProvider commandForConda(final String condaEnvDir) { + private static ExternalProcessProvider commandForConda(final String condaEnvDir) { return new CondaPythonCommand(CondaPreferences.getCondaInstallationDirectory(), condaEnvDir); } - private static PythonProcessProvider commandForString(final String pythonExecutable) { + private static ExternalProcessProvider commandForString(final String pythonExecutable) { return new SimplePythonCommand(pythonExecutable); } - private static PythonProcessProvider commandForPreferences() { + private static ExternalProcessProvider commandForPreferences() { return Python3ScriptingPreferences.getPythonCommandPreference(); } } diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptNodeModel.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptNodeModel.java index 62a4ca640..962367bf5 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptNodeModel.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptNodeModel.java @@ -86,9 +86,9 @@ import org.knime.core.node.workflow.VariableTypeRegistry; import org.knime.core.util.asynclose.AsynchronousCloseableTracker; import org.knime.core.webui.node.dialog.scripting.ScriptingService.ConsoleText; +import org.knime.externalprocessprovider.ExternalProcessProvider; import org.knime.pixi.port.PythonEnvironmentPortObject; import org.knime.python3.PythonProcessTerminatedException; -import org.knime.python3.processprovider.PythonProcessProvider; import org.knime.python3.scripting.nodes.PortsConfigurationUtils; import org.knime.python3.scripting.nodes2.ConsoleOutputUtils.ConsoleOutputStorage; import org.knime.python3.scripting.nodes2.PythonScriptingSession.ExecutionInfo; @@ -191,20 +191,20 @@ protected PortObject[] execute(final PortObject[] inObjects, final ExecutionCont throws IOException, InterruptedException, CanceledExecutionException, KNIMEException { // Install Python environment early to avoid timeout issues during gateway connection - final ExecutionMonitor restOfTheProgress; + final ExecutionMonitor remainingProgress; if (m_ports.hasPythonEnvironmentPort()) { //PortsConfigurationUtils.installPythonEnvironmentIfPresent(getPortsConfiguration(), inObjects, // exec.createSubProgress(0.2)); PythonEnvironmentPortObject.installPythonEnvironmentWithProgress(getPortsConfiguration(), inObjects, exec.createSubProgress(0.2)); - restOfTheProgress = exec.createSubProgress(0.8); + remainingProgress = exec.createSubProgress(0.8); } else { - restOfTheProgress = exec; + remainingProgress = exec; } // Extract Python command from environment port if connected, otherwise use configured command - final PythonProcessProvider pythonCommand; - final PythonProcessProvider pythonCommandFromEnv = PythonEnvironmentPortObject.extractPythonCommand( + final ExternalProcessProvider pythonCommand; + final ExternalProcessProvider pythonCommandFromEnv = PythonEnvironmentPortObject.extractPythonCommand( PortsConfigurationUtils.extractPythonEnvironmentPort(getPortsConfiguration(), inObjects)); if (pythonCommandFromEnv != null) { LOGGER.debug("Using Python from environment port"); @@ -223,15 +223,15 @@ protected PortObject[] execute(final PortObject[] inObjects, final ExecutionCont final PortObject[] dataPortObjects = PortsConfigurationUtils.filterEnvironmentPort(getPortsConfiguration(), inObjects); - restOfTheProgress.setProgress(0.0, "Setting up inputs"); + remainingProgress.setProgress(0.0, "Setting up inputs"); session.setupIO(dataPortObjects, getAvailableFlowVariables(KNOWN_FLOW_VARIABLE_TYPES).values(), m_ports.getNumOutTables(), m_ports.getNumOutImages(), m_ports.getNumOutObjects(), m_hasView, - restOfTheProgress.createSubProgress(0.3)); - restOfTheProgress.setProgress(0.3, "Running script"); + remainingProgress.createSubProgress(0.3)); + remainingProgress.setProgress(0.3, "Running script"); runUserScript(session); - restOfTheProgress.setProgress(0.7, "Processing output"); + remainingProgress.setProgress(0.7, "Processing output"); final var outputs = session.getOutputs(exec.createSubExecutionContext(0.3)); final var flowVars = session.getFlowVariables(); addNewFlowVariables(flowVars); diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingService.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingService.java index 245a3954f..9155d1337 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingService.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingService.java @@ -77,8 +77,8 @@ import org.knime.core.webui.node.dialog.scripting.CodeGenerationRequest; import org.knime.core.webui.node.dialog.scripting.InputOutputModel; import org.knime.core.webui.node.dialog.scripting.ScriptingService; +import org.knime.externalprocessprovider.ExternalProcessProvider; import org.knime.pixi.port.PythonEnvironmentPortObject; -import org.knime.python3.processprovider.PythonProcessProvider; import org.knime.python3.scripting.nodes2.PythonScriptingService.ExecutableOption.ExecutableOptionType; import org.knime.python3.scripting.nodes2.PythonScriptingSession.ExecutionInfo; import org.knime.python3.scripting.nodes2.PythonScriptingSession.ExecutionStatus; @@ -253,7 +253,7 @@ private void startNewInteractiveSession() throws IOException, InterruptedExcepti final var inputData = workflowControl.getInputData(); // Check if environment port is connected and extract Python command - PythonProcessProvider pythonCommand = null; + ExternalProcessProvider pythonCommand = null; PortObject[] dataPortObjects = inputData; // By default, all inputs are data ports if (m_ports.hasPythonEnvironmentPort() && inputData != null && inputData.length > 0) { @@ -393,7 +393,7 @@ public String getLanguageServerConfig(final String executableSelection) { var executableOption = getExecutableOption(executableSelection); String executablePath = null; if (executableOption.type != ExecutableOptionType.MISSING_VAR) { - executablePath = ExecutableSelectionUtils.getPythonCommand(executableOption).getPythonExecutablePath() + executablePath = ExecutableSelectionUtils.getPythonCommand(executableOption).getExecutablePath() .toAbsolutePath().toString(); } var extraPaths = PythonScriptingSession.getExtraPythonPaths().stream() // diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingSession.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingSession.java index f70cb3913..538b88f1a 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingSession.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingSession.java @@ -85,6 +85,7 @@ import org.knime.core.util.asynclose.AsynchronousCloseable; import org.knime.core.util.pathresolve.ResolverUtil; import org.knime.core.webui.node.dialog.scripting.ScriptingService.ConsoleText; +import org.knime.externalprocessprovider.ExternalProcessProvider; import org.knime.python3.Activator; import org.knime.python3.Python3SourceDirectory; import org.knime.python3.PythonEntryPointUtils; @@ -99,7 +100,6 @@ import org.knime.python3.arrow.PythonArrowDataUtils; import org.knime.python3.arrow.PythonArrowExtension; import org.knime.python3.arrow.PythonArrowTableConverter; -import org.knime.python3.processprovider.PythonProcessProvider; import org.knime.python3.types.PythonValueFactoryModule; import org.knime.python3.types.PythonValueFactoryRegistry; import org.knime.python3.utils.FlowVariableUtils; @@ -155,7 +155,7 @@ final class PythonScriptingSession implements AsynchronousCloseable private int m_numOutObjects; - PythonScriptingSession(final PythonProcessProvider pythonCommand, final Consumer consoleTextConsumer, + PythonScriptingSession(final ExternalProcessProvider pythonCommand, final Consumer consoleTextConsumer, final FileStoreHandlerSupplier fileStoreHandlerSupplier) throws IOException, InterruptedException { m_consoleTextConsumer = consoleTextConsumer; m_fileStoreHandlerSupplier = fileStoreHandlerSupplier; @@ -418,9 +418,9 @@ Optional getOutputView() throws IOException { } } - private static PythonGateway createGateway(final PythonProcessProvider pythonCommand) + private static PythonGateway createGateway(final ExternalProcessProvider pythonCommand) throws IOException, InterruptedException { - if (pythonCommand.getPythonExecutablePath() + if (pythonCommand.getExecutablePath() .startsWith(CondaEnvironmentIdentifier.NOT_EXECUTED_PATH_PLACEHOLDER)) { throw new IOException(CondaEnvironmentIdentifier.NOT_EXECUTED_PATH_PLACEHOLDER); } @@ -530,10 +530,10 @@ public interface FileStoreHandlerSupplier extends AutoCloseable { * Adapter that wraps org.knime.pixi.port.PythonCommand to implement org.knime.python3.PythonCommand. * This is needed because PythonGatewayDescription.builder() requires the public API type. */ - private static final class PythonCommandAdapter implements PythonProcessProvider { - private final PythonProcessProvider m_delegate; + private static final class PythonCommandAdapter implements ExternalProcessProvider { + private final ExternalProcessProvider m_delegate; - PythonCommandAdapter(final PythonProcessProvider delegate) { + PythonCommandAdapter(final ExternalProcessProvider delegate) { m_delegate = delegate; } @@ -543,8 +543,8 @@ public ProcessBuilder createProcessBuilder() { } @Override - public Path getPythonExecutablePath() { - return m_delegate.getPythonExecutablePath(); + public Path getExecutablePath() { + return m_delegate.getExecutablePath(); } @Override @@ -561,7 +561,7 @@ public boolean equals(final Object obj) { return m_delegate.equals(other.m_delegate); } // Add symmetric equality with delegate type for gateway queue cleanup - if (obj instanceof PythonProcessProvider) { + if (obj instanceof ExternalProcessProvider) { return m_delegate.equals(obj); } return false; diff --git a/org.knime.python3.scripting.tests/META-INF/MANIFEST.MF b/org.knime.python3.scripting.tests/META-INF/MANIFEST.MF index b064208dd..b9d53874f 100644 --- a/org.knime.python3.scripting.tests/META-INF/MANIFEST.MF +++ b/org.knime.python3.scripting.tests/META-INF/MANIFEST.MF @@ -12,4 +12,4 @@ Export-Package: org.knime.python3.scripting Require-Bundle: org.junit;bundle-version="[4.13.0,5.0.0)", org.apache.arrow.memory-core;bundle-version="[18.1.0,19.0.0)", org.knime.python3.testing;bundle-version="[5.6.0,6.0.0)" -Import-Package: org.knime.python3.processprovider +Import-Package: org.knime.externalprocessprovider diff --git a/org.knime.python3.scripting.tests/src/test/java/org/knime/python3/scripting/Python3KernelBackendProxyTest.java b/org.knime.python3.scripting.tests/src/test/java/org/knime/python3/scripting/Python3KernelBackendProxyTest.java index 7b4e54b2d..aa247c6d1 100644 --- a/org.knime.python3.scripting.tests/src/test/java/org/knime/python3/scripting/Python3KernelBackendProxyTest.java +++ b/org.knime.python3.scripting.tests/src/test/java/org/knime/python3/scripting/Python3KernelBackendProxyTest.java @@ -81,6 +81,7 @@ import org.knime.core.columnar.data.StringData.StringWriteData; import org.knime.core.table.schema.ColumnarSchema; import org.knime.core.util.FileUtil; +import org.knime.externalprocessprovider.ExternalProcessProvider; import org.knime.python2.kernel.PythonKernelBackendUtils; import org.knime.python3.DefaultPythonGateway; import org.knime.python3.Python3SourceDirectory; @@ -93,7 +94,6 @@ import org.knime.python3.arrow.PythonArrowDataSource; import org.knime.python3.arrow.PythonArrowDataUtils; import org.knime.python3.arrow.PythonArrowExtension; -import org.knime.python3.processprovider.PythonProcessProvider; import org.knime.python3.testing.Python3ArrowTestUtils; import org.knime.python3.testing.Python3TestUtils; import org.knime.python3.views.Python3ViewsSourceDirectory; diff --git a/org.knime.python3.testing/META-INF/MANIFEST.MF b/org.knime.python3.testing/META-INF/MANIFEST.MF index 7e1e42910..db060a986 100644 --- a/org.knime.python3.testing/META-INF/MANIFEST.MF +++ b/org.knime.python3.testing/META-INF/MANIFEST.MF @@ -11,4 +11,4 @@ Require-Bundle: org.apache.commons.lang3;bundle-version="[3.9.0,4.0.0)", org.knime.core.columnar;bundle-version="[5.6.0,6.0.0)" Automatic-Module-Name: org.knime.python3.testing Export-Package: org.knime.python3.testing -Import-Package: org.knime.python3.processprovider +Import-Package: org.knime.externalprocessprovider diff --git a/org.knime.python3.testing/src/main/java/org/knime/python3/testing/Python3TestUtils.java b/org.knime.python3.testing/src/main/java/org/knime/python3/testing/Python3TestUtils.java index 85567ed16..8f0fb5346 100644 --- a/org.knime.python3.testing/src/main/java/org/knime/python3/testing/Python3TestUtils.java +++ b/org.knime.python3.testing/src/main/java/org/knime/python3/testing/Python3TestUtils.java @@ -51,8 +51,8 @@ import java.io.IOException; import org.apache.commons.lang3.SystemUtils; +import org.knime.externalprocessprovider.ExternalProcessProvider; import org.knime.python3.SimplePythonCommand; -import org.knime.python3.processprovider.PythonProcessProvider; /** * Contains utilities shared by multiple test fragments in knime-python. @@ -74,7 +74,7 @@ private Python3TestUtils() { * @return The command created from environment variable. * @throws IOException If none of the environment variables is set. */ - public static PythonProcessProvider getPythonCommand() throws IOException { + public static ExternalProcessProvider getPythonCommand() throws IOException { final String osSuffix; if (SystemUtils.IS_OS_LINUX) { osSuffix = "LINUX"; diff --git a/org.knime.python3/META-INF/MANIFEST.MF b/org.knime.python3/META-INF/MANIFEST.MF index 6f8eae53c..12e8981c1 100644 --- a/org.knime.python3/META-INF/MANIFEST.MF +++ b/org.knime.python3/META-INF/MANIFEST.MF @@ -19,7 +19,7 @@ Require-Bundle: com.google.guava;bundle-version="[31.0.1,32.0.0)", org.eclipse.equinox.p2.engine;bundle-version="[2.0.0,3.0.0)";visibility:=reexport, org.knime.pixi.port;bundle-version="[5.11.0,6.0.0)";resolution:=optional, org.knime.conda.envinstall;bundle-version="[5.10.0,6.0.0)";resolution:=optional, - org.knime.python3.processprovider;bundle-version="[5.11.0,6.0.0)";resolution:=optional + org.knime.externalprocessprovider;bundle-version="[5.11.0,6.0.0)";resolution:=optional Export-Package: org.knime.python3, org.knime.python3.utils Automatic-Module-Name: org.knime.python3 diff --git a/org.knime.python3/src/main/java/org/knime/python3/AbstractPythonCommand.java b/org.knime.python3/src/main/java/org/knime/python3/AbstractPythonCommand.java index 342073c8f..e03a9793c 100644 --- a/org.knime.python3/src/main/java/org/knime/python3/AbstractPythonCommand.java +++ b/org.knime.python3/src/main/java/org/knime/python3/AbstractPythonCommand.java @@ -53,14 +53,15 @@ import java.util.Collections; import java.util.List; import java.util.Objects; -import org.knime.python3.processprovider.PythonProcessProvider; + +import org.knime.externalprocessprovider.ExternalProcessProvider; /** * @author Marcel Wiedenmann, KNIME GmbH, Konstanz, Germany * @author Christian Dietz, KNIME GmbH, Konstanz, Germany * @author Benjamin Wilhelm, KNIME GmbH, Konstanz, Germany */ -abstract class AbstractPythonCommand implements PythonProcessProvider { +abstract class AbstractPythonCommand implements ExternalProcessProvider { /** The Python command and possible arguments */ protected final List m_command; @@ -76,7 +77,7 @@ public ProcessBuilder createProcessBuilder() { } @Override - public Path getPythonExecutablePath() { + public Path getExecutablePath() { return Path.of(m_command.get(0)); } diff --git a/org.knime.python3/src/main/java/org/knime/python3/BundledPythonCommand.java b/org.knime.python3/src/main/java/org/knime/python3/BundledPythonCommand.java index d5a084f54..50962f85a 100644 --- a/org.knime.python3/src/main/java/org/knime/python3/BundledPythonCommand.java +++ b/org.knime.python3/src/main/java/org/knime/python3/BundledPythonCommand.java @@ -51,7 +51,7 @@ import org.knime.conda.CondaEnvironmentDirectory; /** - * Conda-specific implementation of {@link PythonProcessProvider} that works with bundled Python environments. Allows to build + * Conda-specific implementation of {@link ExternalProcessProvider} that works with bundled Python environments. Allows to build * Python processes for a given Conda environment. Takes care of resolving PATH-related issues on Windows. * * @author Marcel Wiedenmann, KNIME GmbH, Konstanz, Germany @@ -60,7 +60,7 @@ public final class BundledPythonCommand extends AbstractCondaPythonCommand { /** - * Constructs a {@link PythonProcessProvider} that describes a Python process that is run in the bundled Conda environment + * Constructs a {@link ExternalProcessProvider} that describes a Python process that is run in the bundled Conda environment * identified by the given Conda environment directory. The validity of the given argument is not tested. * * @param environmentDirectoryPath The path to the directory of the bundled Conda environment. diff --git a/org.knime.python3/src/main/java/org/knime/python3/CondaPythonCommand.java b/org.knime.python3/src/main/java/org/knime/python3/CondaPythonCommand.java index 04722b5fe..1381006d8 100644 --- a/org.knime.python3/src/main/java/org/knime/python3/CondaPythonCommand.java +++ b/org.knime.python3/src/main/java/org/knime/python3/CondaPythonCommand.java @@ -51,7 +51,7 @@ import org.knime.conda.CondaEnvironmentDirectory; /** - * Conda-specific implementation of {@link PythonProcessProvider}. Allows to build Python processes for a given Conda + * Conda-specific implementation of {@link ExternalProcessProvider}. Allows to build Python processes for a given Conda * installation and environment. Takes care of resolving PATH-related issues on Windows. * * @author Marcel Wiedenmann, KNIME GmbH, Konstanz, Germany @@ -60,7 +60,7 @@ public final class CondaPythonCommand extends AbstractCondaPythonCommand { /** - * Constructs a {@link PythonProcessProvider} that describes a Python process that is run in the Conda environment + * Constructs a {@link ExternalProcessProvider} that describes a Python process that is run in the Conda environment * identified by the given Conda installation directory and the given Conda environment directory.
* The validity of the given arguments is not tested. * diff --git a/org.knime.python3/src/main/java/org/knime/python3/PythonCommand.java b/org.knime.python3/src/main/java/org/knime/python3/PythonCommand.java index 7e7ba96c7..e4259c334 100644 --- a/org.knime.python3/src/main/java/org/knime/python3/PythonCommand.java +++ b/org.knime.python3/src/main/java/org/knime/python3/PythonCommand.java @@ -50,7 +50,7 @@ import java.nio.file.Path; -import org.knime.python3.processprovider.PythonProcessProvider; +import org.knime.externalprocessprovider.ExternalProcessProvider; /** * Describes an external Python process. The process can be started via the {@link ProcessBuilder} returned by @@ -62,10 +62,10 @@ * @author Marcel Wiedenmann, KNIME GmbH, Konstanz, Germany * @author Christian Dietz, KNIME GmbH, Konstanz, Germany * @author Benjamin Wilhelm, KNIME GmbH, Konstanz, Germany - * @deprecated Use {@link PythonProcessProvider} instead. This interface is kept for backward compatibility. + * @deprecated Use {@link ExternalProcessProvider} instead. This interface is kept for backward compatibility. */ @Deprecated(since = "5.11", forRemoval = true) -public interface PythonCommand extends PythonProcessProvider { +public interface PythonCommand extends ExternalProcessProvider { /** * @return A {@link ProcessBuilder} that can be used to parameterize and start the Python process represented by @@ -81,7 +81,7 @@ public interface PythonCommand extends PythonProcessProvider { */ @Deprecated(since = "5.11", forRemoval = true) @Override - Path getPythonExecutablePath(); + Path getExecutablePath(); @Deprecated(since = "5.11", forRemoval = true) @Override diff --git a/org.knime.python3/src/main/java/org/knime/python3/PythonGatewayFactory.java b/org.knime.python3/src/main/java/org/knime/python3/PythonGatewayFactory.java index 4a11eccf8..576cb765b 100644 --- a/org.knime.python3/src/main/java/org/knime/python3/PythonGatewayFactory.java +++ b/org.knime.python3/src/main/java/org/knime/python3/PythonGatewayFactory.java @@ -54,8 +54,8 @@ import java.util.List; import java.util.Objects; +import org.knime.externalprocessprovider.ExternalProcessProvider; import org.knime.python3.PythonPath.PythonPathBuilder; -import org.knime.python3.processprovider.PythonProcessProvider; /** * An object that provides {@link PythonGateway PythonGateways} for a particular combination of environment, launcher @@ -106,7 +106,7 @@ interface EntryPointCustomizer { */ final class PythonGatewayDescription { - private final PythonProcessProvider m_command; + private final ExternalProcessProvider m_command; private final Path m_launcherPath; @@ -131,7 +131,7 @@ Path getLauncherPath() { return m_launcherPath; } - PythonProcessProvider getCommand() { + ExternalProcessProvider getCommand() { return m_command; } @@ -189,7 +189,7 @@ public boolean equals(final Object obj) { * @param entryPointClass the type of entry point * @return a builder for a PythonGatewayDescription */ - public static Builder builder(final PythonProcessProvider pythonCommand, + public static Builder builder(final ExternalProcessProvider pythonCommand, final Path launcherPath, final Class entryPointClass) { return new Builder<>(pythonCommand, launcherPath, entryPointClass); } @@ -204,7 +204,7 @@ public static final class Builder { private final Path m_launcherPath; - private final PythonProcessProvider m_pythonCommand; + private final ExternalProcessProvider m_pythonCommand; private final Class m_entryPointClass; @@ -214,7 +214,7 @@ public static final class Builder { private final List> m_entryPointCustomizers = new ArrayList<>(); - private Builder(final PythonProcessProvider pythonCommand, final Path launcherPath, + private Builder(final ExternalProcessProvider pythonCommand, final Path launcherPath, final Class entryPointClass) { m_launcherPath = launcherPath; m_pythonCommand = pythonCommand; diff --git a/org.knime.python3/src/main/java/org/knime/python3/QueuedPythonGatewayFactory.java b/org.knime.python3/src/main/java/org/knime/python3/QueuedPythonGatewayFactory.java index 89e6e2fde..e0b2965c8 100644 --- a/org.knime.python3/src/main/java/org/knime/python3/QueuedPythonGatewayFactory.java +++ b/org.knime.python3/src/main/java/org/knime/python3/QueuedPythonGatewayFactory.java @@ -72,8 +72,8 @@ import java.util.stream.Collectors; import org.knime.core.node.NodeLogger; +import org.knime.externalprocessprovider.ExternalProcessProvider; import org.knime.python3.PythonGatewayCreationGate.PythonGatewayCreationGateListener; -import org.knime.python3.processprovider.PythonProcessProvider; import com.google.common.util.concurrent.ThreadFactoryBuilder; @@ -154,7 +154,7 @@ public synchronized void reconfigureQueue(final int maxNumberOfIdlingGateways, * * @param command The Python command whose corresponding gateways to remove from the queue */ - public synchronized void clearQueuedGateways(final PythonProcessProvider command) { + public synchronized void clearQueuedGateways(final ExternalProcessProvider command) { if (m_queue != null) { m_queue.clearQueuedGateways(command); } @@ -317,7 +317,7 @@ private BlockingQueue getGatewayQueue(final PythonGatewayDescript } @Override - public synchronized void clearQueuedGateways(final PythonProcessProvider command) { + public synchronized void clearQueuedGateways(final ExternalProcessProvider command) { final List gatewaysToEvict = new ArrayList<>(); for (final var entry : m_gateways.entrySet()) { if (entry.getKey().getCommand().equals(command)) { @@ -468,7 +468,7 @@ public PythonGatewayDummyQueue(final int maxNumberOfIdlingGateways, final int ex } @Override - public void clearQueuedGateways(final PythonProcessProvider command) { + public void clearQueuedGateways(final ExternalProcessProvider command) { // Nothing to do. } @@ -497,11 +497,11 @@ public AbstractPythonGatewayQueue(final int maxNumberOfIdlingGateways, final int getNextGateway(PythonGatewayDescription description) throws IOException, InterruptedException; /** - * Clears all queued gateways that were created with the specified {@link PythonProcessProvider}. + * Clears all queued gateways that were created with the specified {@link ExternalProcessProvider}. * - * @param command The {@link PythonProcessProvider} + * @param command The {@link ExternalProcessProvider} */ - public abstract void clearQueuedGateways(PythonProcessProvider command); + public abstract void clearQueuedGateways(ExternalProcessProvider command); @Override public abstract void close(); diff --git a/org.knime.python3/src/main/java/org/knime/python3/SimplePythonCommand.java b/org.knime.python3/src/main/java/org/knime/python3/SimplePythonCommand.java index 2c5da808f..eb8b3b640 100644 --- a/org.knime.python3/src/main/java/org/knime/python3/SimplePythonCommand.java +++ b/org.knime.python3/src/main/java/org/knime/python3/SimplePythonCommand.java @@ -51,7 +51,7 @@ import java.util.List; /** - * A simple implementation of {@link PythonProcessProvider}. Runs a command that is given by a list of strings. + * A simple implementation of {@link ExternalProcessProvider}. Runs a command that is given by a list of strings. * * @author Marcel Wiedenmann, KNIME GmbH, Konstanz, Germany * @author Christian Dietz, KNIME GmbH, Konstanz, Germany From 8ba97376a0ba933995acde5e340b8cdc54525ab8 Mon Sep 17 00:00:00 2001 From: Benjamin Wilhelm Date: Thu, 12 Feb 2026 12:24:42 +0100 Subject: [PATCH 15/34] AP-25245: FIXUP AP-25245 (Python Environment Provider (Preview - Hidden)) --- org.knime.python3.arrow.tests/META-INF/MANIFEST.MF | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/org.knime.python3.arrow.tests/META-INF/MANIFEST.MF b/org.knime.python3.arrow.tests/META-INF/MANIFEST.MF index 47c7be9eb..31ac6010e 100644 --- a/org.knime.python3.arrow.tests/META-INF/MANIFEST.MF +++ b/org.knime.python3.arrow.tests/META-INF/MANIFEST.MF @@ -12,6 +12,6 @@ Require-Bundle: org.junit;bundle-version="[4.13.0,5.0.0)", org.apache.commons.lang3;bundle-version="[3.9.0,4.0.0)", org.knime.core.columnar;bundle-version="[5.6.0,6.0.0)", org.knime.core.data.columnar;bundle-version="[5.6.0,6.0.0)", - org.knime.python3.testing;bundle-version="[5.6.0,6.0.0)" + org.knime.python3.testing;bundle-version="[5.6.0,6.0.0)", + org.knime.externalprocessprovider;bundle-version="[5.11.0,6.0.0)" Automatic-Module-Name: org.knime.python3.arrow.tests -Import-Package: org.knime.externalprocessprovider From 691616389eecf48b5ae4c12a1014281a66bc54b8 Mon Sep 17 00:00:00 2001 From: Benjamin Wilhelm Date: Thu, 12 Feb 2026 12:26:29 +0100 Subject: [PATCH 16/34] AP-25245: FIXUP AP-25245 (Python Environment Provider (Preview - Hidden)) --- org.knime.python3.arrow.types.tests/META-INF/MANIFEST.MF | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/org.knime.python3.arrow.types.tests/META-INF/MANIFEST.MF b/org.knime.python3.arrow.types.tests/META-INF/MANIFEST.MF index 69c49056f..7d6d3dc73 100644 --- a/org.knime.python3.arrow.types.tests/META-INF/MANIFEST.MF +++ b/org.knime.python3.arrow.types.tests/META-INF/MANIFEST.MF @@ -16,7 +16,7 @@ Require-Bundle: org.knime.core.table;bundle-version="[5.6.0,6.0.0)", org.knime.core.data.columnar;bundle-version="[5.6.0,6.0.0)", com.fasterxml.jackson.core.jackson-databind;bundle-version="[2.12.1,3.0.0)", org.knime.python3.types;bundle-version="[5.6.0,6.0.0)", - org.knime.python3.testing;bundle-version="[5.6.0,6.0.0)" + org.knime.python3.testing;bundle-version="[5.6.0,6.0.0)", + org.knime.externalprocessprovider;bundle-version="[5.11.0,6.0.0)" Automatic-Module-Name: org.knime.python3.arrow.types.tests Export-Package: org.knime.python3.arrow.types -Import-Package: org.knime.externalprocessprovider From 0e0a587da8f6406f5e8a86d37d3687b7d94585ba Mon Sep 17 00:00:00 2001 From: Benjamin Wilhelm Date: Thu, 12 Feb 2026 12:27:27 +0100 Subject: [PATCH 17/34] AP-25245: FIXUP AP-25245 (Python Environment Provider (Preview - Hidden)) --- org.knime.python3.scripting.tests/META-INF/MANIFEST.MF | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/org.knime.python3.scripting.tests/META-INF/MANIFEST.MF b/org.knime.python3.scripting.tests/META-INF/MANIFEST.MF index b9d53874f..336ac68dd 100644 --- a/org.knime.python3.scripting.tests/META-INF/MANIFEST.MF +++ b/org.knime.python3.scripting.tests/META-INF/MANIFEST.MF @@ -11,5 +11,5 @@ Automatic-Module-Name: org.knime.python3.scripting.tests Export-Package: org.knime.python3.scripting Require-Bundle: org.junit;bundle-version="[4.13.0,5.0.0)", org.apache.arrow.memory-core;bundle-version="[18.1.0,19.0.0)", - org.knime.python3.testing;bundle-version="[5.6.0,6.0.0)" -Import-Package: org.knime.externalprocessprovider + org.knime.python3.testing;bundle-version="[5.6.0,6.0.0)", + org.knime.externalprocessprovider;bundle-version="[5.11.0,6.0.0)" From 75264f0f6f27358845ee7478418277928aae8394 Mon Sep 17 00:00:00 2001 From: Benjamin Wilhelm Date: Thu, 12 Feb 2026 12:27:53 +0100 Subject: [PATCH 18/34] AP-25245: FIXUP AP-25245 (Python Environment Provider (Preview - Hidden)) --- org.knime.python3.testing/META-INF/MANIFEST.MF | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/org.knime.python3.testing/META-INF/MANIFEST.MF b/org.knime.python3.testing/META-INF/MANIFEST.MF index db060a986..4f5fa5456 100644 --- a/org.knime.python3.testing/META-INF/MANIFEST.MF +++ b/org.knime.python3.testing/META-INF/MANIFEST.MF @@ -8,7 +8,7 @@ Bundle-Vendor: KNIME AG, Zurich, Switzerland Bundle-RequiredExecutionEnvironment: JavaSE-17 Require-Bundle: org.apache.commons.lang3;bundle-version="[3.9.0,4.0.0)", org.knime.python3;bundle-version="[5.6.0,6.0.0)", - org.knime.core.columnar;bundle-version="[5.6.0,6.0.0)" + org.knime.core.columnar;bundle-version="[5.6.0,6.0.0)", + org.knime.externalprocessprovider;bundle-version="[5.11.0,6.0.0)" Automatic-Module-Name: org.knime.python3.testing Export-Package: org.knime.python3.testing -Import-Package: org.knime.externalprocessprovider From aed540df43ac76017c1d8195590b4fd78e6b8033 Mon Sep 17 00:00:00 2001 From: Benjamin Wilhelm Date: Thu, 12 Feb 2026 12:30:29 +0100 Subject: [PATCH 19/34] AP-25245: FIXUP AP-25245 (Python Environment Provider (Preview - Hidden)) --- org.knime.python3/META-INF/MANIFEST.MF | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/org.knime.python3/META-INF/MANIFEST.MF b/org.knime.python3/META-INF/MANIFEST.MF index 12e8981c1..743415d30 100644 --- a/org.knime.python3/META-INF/MANIFEST.MF +++ b/org.knime.python3/META-INF/MANIFEST.MF @@ -17,9 +17,7 @@ Require-Bundle: com.google.guava;bundle-version="[31.0.1,32.0.0)", org.knime.python3.types;bundle-version="[5.9.0,6.0.0)", org.eclipse.equinox.p2.core;bundle-version="[2.0.0,3.0.0)";visibility:=reexport, org.eclipse.equinox.p2.engine;bundle-version="[2.0.0,3.0.0)";visibility:=reexport, - org.knime.pixi.port;bundle-version="[5.11.0,6.0.0)";resolution:=optional, - org.knime.conda.envinstall;bundle-version="[5.10.0,6.0.0)";resolution:=optional, - org.knime.externalprocessprovider;bundle-version="[5.11.0,6.0.0)";resolution:=optional + org.knime.externalprocessprovider;bundle-version="[5.11.0,6.0.0)" Export-Package: org.knime.python3, org.knime.python3.utils Automatic-Module-Name: org.knime.python3 From 0e150ca82991ecd83f65733cc8906b555f19b12f Mon Sep 17 00:00:00 2001 From: Benjamin Wilhelm Date: Thu, 12 Feb 2026 12:33:14 +0100 Subject: [PATCH 20/34] AP-25245: FIXUP AP-25245 (Python Environment Provider (Preview - Hidden)) --- org.knime.python3.scripting.nodes/META-INF/MANIFEST.MF | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/org.knime.python3.scripting.nodes/META-INF/MANIFEST.MF b/org.knime.python3.scripting.nodes/META-INF/MANIFEST.MF index d7598f311..b6ba827bb 100644 --- a/org.knime.python3.scripting.nodes/META-INF/MANIFEST.MF +++ b/org.knime.python3.scripting.nodes/META-INF/MANIFEST.MF @@ -23,7 +23,7 @@ Require-Bundle: org.knime.core;bundle-version="[5.11.0,6.0.0)", org.eclipse.ui;bundle-version="3.119.0", org.knime.conda;bundle-version="[5.11.0,6.0.0)", org.knime.conda.envbundling;bundle-version="[5.10.0,6.0.0)", - org.knime.pixi.port;bundle-version="[5.11.0,6.0.0)";resolution:=optional, + org.knime.pixi.port;bundle-version="[5.11.0,6.0.0)", org.knime.core.ui;bundle-version="[5.10.0,6.0.0)", org.knime.workbench.editor;bundle-version="[5.9.0,6.0.0)", org.apache.batik.util;bundle-version="[1.16.0,2.0.0)", @@ -39,9 +39,9 @@ Require-Bundle: org.knime.core;bundle-version="[5.11.0,6.0.0)", com.fasterxml.jackson.core.jackson-core;bundle-version="2.13.2", com.fasterxml.jackson.core.jackson-databind;bundle-version="2.13.2", org.eclipse.orbit.xml-apis-ext;bundle-version="[1.0.0,2.0.0)", - org.apache.commons.commons-io;bundle-version="[2.15.1,3.0.0)" + org.apache.commons.commons-io;bundle-version="[2.15.1,3.0.0)", + org.knime.externalprocessprovider;bundle-version="[5.11.0,6.0.0)" Automatic-Module-Name: org.knime.python3.scripting.nodes Export-Package: org.knime.python3.scripting.nodes.prefs Eclipse-RegisterBuddy: org.knime.ext.py4j Eclipse-BundleShape: dir -Import-Package: org.knime.externalprocessprovider From 4d32cbd0199a3b15eaa4e380236b6fa7842e5531 Mon Sep 17 00:00:00 2001 From: Benjamin Wilhelm Date: Thu, 12 Feb 2026 12:40:58 +0100 Subject: [PATCH 21/34] AP-25245: FIXUP AP-25245 (Python Environment Provider (Preview - Hidden)) --- .../src/main/java/org/knime/python3/PythonCommand.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/org.knime.python3/src/main/java/org/knime/python3/PythonCommand.java b/org.knime.python3/src/main/java/org/knime/python3/PythonCommand.java index e4259c334..52c2e8729 100644 --- a/org.knime.python3/src/main/java/org/knime/python3/PythonCommand.java +++ b/org.knime.python3/src/main/java/org/knime/python3/PythonCommand.java @@ -80,8 +80,12 @@ public interface PythonCommand extends ExternalProcessProvider { * without running the Python executable. Use {@link #createProcessBuilder()} to start Python processes. */ @Deprecated(since = "5.11", forRemoval = true) + Path getPythonExecutablePath(); + @Override - Path getExecutablePath(); + default Path getExecutablePath() { + return getPythonExecutablePath(); + } @Deprecated(since = "5.11", forRemoval = true) @Override From 97d8a997b71f892a180ae7c8f0889b9447340a2d Mon Sep 17 00:00:00 2001 From: Benjamin Wilhelm Date: Thu, 12 Feb 2026 12:42:30 +0100 Subject: [PATCH 22/34] AP-25245: FIXUP AP-25245 (Python Environment Provider (Preview - Hidden)) --- .../src/main/java/org/knime/python3/PythonCommand.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/org.knime.python3/src/main/java/org/knime/python3/PythonCommand.java b/org.knime.python3/src/main/java/org/knime/python3/PythonCommand.java index 52c2e8729..32b5aecfc 100644 --- a/org.knime.python3/src/main/java/org/knime/python3/PythonCommand.java +++ b/org.knime.python3/src/main/java/org/knime/python3/PythonCommand.java @@ -98,4 +98,4 @@ default Path getExecutablePath() { @Deprecated(since = "5.11", forRemoval = true) @Override String toString(); -} \ No newline at end of file +} From e11c36a9bd35ea71edd8a5bdc89249de2ede0c1c Mon Sep 17 00:00:00 2001 From: Benjamin Wilhelm Date: Thu, 12 Feb 2026 12:48:30 +0100 Subject: [PATCH 23/34] AP-25245: FIXUP AP-25245 (Python Environment Provider (Preview - Hidden)) --- .../nodes/prefs/BundledCondaEnvironmentConfig.java | 2 +- .../nodes/prefs/Python3ScriptingPreferences.java | 9 +-------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/BundledCondaEnvironmentConfig.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/BundledCondaEnvironmentConfig.java index bdbbd6533..c433a098b 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/BundledCondaEnvironmentConfig.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/BundledCondaEnvironmentConfig.java @@ -107,7 +107,7 @@ public boolean isAvailable() { } @Override - public ExternalProcessProvider getPythonCommand() { + public BundledPythonCommand getPythonCommand() { final var condaEnv = CondaEnvironmentRegistry.getEnvironment(m_bundledCondaEnvironment.getStringValue()); if (condaEnv == null) { final var errorMsg = "You have selected the 'Bundled' option in KNIME Python preferences, " diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/Python3ScriptingPreferences.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/Python3ScriptingPreferences.java index 153de151e..139c1ace1 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/Python3ScriptingPreferences.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/Python3ScriptingPreferences.java @@ -127,14 +127,7 @@ public static ExternalProcessProvider getPythonCommandPreference() { * @return The {@link ExternalProcessProvider} for the installed bundled environment. */ public static BundledPythonCommand getBundledPythonCommand() { - final var pythonCommand = getBundledCondaEnvironmentConfig().getPythonCommand(); - if (!(pythonCommand instanceof BundledPythonCommand)) { - throw new IllegalStateException( - "Bundled Python environment '" + BUNDLED_PYTHON_ENV_ID - + "' does not provide a BundledPythonCommand (got: " - + (pythonCommand == null ? "null" : pythonCommand.getClass().getName()) + ")"); - } - return (BundledPythonCommand)pythonCommand; + return getBundledCondaEnvironmentConfig().getPythonCommand(); } private static BundledCondaEnvironmentConfig getBundledCondaEnvironmentConfig() { From 565e579111ac980e0acca2f1ebe573bf5757f914 Mon Sep 17 00:00:00 2001 From: Benjamin Wilhelm Date: Thu, 12 Feb 2026 12:50:40 +0100 Subject: [PATCH 24/34] AP-25245: FIXUP AP-25245 (Python Environment Provider (Preview - Hidden)) --- .../nodes/script/PythonScriptNodeFactory.java | 16 +--------------- .../nodes/script/PythonScriptNodeModel.java | 11 +++++------ .../nodes/view/PythonViewNodeFactory.java | 14 +------------- .../nodes/view/PythonViewNodeModel.java | 4 ++-- 4 files changed, 9 insertions(+), 36 deletions(-) diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/script/PythonScriptNodeFactory.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/script/PythonScriptNodeFactory.java index 85550b5bd..0fbfb7dce 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/script/PythonScriptNodeFactory.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/script/PythonScriptNodeFactory.java @@ -58,13 +58,10 @@ import org.knime.core.node.BufferedDataTable; import org.knime.core.node.ConfigurableNodeFactory; import org.knime.core.node.NodeDialogPane; -import org.knime.core.node.NodeLogger; import org.knime.core.node.NodeView; import org.knime.core.node.context.NodeCreationConfiguration; import org.knime.core.node.port.PortType; import org.knime.core.node.port.image.ImagePortObject; - -import org.knime.pixi.port.PythonEnvironmentPortObject; import org.knime.python2.port.PickledObjectFileStorePortObject; import org.knime.python2.ports.InputPort; import org.knime.python2.ports.OutputPort; @@ -75,22 +72,12 @@ */ public final class PythonScriptNodeFactory extends ConfigurableNodeFactory { - private static final NodeLogger LOGGER = NodeLogger.getLogger(PythonScriptNodeFactory.class); - @Override protected Optional createPortsConfigBuilder() { final var b = new PortsConfigurationBuilder(); b.addExtendableInputPortGroup("Input object (pickled)", PickledObjectFileStorePortObject.TYPE); b.addExtendableInputPortGroupWithDefault("Input table", new PortType[0], new PortType[]{BufferedDataTable.TYPE}, BufferedDataTable.TYPE); - try { - b.addOptionalInputPortGroup("Python environment", PythonEnvironmentPortObject.TYPE_OPTIONAL); - LOGGER.debug("Successfully added optional Python environment port"); - } catch (NoClassDefFoundError e) { - LOGGER.warn("Could not add Python environment port - bundle not available: " + e.getMessage()); - } catch (Exception e) { - LOGGER.error("Unexpected error adding Python environment port", e); - } b.addExtendableOutputPortGroupWithDefault("Output table", new PortType[0], new PortType[]{BufferedDataTable.TYPE}, BufferedDataTable.TYPE); b.addExtendableOutputPortGroup("Output image", ImagePortObject.TYPE); @@ -105,8 +92,7 @@ protected PythonScriptNodeModel createNodeModel(final NodeCreationConfiguration if (urlConfig.isPresent()) { return PythonScriptNodeModel.createDnDNodeModel(urlConfig.get().getUrl()); } - return new PythonScriptNodeModel(createInputPorts(config), createOutputPorts(config), - PortsConfigurationUtils.hasPythonEnvironmentPort(config)); + return new PythonScriptNodeModel(createInputPorts(config), createOutputPorts(config)); } @Override diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/script/PythonScriptNodeModel.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/script/PythonScriptNodeModel.java index ca4381d59..ab9441a72 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/script/PythonScriptNodeModel.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/script/PythonScriptNodeModel.java @@ -66,13 +66,12 @@ */ final class PythonScriptNodeModel extends AbstractPythonScriptingNodeModel { - public PythonScriptNodeModel(final InputPort[] inPorts, final OutputPort[] outPorts, final boolean hasPythonEnvironmentPort) { - super(inPorts, outPorts, false, hasPythonEnvironmentPort, createDefaultScript(inPorts, outPorts)); + public PythonScriptNodeModel(final InputPort[] inPorts, final OutputPort[] outPorts) { + super(inPorts, outPorts, false, createDefaultScript(inPorts, outPorts)); } - PythonScriptNodeModel(final InputPort[] inPorts, final OutputPort[] outPorts, final boolean hasPythonEnvironmentPort, - final String defaultScript) { - super(inPorts, outPorts, false, hasPythonEnvironmentPort, defaultScript); + PythonScriptNodeModel(final InputPort[] inPorts, final OutputPort[] outPorts, final String defaultScript) { + super(inPorts, outPorts, false, defaultScript); } @@ -82,7 +81,7 @@ static PythonScriptNodeModel createDnDNodeModel(final URL url) { var variableNames = VariableNamesUtils.getVariableNames(inPorts, outPorts); var defaultScript = "import knime.scripting.io as knio\n\n" + getPythonObjectReaderDefaultScript(variableNames, getPath(url)); - return new PythonScriptNodeModel(inPorts, outPorts, false, defaultScript); + return new PythonScriptNodeModel(inPorts, outPorts, defaultScript); } private static String getPath(final URL url) { diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/view/PythonViewNodeFactory.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/view/PythonViewNodeFactory.java index d6bfc067e..d3d89c408 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/view/PythonViewNodeFactory.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/view/PythonViewNodeFactory.java @@ -69,7 +69,6 @@ import org.knime.python2.port.PickledObjectFileStorePortObject; import org.knime.python2.ports.ImageOutputPort; import org.knime.python2.ports.OutputPort; -import org.knime.python3.scripting.nodes.PortsConfigurationUtils; import org.knime.python3.views.HtmlFileNodeView; /** @@ -79,22 +78,12 @@ public final class PythonViewNodeFactory extends ConfigurableNodeFactory implements NodeViewFactory { - private static final NodeLogger LOGGER = NodeLogger.getLogger(PythonViewNodeFactory.class); - @Override protected Optional createPortsConfigBuilder() { final var b = new PortsConfigurationBuilder(); b.addExtendableInputPortGroup("Input object (pickled)", PickledObjectFileStorePortObject.TYPE); b.addExtendableInputPortGroupWithDefault("Input table", new PortType[0], new PortType[]{BufferedDataTable.TYPE}, BufferedDataTable.TYPE); - try { - b.addOptionalInputPortGroup("Python environment", PythonEnvironmentPortObject.TYPE_OPTIONAL); - LOGGER.debug("Successfully added optional Python environment port"); - } catch (NoClassDefFoundError e) { - LOGGER.warn("Could not add Python environment port - bundle not available: " + e.getMessage()); - } catch (Exception e) { - LOGGER.error("Unexpected error adding Python environment port", e); - } b.addOptionalOutputPortGroup("Output image", ImagePortObject.TYPE); return Optional.of(b); } @@ -102,8 +91,7 @@ protected Optional createPortsConfigBuilder() { @Override protected PythonViewNodeModel createNodeModel(final NodeCreationConfiguration creationConfig) { final var config = creationConfig.getPortConfig().get(); // NOSONAR - return new PythonViewNodeModel(createInputPorts(config), createOutputPorts(config), - PortsConfigurationUtils.hasPythonEnvironmentPort(config)); + return new PythonViewNodeModel(createInputPorts(config), createOutputPorts(config)); } @Override diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/view/PythonViewNodeModel.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/view/PythonViewNodeModel.java index bba354707..3ffa2ea0c 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/view/PythonViewNodeModel.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/view/PythonViewNodeModel.java @@ -61,8 +61,8 @@ */ final class PythonViewNodeModel extends AbstractPythonScriptingNodeModel { - public PythonViewNodeModel(final InputPort[] inPorts, final OutputPort[] outPorts, final boolean hasPythonEnvironmentPort) { - super(inPorts, outPorts, true, hasPythonEnvironmentPort, createDefaultScript(inPorts)); + public PythonViewNodeModel(final InputPort[] inPorts, final OutputPort[] outPorts) { + super(inPorts, outPorts, true, createDefaultScript(inPorts)); } Path getPathToHtml() { From 91734ffa9edb92df782778ff654ab95f77cb31f0 Mon Sep 17 00:00:00 2001 From: Benjamin Wilhelm Date: Thu, 12 Feb 2026 12:57:48 +0100 Subject: [PATCH 25/34] AP-25245: FIXUP AP-25245 (Python Environment Provider (Preview - Hidden)) --- .../AbstractPythonScriptingNodeModel.java | 58 ++----------------- 1 file changed, 5 insertions(+), 53 deletions(-) diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/AbstractPythonScriptingNodeModel.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/AbstractPythonScriptingNodeModel.java index b67166b79..251a86aa4 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/AbstractPythonScriptingNodeModel.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/AbstractPythonScriptingNodeModel.java @@ -82,8 +82,6 @@ import org.knime.core.util.asynclose.AsynchronousCloseableTracker; import org.knime.core.webui.node.view.NodeView; import org.knime.externalprocessprovider.ExternalProcessProvider; -import org.knime.pixi.port.PixiPythonCommand; -import org.knime.pixi.port.PythonEnvironmentPortObject; import org.knime.python2.PythonCommand; import org.knime.python2.PythonModuleSpec; import org.knime.python2.PythonVersion; @@ -158,8 +156,6 @@ static void setExpectedOutputView(final PythonKernel kernel, final boolean expec private final boolean m_hasView; - private final boolean m_hasPythonEnvironmentPort; - private String m_script; private final PythonCommandConfig m_command = createCommandConfig(); @@ -170,12 +166,11 @@ static void setExpectedOutputView(final PythonKernel kernel, final boolean expec new AsynchronousCloseableTracker<>(t -> LOGGER.debug("Kernel shutdown failed.", t)); protected AbstractPythonScriptingNodeModel(final InputPort[] inPorts, final OutputPort[] outPorts, - final boolean hasView, final boolean hasPythonEnvironmentPort, final String defaultScript) { - super(toPortTypes(inPorts, hasPythonEnvironmentPort), toPortTypes(outPorts)); + final boolean hasView, final String defaultScript) { + super(toPortTypes(inPorts), toPortTypes(outPorts)); m_inPorts = inPorts; m_outPorts = outPorts; m_hasView = hasView; - m_hasPythonEnvironmentPort = hasPythonEnvironmentPort; m_view = Optional.empty(); m_script = defaultScript; } @@ -184,19 +179,6 @@ private static final PortType[] toPortTypes(final Port[] ports) { return Arrays.stream(ports).map(Port::getPortType).toArray(PortType[]::new); } - private static final PortType[] toPortTypes(final Port[] ports, final boolean hasPythonEnvironmentPort) { - if (!hasPythonEnvironmentPort) { - return toPortTypes(ports); - } - // Add the optional Python environment port at the end of the input ports - final PortType[] portTypes = new PortType[ports.length + 1]; - for (int i = 0; i < ports.length; i++) { - portTypes[i] = ports[i].getPortType(); - } - portTypes[ports.length] = PythonEnvironmentPortObject.TYPE_OPTIONAL; - return portTypes; - } - @Override protected void saveSettingsTo(final NodeSettingsWO settings) { saveScriptTo(m_script, settings); @@ -217,9 +199,7 @@ protected void loadValidatedSettingsFrom(final NodeSettingsRO settings) throws I @Override protected PortObjectSpec[] configure(final PortObjectSpec[] inSpecs) throws InvalidSettingsException { - // The Python environment port (if present) is at the end of the input specs - final int numRegularPorts = m_inPorts.length; - for (int i = 0; i < numRegularPorts; i++) { + for (int i = 0; i < m_inPorts.length; i++) { m_inPorts[i].configure(inSpecs[i]); } return null; // NOSONAR Conforms to KNIME API. @@ -227,21 +207,6 @@ protected PortObjectSpec[] configure(final PortObjectSpec[] inSpecs) throws Inva @Override protected PortObject[] execute(final PortObject[] inObjects, final ExecutionContext exec) throws Exception { - // Extract Python command from environment port if present - final PythonCommand pythonCommandFromEnv; - if (m_hasPythonEnvironmentPort) { - try { - final ExternalProcessProvider pythonProvider = PythonEnvironmentPortObject.extractPythonCommand( - PortsConfigurationUtils.extractPythonEnvironmentPort(getPortsConfiguration(), inObjects)); - pythonCommandFromEnv = pythonProvider != null ? new LegacyPythonCommand(pythonProvider) : null; - } catch (NoClassDefFoundError e) { - LOGGER.debug("Environment port class not available", e); - pythonCommandFromEnv = null; - } - } else { - pythonCommandFromEnv = null; - } - double inWeight = 0d; final Set requiredAdditionalModules = new HashSet<>(); for (int i = 0; i < m_inPorts.length; i++) { @@ -253,8 +218,7 @@ protected PortObject[] execute(final PortObject[] inObjects, final ExecutionCont final var cancelable = new PythonExecutionMonitorCancelable(exec); try (final PythonKernel kernel = - getNextKernelFromQueue(requiredAdditionalModules, Collections.emptySet(), cancelable, - pythonCommandFromEnv)) { + getNextKernelFromQueue(requiredAdditionalModules, Collections.emptySet(), cancelable)) { final Collection inFlowVariables = getAvailableFlowVariables(Python3KernelBackend.getCompatibleFlowVariableTypes()).values(); kernel.putFlowVariables(null, inFlowVariables); @@ -389,19 +353,7 @@ private static Path persistedViewPath(final File nodeInternDir) { protected PythonKernel getNextKernelFromQueue(final Set requiredAdditionalModules, final Set optionalAdditionalModules, final PythonCancelable cancelable) throws PythonCanceledExecutionException, PythonIOException { - return getNextKernelFromQueue(requiredAdditionalModules, optionalAdditionalModules, cancelable, null); - } - - protected PythonKernel getNextKernelFromQueue(final Set requiredAdditionalModules, - final Set optionalAdditionalModules, final PythonCancelable cancelable, - final PythonCommand pythonCommandFromEnv) - throws PythonCanceledExecutionException, PythonIOException { - // Use Python command from environment port if available - // TODO: We might want to consider flow variables in addition to the environment port in the future - final PythonCommand commandToUse = - pythonCommandFromEnv != null ? pythonCommandFromEnv : m_command.getCommand(); - - return PythonKernelQueue.getNextKernel(commandToUse, PythonKernelBackendType.PYTHON3, + return PythonKernelQueue.getNextKernel(m_command.getCommand(), PythonKernelBackendType.PYTHON3, requiredAdditionalModules, optionalAdditionalModules, new PythonKernelOptions(), cancelable); } From 117afb53c3445dcf97684a088ba2e79cd90e4472 Mon Sep 17 00:00:00 2001 From: Benjamin Wilhelm Date: Thu, 12 Feb 2026 16:03:00 +0100 Subject: [PATCH 26/34] AP-25245: FIXUP AP-25245 (Python Environment Provider (Preview - Hidden)) --- .../scripting/nodes/prefs/BundledCondaEnvironmentConfig.java | 1 - 1 file changed, 1 deletion(-) diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/BundledCondaEnvironmentConfig.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/BundledCondaEnvironmentConfig.java index c433a098b..8ccf449a2 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/BundledCondaEnvironmentConfig.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/prefs/BundledCondaEnvironmentConfig.java @@ -51,7 +51,6 @@ import org.knime.conda.envbundling.environment.CondaEnvironmentRegistry; import org.knime.core.node.NodeLogger; import org.knime.core.node.defaultnodesettings.SettingsModelString; -import org.knime.externalprocessprovider.ExternalProcessProvider; import org.knime.python3.BundledPythonCommand; /** From 4e14218c4b4a56baf359acb1b18d316b1fd318c0 Mon Sep 17 00:00:00 2001 From: Benjamin Wilhelm Date: Thu, 12 Feb 2026 16:03:36 +0100 Subject: [PATCH 27/34] AP-25245: FIXUP AP-25245 (Python Environment Provider (Preview - Hidden)) --- .../python3/scripting/nodes/view/PythonViewNodeFactory.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/view/PythonViewNodeFactory.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/view/PythonViewNodeFactory.java index d3d89c408..b745307a5 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/view/PythonViewNodeFactory.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/view/PythonViewNodeFactory.java @@ -57,15 +57,12 @@ import org.knime.core.node.BufferedDataTable; import org.knime.core.node.ConfigurableNodeFactory; import org.knime.core.node.NodeDialogPane; -import org.knime.core.node.NodeLogger; import org.knime.core.node.NodeView; import org.knime.core.node.context.NodeCreationConfiguration; import org.knime.core.node.context.ports.PortsConfiguration; import org.knime.core.node.port.PortType; import org.knime.core.node.port.image.ImagePortObject; import org.knime.core.webui.node.view.NodeViewFactory; - -import org.knime.pixi.port.PythonEnvironmentPortObject; import org.knime.python2.port.PickledObjectFileStorePortObject; import org.knime.python2.ports.ImageOutputPort; import org.knime.python2.ports.OutputPort; From 313606c94b9034d79a77de216217ecf5681858c3 Mon Sep 17 00:00:00 2001 From: Benjamin Wilhelm Date: Thu, 12 Feb 2026 16:09:07 +0100 Subject: [PATCH 28/34] AP-25245: Fixup AP-25245 (Python Environment Provider (Preview - Hidden)) --- .../nodes2/script/PythonScriptNodeFactory.java | 17 ++++------------- .../nodes2/view/PythonViewNodeFactory.java | 17 ++++------------- 2 files changed, 8 insertions(+), 26 deletions(-) diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/script/PythonScriptNodeFactory.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/script/PythonScriptNodeFactory.java index 5a025dff7..c719630ee 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/script/PythonScriptNodeFactory.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/script/PythonScriptNodeFactory.java @@ -53,13 +53,13 @@ import static org.knime.python3.scripting.nodes2.PythonScriptPortsConfiguration.PORTGR_ID_OUT_IMAGE; import static org.knime.python3.scripting.nodes2.PythonScriptPortsConfiguration.PORTGR_ID_OUT_OBJECT; import static org.knime.python3.scripting.nodes2.PythonScriptPortsConfiguration.PORTGR_ID_OUT_TABLE; +import static org.knime.python3.scripting.nodes2.PythonScriptPortsConfiguration.PORTGR_ID_PYTHON_ENV; import java.util.Optional; import org.knime.core.node.BufferedDataTable; import org.knime.core.node.ConfigurableNodeFactory; import org.knime.core.node.NodeDialogPane; -import org.knime.core.node.NodeLogger; import org.knime.core.node.NodeView; import org.knime.core.node.context.NodeCreationConfiguration; import org.knime.core.node.port.PortType; @@ -81,10 +81,6 @@ public final class PythonScriptNodeFactory extends ConfigurableNodeFactory implements NodeDialogFactory { - private static final NodeLogger LOGGER = NodeLogger.getLogger(PythonScriptNodeFactory.class); - - private static final String PORTGR_ID_PYTHON_ENV = "Python environment"; - @Override public NodeDialog createNodeDialog() { return new PythonScriptNodeDialog(false); @@ -124,15 +120,10 @@ protected Optional createPortsConfigBuilder() { b.addExtendableInputPortGroup(PORTGR_ID_INP_OBJECT, PickledObjectFileStorePortObject.TYPE); b.addExtendableInputPortGroupWithDefault(PORTGR_ID_INP_TABLE, new PortType[0], new PortType[]{BufferedDataTable.TYPE}, BufferedDataTable.TYPE); - + // Add Python environment port - try { - b.addOptionalInputPortGroup(PORTGR_ID_PYTHON_ENV, PythonEnvironmentPortObject.TYPE_OPTIONAL); - LOGGER.debug("Successfully added Python environment port"); - } catch (NoClassDefFoundError e) { - LOGGER.debug("PythonEnvironmentPortObject not available: " + e.getMessage()); - } - + b.addOptionalInputPortGroup(PORTGR_ID_PYTHON_ENV, PythonEnvironmentPortObject.TYPE_OPTIONAL); + b.addExtendableOutputPortGroupWithDefault(PORTGR_ID_OUT_TABLE, new PortType[0], new PortType[]{BufferedDataTable.TYPE}, BufferedDataTable.TYPE); b.addExtendableOutputPortGroup(PORTGR_ID_OUT_IMAGE, ImagePortObject.TYPE); diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/view/PythonViewNodeFactory.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/view/PythonViewNodeFactory.java index 908d2d1fa..08239197d 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/view/PythonViewNodeFactory.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/view/PythonViewNodeFactory.java @@ -51,13 +51,13 @@ import static org.knime.python3.scripting.nodes2.PythonScriptPortsConfiguration.PORTGR_ID_INP_OBJECT; import static org.knime.python3.scripting.nodes2.PythonScriptPortsConfiguration.PORTGR_ID_INP_TABLE; import static org.knime.python3.scripting.nodes2.PythonScriptPortsConfiguration.PORTGR_ID_OUT_IMAGE; +import static org.knime.python3.scripting.nodes2.PythonScriptPortsConfiguration.PORTGR_ID_PYTHON_ENV; import java.util.Optional; import org.knime.core.node.BufferedDataTable; import org.knime.core.node.ConfigurableNodeFactory; import org.knime.core.node.NodeDialogPane; -import org.knime.core.node.NodeLogger; import org.knime.core.node.context.NodeCreationConfiguration; import org.knime.core.node.port.PortType; import org.knime.core.node.port.image.ImagePortObject; @@ -81,10 +81,6 @@ public class PythonViewNodeFactory extends ConfigurableNodeFactory implements NodeDialogFactory, NodeViewFactory { - private static final NodeLogger LOGGER = NodeLogger.getLogger(PythonViewNodeFactory.class); - - private static final String PORTGR_ID_PYTHON_ENV = "Python environment"; - @Override public NodeDialog createNodeDialog() { return new PythonScriptNodeDialog(true); @@ -133,15 +129,10 @@ protected Optional createPortsConfigBuilder() { b.addExtendableInputPortGroup(PORTGR_ID_INP_OBJECT, PickledObjectFileStorePortObject.TYPE); b.addExtendableInputPortGroupWithDefault(PORTGR_ID_INP_TABLE, new PortType[0], new PortType[]{BufferedDataTable.TYPE}, BufferedDataTable.TYPE); - + // Add Python environment port - try { - b.addOptionalInputPortGroup(PORTGR_ID_PYTHON_ENV, PythonEnvironmentPortObject.TYPE_OPTIONAL); - LOGGER.debug("Successfully added Python environment port"); - } catch (NoClassDefFoundError e) { - LOGGER.debug("PythonEnvironmentPortObject not available: " + e.getMessage()); - } - + b.addOptionalInputPortGroup(PORTGR_ID_PYTHON_ENV, PythonEnvironmentPortObject.TYPE_OPTIONAL); + b.addOptionalOutputPortGroup(PORTGR_ID_OUT_IMAGE, ImagePortObject.TYPE); return Optional.of(b); } From 470e5e07b060fe6b2ffc9d9a00906a514131ac01 Mon Sep 17 00:00:00 2001 From: Benjamin Wilhelm Date: Thu, 12 Feb 2026 16:12:45 +0100 Subject: [PATCH 29/34] AP-25245: FIXUP AP-25245 (Python Environment Provider (Preview - Hidden)) --- .../nodes2/PythonScriptPortsConfiguration.java | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptPortsConfiguration.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptPortsConfiguration.java index 79d07345f..8522c3af7 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptPortsConfiguration.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptPortsConfiguration.java @@ -103,7 +103,8 @@ public final class PythonScriptPortsConfiguration { * @param portsConfig * @return a new {@link PythonScriptPortsConfiguration} */ - static PythonScriptPortsConfiguration fromPortsConfiguration(final PortsConfiguration portsConfig, final boolean hasView) { + static PythonScriptPortsConfiguration fromPortsConfiguration(final PortsConfiguration portsConfig, + final boolean hasView) { // Get the number of different output ports from the ports configuration (ArrayUtils#getLength handles null) final Map inPortsLocation = portsConfig.getInputPortLocation(); @@ -117,7 +118,8 @@ static PythonScriptPortsConfiguration fromPortsConfiguration(final PortsConfigur final var numOutTables = ArrayUtils.getLength(outPortsLocation.get(PORTGR_ID_OUT_TABLE)); final var numOutImages = ArrayUtils.getLength(outPortsLocation.get(PORTGR_ID_OUT_IMAGE)); final var numOutObjects = ArrayUtils.getLength(outPortsLocation.get(PORTGR_ID_OUT_OBJECT)); - return new PythonScriptPortsConfiguration(numInTables, numInObjects, numOutTables, numOutImages, numOutObjects, hasView, hasEnvironmentPort); + return new PythonScriptPortsConfiguration(numInTables, numInObjects, numOutTables, numOutImages, numOutObjects, + hasView, hasEnvironmentPort); } /** @@ -174,11 +176,13 @@ static PythonScriptPortsConfiguration fromCurrentNodeContext() { } var hasView = nodeContainer.getNrViews() > 0; - return new PythonScriptPortsConfiguration(numInTables, numInObjects, numOutTables, numOutImages, numOutObjects, hasView, hasEnvironmentPort); + return new PythonScriptPortsConfiguration(numInTables, numInObjects, numOutTables, numOutImages, numOutObjects, + hasView, hasEnvironmentPort); } private PythonScriptPortsConfiguration(final int numInTables, final int numInObjects, final int numOutTables, - final int numOutImages, final int numOutObjects, final boolean hasView, final boolean hasPythonEnvironmentPort) { + final int numOutImages, final int numOutObjects, final boolean hasView, + final boolean hasPythonEnvironmentPort) { m_numInTables = numInTables; m_numInObjects = numInObjects; m_numOutTables = numOutTables; @@ -229,6 +233,7 @@ public int getNumOutObjects() { public boolean hasView() { return m_hasView; } + /** * @return if the node has a Python environment port (accepts PythonEnvironmentPortObject) */ From 448dcf1df3b6eebf00767ed4674bec5eb42701f4 Mon Sep 17 00:00:00 2001 From: Benjamin Wilhelm Date: Thu, 12 Feb 2026 16:18:00 +0100 Subject: [PATCH 30/34] AP-25245: FIXUP AP-25245 (Python Environment Provider (Preview - Hidden)) --- .../PythonScriptingInputOutputModelUtils.java | 25 ++----------------- 1 file changed, 2 insertions(+), 23 deletions(-) diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingInputOutputModelUtils.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingInputOutputModelUtils.java index 9290efdf0..950f30700 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingInputOutputModelUtils.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingInputOutputModelUtils.java @@ -58,7 +58,6 @@ import org.knime.core.data.DataTableSpec; import org.knime.core.data.DataType; import org.knime.core.node.BufferedDataTable; -import org.knime.core.node.NodeLogger; import org.knime.core.node.port.PortType; import org.knime.core.node.port.flowvariable.FlowVariablePortObject; import org.knime.core.node.port.image.ImagePortObject; @@ -75,8 +74,6 @@ */ final class PythonScriptingInputOutputModelUtils { - private static final NodeLogger LOGGER = NodeLogger.getLogger(PythonScriptingInputOutputModelUtils.class); - /** * The type string used for tables */ @@ -131,13 +128,6 @@ static List getInputObjects(final InputPortInfo[] inputPorts) var objectIdx = 0; for (int i = 0; i < inputPorts.length; i++) { final var type = inputPorts[i].portType(); - - // Skip Python environment ports - they are not data ports - if (isPythonEnvironmentPort(type)) { - LOGGER.debugWithFormat("Skipping environment port at index %d (type: %s) - not exposed to Python script", i, type.getName()); - continue; - } - final var spec = inputPorts[i].portSpec(); if (spec instanceof DataTableSpec dataTableSpec) { // Table with specs available @@ -160,6 +150,8 @@ static List getInputObjects(final InputPortInfo[] inputPorts) // Object (spec not used) inputInfos.add(createInputModel(objectIdx, INPUT_OUTPUT_TYPE_OBJECT)); objectIdx++; + } else if (type.acceptsPortObjectClass(PythonEnvironmentPortObject.class)) { + // Skip Python environment ports - they are not data ports } else { throw new IllegalStateException("Unsupported input port. This is an implementation error."); } @@ -215,19 +207,6 @@ private static boolean isNoFlowVariablePort(final PortType portType) { return !portType.acceptsPortObjectClass(FlowVariablePortObject.class); } - private static boolean isPythonEnvironmentPort(final PortType portType) { - try { - final boolean isPythonEnvPort = portType.acceptsPortObjectClass(PythonEnvironmentPortObject.class); - LOGGER.debugWithFormat("Checking if port type '%s' is environment port: %s", - portType.getName(), isPythonEnvPort); - return isPythonEnvPort; - } catch (NoClassDefFoundError e) { - // Python environment bundle not available - LOGGER.debugWithFormat("Python environment bundle not available for port type '%s'", portType.getName()); - return false; - } - } - private static String portTypeToInputOutputType(final PortType portType) { if (portType.acceptsPortObjectClass(BufferedDataTable.class)) { return INPUT_OUTPUT_TYPE_TABLE; From 21c860d4901fcab3c9bf302b3c261956c40b19e9 Mon Sep 17 00:00:00 2001 From: Benjamin Wilhelm Date: Thu, 12 Feb 2026 16:22:03 +0100 Subject: [PATCH 31/34] AP-25245: FIXUP AP-25245 (Python Environment Provider (Preview - Hidden)) --- .../python3/scripting/nodes2/PythonIOUtils.java | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonIOUtils.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonIOUtils.java index 6b784dd9c..0e36eb36b 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonIOUtils.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonIOUtils.java @@ -105,8 +105,8 @@ private PythonIOUtils() { * Note that {@link PythonEnvironmentPortObject}s are filtered out as they are only used for environment * configuration and not passed to Python as data. * - * @param data a list of port objects. Only {@link BufferedDataTable}, {@link PickledObjectFileStorePortObject}, - * and {@link PythonEnvironmentPortObject} are supported. + * @param data a list of port objects. Only {@link BufferedDataTable}, {@link PickledObjectFileStorePortObject}, and + * {@link PythonEnvironmentPortObject} are supported. * @param tableConverter a table converter that is used to convert the {@link BufferedDataTable}s to Python sources * @param exec for progress reporting and cancellation * @return an array of Python data sources @@ -123,18 +123,6 @@ static PythonDataSource[] createSources(final PortObject[] data, final PythonArr final var tablePortObjects = Arrays.stream(data) // .filter(BufferedDataTable.class::isInstance) // .toArray(BufferedDataTable[]::new); - final var pythonEnvPortObjects = Arrays.stream(data) // - .filter(PythonEnvironmentPortObject.class::isInstance) // - .toArray(PythonEnvironmentPortObject[]::new); - - NodeLogger.getLogger(PythonIOUtils.class).debugWithFormat( - "Creating sources from %d input ports: %d tables, %d pickled objects, %d PythonEnvironment ports (filtered out)", - data.length, tablePortObjects.length, pickledPortObjects.length, pythonEnvPortObjects.length); - - // Make sure that all ports are tables, pickled port objects, or environment ports - if (pickledPortObjects.length + tablePortObjects.length + pythonEnvPortObjects.length < data.length) { - throw new IllegalArgumentException("Unsupported port type connected. This is an implementation error."); - } // Progress handling final var pickledProgressWeight = 1; From 6230861c604482fdaecad997cc27ff7201d0b6dd Mon Sep 17 00:00:00 2001 From: Benjamin Wilhelm Date: Thu, 12 Feb 2026 17:19:20 +0100 Subject: [PATCH 32/34] AP-25245: WIP AP-25245 (Python Environment Provider (Preview - Hidden)) --- .../nodes/PortsConfigurationUtils.java | 27 ++++------- .../nodes2/PythonScriptNodeModel.java | 42 +++++------------ .../nodes2/PythonScriptingService.java | 47 ++++++++++--------- 3 files changed, 46 insertions(+), 70 deletions(-) diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/PortsConfigurationUtils.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/PortsConfigurationUtils.java index d3e58ef55..329da5b9d 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/PortsConfigurationUtils.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes/PortsConfigurationUtils.java @@ -190,19 +190,18 @@ public static OutputPort createPickledObjectOutputPort(final int outObjectSuffix /** * Extract the Python environment port object from the input port objects, if present. * - * @param config the ports configuration * @param inObjects the input port objects * @return the Python environment port object, or null if not present + * @throws IllegalArgumentException if a Python environment port is expected but not found */ - public static PythonEnvironmentPortObject extractPythonEnvironmentPort( - final PortsConfiguration config, final PortObject[] inObjects) { - final PortType[] inTypes = config.getInputPorts(); - for (int i = 0; i < inTypes.length; i++) { - if (isPythonEnvironmentPort(inTypes[i]) && PythonEnvironmentPortObject.isPythonEnvironmentPortObject(inObjects[i])) { - return (PythonEnvironmentPortObject) inObjects[i]; + public static PythonEnvironmentPortObject extractPythonEnvironmentPort(final PortObject[] inObjects) { + for (final PortObject inObject : inObjects) { + if (inObject instanceof PythonEnvironmentPortObject envPort) { + return envPort; } } - return null; + throw new IllegalArgumentException( + "Expected a Python environment port object in the input ports, but none was found."); } /*public static void installPythonEnvironmentIfPresent(final PortsConfiguration config, final PortObject[] inObjects, @@ -217,21 +216,15 @@ public static PythonEnvironmentPortObject extractPythonEnvironmentPort( * Filter out the Python environment port from the input port objects array. * The environment port is not a data port and should not be passed to the session. * - * @param config the ports configuration * @param inObjects the input port objects * @return the filtered array without the Python environment port */ - public static PortObject[] filterEnvironmentPort(final PortsConfiguration config, final PortObject[] inObjects) { - if (!hasPythonEnvironmentPort(config)) { - return inObjects; - } - + public static PortObject[] filterEnvironmentPort(final PortObject[] inObjects) { // Find and exclude the environment port - final PortType[] inTypes = config.getInputPorts(); final PortObject[] filtered = new PortObject[inObjects.length - 1]; int filteredIndex = 0; - for (int i = 0; i < inTypes.length && i < inObjects.length; i++) { - if (!isPythonEnvironmentPort(inTypes[i])) { + for (int i = 0; i < inObjects.length; i++) { + if (!(inObjects[i] instanceof PythonEnvironmentPortObject)) { filtered[filteredIndex++] = inObjects[i]; } } diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptNodeModel.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptNodeModel.java index 962367bf5..5670123d5 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptNodeModel.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptNodeModel.java @@ -87,7 +87,6 @@ import org.knime.core.util.asynclose.AsynchronousCloseableTracker; import org.knime.core.webui.node.dialog.scripting.ScriptingService.ConsoleText; import org.knime.externalprocessprovider.ExternalProcessProvider; -import org.knime.pixi.port.PythonEnvironmentPortObject; import org.knime.python3.PythonProcessTerminatedException; import org.knime.python3.scripting.nodes.PortsConfigurationUtils; import org.knime.python3.scripting.nodes2.ConsoleOutputUtils.ConsoleOutputStorage; @@ -120,8 +119,6 @@ public final class PythonScriptNodeModel extends NodeModel { private final PythonScriptPortsConfiguration m_ports; - private final PortsConfiguration m_portsConfiguration; - private final AsynchronousCloseableTracker m_sessionShutdownTracker = new AsynchronousCloseableTracker<>(t -> LOGGER.debug("Kernel shutdown failed.", t)); @@ -151,19 +148,11 @@ public final class PythonScriptNodeModel extends NodeModel { public PythonScriptNodeModel(final PortsConfiguration portsConfiguration, final boolean hasView) { super(portsConfiguration.getInputPorts(), portsConfiguration.getOutputPorts()); m_hasView = hasView; - m_portsConfiguration = portsConfiguration; m_ports = PythonScriptPortsConfiguration.fromPortsConfiguration(portsConfiguration, hasView); m_settings = new PythonScriptNodeSettings(m_ports); m_view = Optional.empty(); } - /** - * @return the ports configuration - */ - private PortsConfiguration getPortsConfiguration() { - return m_portsConfiguration; - } - /** * @return the path to the HTML view file * @throws IllegalStateException if no view is available @@ -189,30 +178,21 @@ protected PortObjectSpec[] configure(final PortObjectSpec[] inSpecs) throws Inva @Override protected PortObject[] execute(final PortObject[] inObjects, final ExecutionContext exec) throws IOException, InterruptedException, CanceledExecutionException, KNIMEException { + var pythonEnvironmentPort = PortsConfigurationUtils.extractPythonEnvironmentPort(inObjects); // Install Python environment early to avoid timeout issues during gateway connection final ExecutionMonitor remainingProgress; + final ExternalProcessProvider pythonCommand; if (m_ports.hasPythonEnvironmentPort()) { - //PortsConfigurationUtils.installPythonEnvironmentIfPresent(getPortsConfiguration(), inObjects, - // exec.createSubProgress(0.2)); - PythonEnvironmentPortObject.installPythonEnvironmentWithProgress(getPortsConfiguration(), inObjects, - exec.createSubProgress(0.2)); + pythonEnvironmentPort.installPythonEnvironmentWithProgress(exec.createSubProgress(0.2)); + pythonCommand = pythonEnvironmentPort.getPythonCommand(); remainingProgress = exec.createSubProgress(0.8); } else { + // No environment port, just use the configured command and use the whole progress for the execution remainingProgress = exec; - } - - // Extract Python command from environment port if connected, otherwise use configured command - final ExternalProcessProvider pythonCommand; - final ExternalProcessProvider pythonCommandFromEnv = PythonEnvironmentPortObject.extractPythonCommand( - PortsConfigurationUtils.extractPythonEnvironmentPort(getPortsConfiguration(), inObjects)); - if (pythonCommandFromEnv != null) { - LOGGER.debug("Using Python from environment port"); - pythonCommand = pythonCommandFromEnv; - // TODO: Consider if flow variable should take precedence over environment port - } else { pythonCommand = ExecutableSelectionUtils.getPythonCommand(m_settings.getExecutableSelection()); } + m_consoleOutputStorage = null; final var consoleConsumer = ConsoleOutputUtils.createConsoleConsumer(); @@ -220,8 +200,12 @@ protected PortObject[] execute(final PortObject[] inObjects, final ExecutionCont new PythonScriptingSession(pythonCommand, consoleConsumer, new ModelFileStoreHandlerSupplier())) { // Filter out environment port from inObjects - it's not a data port - final PortObject[] dataPortObjects = - PortsConfigurationUtils.filterEnvironmentPort(getPortsConfiguration(), inObjects); + final PortObject[] dataPortObjects; + if (m_ports.hasPythonEnvironmentPort()) { + dataPortObjects = PortsConfigurationUtils.filterEnvironmentPort(inObjects); + } else { + dataPortObjects = inObjects; + } remainingProgress.setProgress(0.0, "Setting up inputs"); session.setupIO(dataPortObjects, getAvailableFlowVariables(KNOWN_FLOW_VARIABLE_TYPES).values(), @@ -316,8 +300,6 @@ private void pushNewFlowVariable(final FlowVariable variable) { variable.getValue(variable.getVariableType())); } - - /** Get the output view from the session if the node has a view and remember the path */ private void collectViewFromSession(final PythonScriptingSession session) throws IOException, KNIMEException { if (m_hasView) { diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingService.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingService.java index 9155d1337..90f556136 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingService.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingService.java @@ -53,7 +53,6 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Arrays; import java.util.Collections; import java.util.Map; import java.util.Optional; @@ -78,7 +77,7 @@ import org.knime.core.webui.node.dialog.scripting.InputOutputModel; import org.knime.core.webui.node.dialog.scripting.ScriptingService; import org.knime.externalprocessprovider.ExternalProcessProvider; -import org.knime.pixi.port.PythonEnvironmentPortObject; +import org.knime.python3.scripting.nodes.PortsConfigurationUtils; import org.knime.python3.scripting.nodes2.PythonScriptingService.ExecutableOption.ExecutableOptionType; import org.knime.python3.scripting.nodes2.PythonScriptingSession.ExecutionInfo; import org.knime.python3.scripting.nodes2.PythonScriptingSession.ExecutionStatus; @@ -257,31 +256,23 @@ private void startNewInteractiveSession() throws IOException, InterruptedExcepti PortObject[] dataPortObjects = inputData; // By default, all inputs are data ports if (m_ports.hasPythonEnvironmentPort() && inputData != null && inputData.length > 0) { - try { - // Environment port is always the last port when present - final PortObject lastPort = inputData[inputData.length - 1]; - pythonCommand = PythonEnvironmentPortObject.extractPythonCommand(lastPort); - if (pythonCommand != null) { - LOGGER.debug("Using Python environment from connected port for interactive session"); - // Filter out environment port from data ports (it's the last one) - dataPortObjects = Arrays.copyOf(inputData, inputData.length - 1); - } - } catch (Exception e) { - LOGGER.warn("Failed to extract Python command from environment port: " + e.getMessage()); - } - } - - // Fall back to user selection if no environment port or extraction failed - if (pythonCommand == null) { + // TODO only log about environment installation if it actually needs to be installed + addConsoleOutputEvent( + new ConsoleText("Installing Python environment from environment port...\n", false)); + var envPort = PortsConfigurationUtils.extractPythonEnvironmentPort(inputData); + envPort.installPythonEnvironmentWithProgress(new ExecutionMonitor()); // Do not report the progress + addConsoleOutputEvent( + new ConsoleText("Successfully installed Python environment from environment port.\n", false)); + pythonCommand = PortsConfigurationUtils.extractPythonEnvironmentPort(inputData).getPythonCommand(); + } else { pythonCommand = ExecutableSelectionUtils.getPythonCommand(getExecutableOption(m_executableSelection)); } // TODO report the progress of converting the tables using the ExecutionMonitor? m_interactiveSession = new PythonScriptingSession(pythonCommand, PythonScriptingService.this::addConsoleOutputEvent, new DialogFileStoreHandlerSupplier()); - m_interactiveSession.setupIO(dataPortObjects, getSupportedFlowVariables(), - m_ports.getNumOutTables(), m_ports.getNumOutImages(), m_ports.getNumOutObjects(), m_hasView, - new ExecutionMonitor()); + m_interactiveSession.setupIO(dataPortObjects, getSupportedFlowVariables(), m_ports.getNumOutTables(), + m_ports.getNumOutImages(), m_ports.getNumOutObjects(), m_hasView, new ExecutionMonitor()); } private synchronized void executeScriptInternal(final String script, final boolean newSession, @@ -289,7 +280,18 @@ private synchronized void executeScriptInternal(final String script, final boole try { // Restart the session if necessary if (m_interactiveSession == null || newSession) { - startNewInteractiveSession(); + try { + startNewInteractiveSession(); + } catch (IOException | InterruptedException | CanceledExecutionException ex) { + // TODO cleanup + if (ex instanceof InterruptedException) { + Thread.currentThread().interrupt(); // Re-interrupt + } + var message = "Failed to start interactive Python session: " + ex.getMessage(); + LOGGER.error(message, ex); + sendExecutionFinishedEvent(new ExecutionInfo(ExecutionStatus.FATAL_ERROR, message)); + return; + } } // Run the script @@ -528,5 +530,4 @@ public void close() { } } } - } From 644192937176b3d1e12113cbf8a631d7109f8c9d Mon Sep 17 00:00:00 2001 From: Benjamin Wilhelm Date: Thu, 12 Feb 2026 17:59:23 +0100 Subject: [PATCH 33/34] AP-25245: WIP AP-25245 (Python Environment Provider (Preview - Hidden)) --- .../nodes2/PythonScriptingSession.java | 54 +------------------ 1 file changed, 2 insertions(+), 52 deletions(-) diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingSession.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingSession.java index 538b88f1a..0929ca279 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingSession.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingSession.java @@ -420,15 +420,12 @@ Optional getOutputView() throws IOException { private static PythonGateway createGateway(final ExternalProcessProvider pythonCommand) throws IOException, InterruptedException { - if (pythonCommand.getExecutablePath() - .startsWith(CondaEnvironmentIdentifier.NOT_EXECUTED_PATH_PLACEHOLDER)) { + if (pythonCommand.getExecutablePath().startsWith(CondaEnvironmentIdentifier.NOT_EXECUTED_PATH_PLACEHOLDER)) { throw new IOException(CondaEnvironmentIdentifier.NOT_EXECUTED_PATH_PLACEHOLDER); } - // Wrap internal PythonCommand in adapter for public API - final var pythonCommandAdapter = new PythonCommandAdapter(pythonCommand); final var gatewayDescriptionBuilder = - PythonGatewayDescription.builder(pythonCommandAdapter, LAUNCHER.toAbsolutePath(), PythonScriptingEntryPoint.class); + PythonGatewayDescription.builder(pythonCommand, LAUNCHER.toAbsolutePath(), PythonScriptingEntryPoint.class); gatewayDescriptionBuilder.withPreloaded(PythonArrowExtension.INSTANCE); gatewayDescriptionBuilder.withPreloaded(PythonViewsExtension.INSTANCE); @@ -525,51 +522,4 @@ public interface FileStoreHandlerSupplier extends AutoCloseable { @Override void close(); } - - /** - * Adapter that wraps org.knime.pixi.port.PythonCommand to implement org.knime.python3.PythonCommand. - * This is needed because PythonGatewayDescription.builder() requires the public API type. - */ - private static final class PythonCommandAdapter implements ExternalProcessProvider { - private final ExternalProcessProvider m_delegate; - - PythonCommandAdapter(final ExternalProcessProvider delegate) { - m_delegate = delegate; - } - - @Override - public ProcessBuilder createProcessBuilder() { - return m_delegate.createProcessBuilder(); - } - - @Override - public Path getExecutablePath() { - return m_delegate.getExecutablePath(); - } - - @Override - public int hashCode() { - return m_delegate.hashCode(); - } - - @Override - public boolean equals(final Object obj) { - if (this == obj) { - return true; - } - if (obj instanceof PythonCommandAdapter other) { - return m_delegate.equals(other.m_delegate); - } - // Add symmetric equality with delegate type for gateway queue cleanup - if (obj instanceof ExternalProcessProvider) { - return m_delegate.equals(obj); - } - return false; - } - - @Override - public String toString() { - return m_delegate.toString(); - } - } } From 276cf92f5684fdadf0a0ce1f4900c9a9b3373f78 Mon Sep 17 00:00:00 2001 From: Benjamin Wilhelm Date: Fri, 13 Feb 2026 14:25:06 +0100 Subject: [PATCH 34/34] AP-25245: WIP AP-25245 (Python Environment Provider (Preview - Hidden)) --- .../knime/python3/scripting/nodes2/PythonScriptNodeModel.java | 2 +- .../knime/python3/scripting/nodes2/PythonScriptingService.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptNodeModel.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptNodeModel.java index 5670123d5..23dddcc2b 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptNodeModel.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptNodeModel.java @@ -184,7 +184,7 @@ protected PortObject[] execute(final PortObject[] inObjects, final ExecutionCont final ExecutionMonitor remainingProgress; final ExternalProcessProvider pythonCommand; if (m_ports.hasPythonEnvironmentPort()) { - pythonEnvironmentPort.installPythonEnvironmentWithProgress(exec.createSubProgress(0.2)); + pythonEnvironmentPort.installPythonEnvironment(exec.createSubProgress(0.2)); pythonCommand = pythonEnvironmentPort.getPythonCommand(); remainingProgress = exec.createSubProgress(0.8); } else { diff --git a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingService.java b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingService.java index 90f556136..40def317b 100644 --- a/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingService.java +++ b/org.knime.python3.scripting.nodes/src/main/java/org/knime/python3/scripting/nodes2/PythonScriptingService.java @@ -260,7 +260,7 @@ private void startNewInteractiveSession() throws IOException, InterruptedExcepti addConsoleOutputEvent( new ConsoleText("Installing Python environment from environment port...\n", false)); var envPort = PortsConfigurationUtils.extractPythonEnvironmentPort(inputData); - envPort.installPythonEnvironmentWithProgress(new ExecutionMonitor()); // Do not report the progress + envPort.installPythonEnvironment(new ExecutionMonitor()); // Do not report the progress addConsoleOutputEvent( new ConsoleText("Successfully installed Python environment from environment port.\n", false)); pythonCommand = PortsConfigurationUtils.extractPythonEnvironmentPort(inputData).getPythonCommand();