Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions org.knime.python3.arrow.tests/META-INF/MANIFEST.MF
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -83,7 +83,7 @@ private TestUtils() {
* @throws InterruptedException
*/
public static PythonGateway<ArrowTestsEntryPoint> 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()) //
Expand Down
1 change: 1 addition & 0 deletions org.knime.python3.arrow.types.tests/META-INF/MANIFEST.MF
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -893,7 +893,7 @@ interface TriConsumer<A, B, C> {

private static <E extends PythonEntryPoint> PythonGateway<E> openPythonGateway(final Class<E> 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()//
Expand Down
1 change: 1 addition & 0 deletions org.knime.python3.nodes/META-INF/MANIFEST.MF
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we do it like this, and don't add a dependency to the plugin org.knime.python3.processprovider? Then we can also get rid of all the NoClassDefFound checks

Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@
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.knime.python3.processprovider.PythonProcessProvider;
import org.yaml.snakeyaml.Yaml;

/**
Expand Down Expand Up @@ -91,7 +91,7 @@ static Stream<Path> getPathsToCustomExtensions() {
.map(Optional::get);
}

static Optional<PythonCommand> getCustomPythonCommand(final String extensionId) {
static Optional<PythonProcessProvider> getCustomPythonCommand(final String extensionId) {
return loadConfigs()//
.filter(e -> extensionId.equals(e.m_id))//
.findFirst()//
Expand Down Expand Up @@ -307,7 +307,7 @@ Optional<Path> getSrcPath() {
}
}

Optional<PythonCommand> getCommand() {
Optional<PythonProcessProvider> getCommand() {
if (m_condaEnvPath != null) {
if (m_pythonExecutable != null) {
LOGGER.warnWithFormat("Both conda_env_path and python_executable are provided for extension '%s'."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,14 @@
import org.knime.python3.BundledPythonCommand;
import org.knime.python3.FreshPythonGatewayFactory;
import org.knime.python3.Python3SourceDirectory;
import org.knime.python3.PythonCommand;
import org.knime.python3.PythonEntryPointUtils;
import org.knime.python3.PythonGateway;
import org.knime.python3.PythonGatewayFactory;
import org.knime.python3.PythonGatewayFactory.EntryPointCustomizer;
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;
Expand Down Expand Up @@ -141,12 +141,12 @@ public PythonGateway<KnimeNodeBackend> 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. "
Expand Down
2 changes: 2 additions & 0 deletions org.knime.python3.scripting.nodes/META-INF/MANIFEST.MF
Original file line number Diff line number Diff line change
Expand Up @@ -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.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)",
Expand All @@ -43,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
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +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.PixiPythonCommand;
import org.knime.pixi.port.PythonEnvironmentPortObject;
import org.knime.python2.PythonCommand;
import org.knime.python2.PythonModuleSpec;
import org.knime.python2.PythonVersion;
Expand All @@ -101,6 +103,7 @@
import org.knime.python2.ports.OutputPort;
import org.knime.python2.ports.PickledObjectOutputPort;
import org.knime.python2.ports.Port;
import org.knime.python3.processprovider.PythonProcessProvider;
import org.knime.python3.scripting.Python3KernelBackend;
import org.knime.python3.scripting.nodes.prefs.Python3ScriptingPreferences;

Expand Down Expand Up @@ -155,6 +158,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();
Expand All @@ -165,11 +170,12 @@ static void setExpectedOutputView(final PythonKernel kernel, final boolean expec
new AsynchronousCloseableTracker<>(t -> LOGGER.debug("Kernel shutdown failed.", t));

protected AbstractPythonScriptingNodeModel(final InputPort[] inPorts, final OutputPort[] outPorts,
final boolean hasView, final 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;
}
Expand All @@ -178,6 +184,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 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] = PythonEnvironmentPortObject.TYPE_OPTIONAL;
} catch (NoClassDefFoundError e) {
throw new IllegalStateException("Could not load PythonEnvironmentPortObject class", e);
}
return portTypes;
}

@Override
protected void saveSettingsTo(final NodeSettingsWO settings) {
saveScriptTo(m_script, settings);
Expand All @@ -198,14 +221,25 @@ 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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should drop the pixi port here, right?

for (int i = 0; i < numRegularPorts; i++) {
m_inPorts[i].configure(inSpecs[i]);
}
return null; // NOSONAR Conforms to KNIME API.
}

@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<PythonModuleSpec> requiredAdditionalModules = new HashSet<>();
for (int i = 0; i < m_inPorts.length; i++) {
Expand All @@ -217,7 +251,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<FlowVariable> inFlowVariables =
getAvailableFlowVariables(Python3KernelBackend.getCompatibleFlowVariableTypes()).values();
kernel.putFlowVariables(null, inFlowVariables);
Expand Down Expand Up @@ -349,10 +384,70 @@ private static Path persistedViewPath(final File nodeInternDir) {
return nodeInternDir.toPath().resolve("view.html");
}

/**
* Extract the Python command from a PythonEnvironmentPortObject.
*
* @param portObject the port object (may be null if optional port is not connected)
* @return the Python command, or null if the port is not connected or doesn't contain a valid Python executable
* @throws InvalidSettingsException if the Python executable path from the Pixi environment doesn't exist
*/
private static PythonCommand extractPythonCommandFromPixiPort(final PortObject portObject)
throws InvalidSettingsException {
if (portObject == null) {
return null;
}

try {
if (!(portObject instanceof PythonEnvironmentPortObject)) {
return null;
}

// Handle PythonEnvironmentPortObject
final PythonEnvironmentPortObject pythonEnvPort = (PythonEnvironmentPortObject)portObject;
final Path pixiTomlPath;
try {
pixiTomlPath = pythonEnvPort.getPixiEnvironmentPath().resolve("pixi.toml");
} catch (IOException e) {
throw new InvalidSettingsException("Failed to get pixi.toml path from PythonEnvironmentPortObject: " + e.getMessage(), e);
}

// Create PixiPythonCommand from the pixi.toml path
final PythonProcessProvider pythonCommand = new PixiPythonCommand(pixiTomlPath);

// Verify that the Python executable exists
final Path pythonExecPath = pythonCommand.getPythonExecutablePath();
if (!Files.exists(pythonExecPath)) {
throw new InvalidSettingsException(
"The Python executable from the Pixi environment does not exist at path: " + pythonExecPath
+ ". Please check that the Pixi environment was created successfully.");
}

LOGGER.debug("Using Python from Pixi environment via pixi run: " + pythonCommand);
return new LegacyPythonCommand(pythonCommand);

} catch (NoClassDefFoundError e) {
// Python environment bundle is not available - this is fine since it's optional
LOGGER.debug("PythonEnvironmentPortObject class not available - bundle may not be installed", e);
return null;
}
}

protected PythonKernel getNextKernelFromQueue(final Set<PythonModuleSpec> requiredAdditionalModules,
final Set<PythonModuleSpec> 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<PythonModuleSpec> requiredAdditionalModules,
final Set<PythonModuleSpec> 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);
}

Expand Down Expand Up @@ -381,14 +476,14 @@ 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.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;
}

Expand Down
Loading