From 0b2e88f62580235c9631b17fd211ea0e1a085e9e Mon Sep 17 00:00:00 2001 From: Marc Lehner Date: Tue, 6 Jan 2026 13:48:16 +0100 Subject: [PATCH 01/10] AP-25245: add pixi port to python scripting node AP-25245 () --- .../META-INF/MANIFEST.MF | 1 + .../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, 313 insertions(+), 24 deletions(-) diff --git a/org.knime.python3.scripting.nodes/META-INF/MANIFEST.MF b/org.knime.python3.scripting.nodes/META-INF/MANIFEST.MF index bb50ca51f..4f3b74809 100644 --- a/org.knime.python3.scripting.nodes/META-INF/MANIFEST.MF +++ b/org.knime.python3.scripting.nodes/META-INF/MANIFEST.MF @@ -23,6 +23,7 @@ Require-Bundle: org.knime.core;bundle-version="[5.10.0,6.0.0)", org.eclipse.ui;bundle-version="3.119.0", org.knime.conda;bundle-version="[5.9.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.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 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 cf05f8631b2d087de5ede0d4448734c26c333799 Mon Sep 17 00:00:00 2001 From: Marc Lehner Date: Wed, 7 Jan 2026 14:45:31 +0100 Subject: [PATCH 02/10] 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 4f3b74809..1b9081fde 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.10.0,6.0.0)", org.eclipse.ui;bundle-version="3.119.0", org.knime.conda;bundle-version="[5.9.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 366d73f9cfa23322386353870cda7bd7a12bd04c Mon Sep 17 00:00:00 2001 From: Marc Lehner Date: Tue, 13 Jan 2026 16:11:24 +0100 Subject: [PATCH 03/10] 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 bbe5ab0eb0119a29e296be7ceb6dfa3085ce1c45 Mon Sep 17 00:00:00 2001 From: Marc Lehner Date: Wed, 14 Jan 2026 08:42:56 +0100 Subject: [PATCH 04/10] 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 1b9081fde..66a9a6c29 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.10.0,6.0.0)", org.knime.conda;bundle-version="[5.9.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 2cc6dfca23270eba2f846efb7022a628bb558e67 Mon Sep 17 00:00:00 2001 From: Marc Lehner Date: Wed, 14 Jan 2026 16:42:40 +0100 Subject: [PATCH 05/10] 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 8b0dbffad46dad6887259cc04f26bc80afd66465 Mon Sep 17 00:00:00 2001 From: Marc Lehner Date: Mon, 19 Jan 2026 18:37:14 +0100 Subject: [PATCH 06/10] 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 66a9a6c29..1b9081fde 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.10.0,6.0.0)", org.knime.conda;bundle-version="[5.9.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 23b735bd62b5b792681e3ab90d0860f4ddd3d89e Mon Sep 17 00:00:00 2001 From: Marc Lehner Date: Wed, 21 Jan 2026 14:52:03 +0100 Subject: [PATCH 07/10] 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 18c10075bcdb3f0d24a9a4932c5163bcc3b1d0c8 Mon Sep 17 00:00:00 2001 From: Marc Lehner Date: Mon, 26 Jan 2026 13:15:12 +0100 Subject: [PATCH 08/10] 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 1b9081fde..b31506930 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 9d644581384a29849fea4dfcfd5af1449efc9211 Mon Sep 17 00:00:00 2001 From: Marc Lehner Date: Mon, 26 Jan 2026 13:23:17 +0100 Subject: [PATCH 09/10] 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 e031c7405..efc25a22c 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 3ae27b1b9d327cfa824a7f0ae5cc6651a21ceddb Mon Sep 17 00:00:00 2001 From: Carsten Haubold Date: Mon, 26 Jan 2026 17:54:55 +0000 Subject: [PATCH 10/10] AP-25245: WIP: use debug instead of info log for trivial messages. Could be removed completely? 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()); }