From ff37963699230d129367897ed420fc3ad0a3ece3 Mon Sep 17 00:00:00 2001 From: Philipp Date: Thu, 2 Jul 2026 13:39:18 -0400 Subject: [PATCH 1/2] Fix pip installability: add missing nest_asyncio dep; relax cobrak pin `nest_asyncio` is imported unconditionally at startup (cnapy/application.py) but was only declared in environment.yml, not pyproject.toml, so wheel/sdist installs crashed on launch with ModuleNotFoundError. Add it to dependencies. Also relax the exact `cobrak==0.0.10` pin to `cobrak>=0.0.11` so the latest COBRA-k is accepted. NOTE: cobrak is still not published on PyPI, so this does not by itself make `pip install cnapy` succeed -- see the tracking issue for the cobrak distribution decision (publish to PyPI vs. make optional+lazy). --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 53404a4e..5aed2607 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,8 @@ dependencies = [ "openpyxl", "jpype1==1.5.0", "setuptools", - "cobrak==0.0.10", + "nest_asyncio", + "cobrak>=0.0.11", ] [project.scripts] From 0d84d68ef9b372c88aea11ba51f73bbcf2a7c467 Mon Sep 17 00:00:00 2001 From: Philipp Date: Thu, 2 Jul 2026 13:48:36 -0400 Subject: [PATCH 2/2] Make cobrak (COBRA-k) an optional dependency so `pip install cnapy` works cobrak was a hard dependency imported eagerly at startup (main_window -> thermodynamics_dialog), so a plain `pip install cnapy` failed to resolve it (cobrak is not currently on PyPI) and, if forced, the app could not even launch. - Move cobrak from [project.dependencies] to [project.optional-dependencies] as the `thermodynamics` extra (`pip install cnapy[thermodynamics]`). - Import cobrak lazily in thermodynamics_dialog via try/except, exposing COBRAK_AVAILABLE / COBRAK_IMPORT_ERROR. Behaviour is unchanged when cobrak is present. - When cobrak is absent, grey out the three Thermodynamic-analyses menu entries (OptMDFpathway, Thermodynamic FBA, bottleneck analysis) with an explanatory tooltip; the perform_* handlers also guard defensively. Verified: with cobrak present, imports/behaviour are identical; with cobrak absent (simulated), all modules import and CNApy starts with those menu entries greyed out instead of crashing. A clean-room `pip install cnapy` (no cobrak) succeeds and the GUI launches. --- cnapy/gui_elements/main_window.py | 44 ++++++++++++++++++++- cnapy/gui_elements/thermodynamics_dialog.py | 21 +++++++--- pyproject.toml | 8 +++- 3 files changed, 66 insertions(+), 7 deletions(-) diff --git a/cnapy/gui_elements/main_window.py b/cnapy/gui_elements/main_window.py index 84e49e9f..c17cab82 100644 --- a/cnapy/gui_elements/main_window.py +++ b/cnapy/gui_elements/main_window.py @@ -46,7 +46,12 @@ from cnapy.gui_elements.configuration_cplex import CplexConfigurationDialog from cnapy.gui_elements.configuration_cplex_new import CplexNewConfigurationDialog from cnapy.gui_elements.configuration_gurobi import GurobiConfigurationDialog -from cnapy.gui_elements.thermodynamics_dialog import ThermodynamicAnalysisTypes, ThermodynamicDialog +from cnapy.gui_elements.thermodynamics_dialog import ( + ThermodynamicAnalysisTypes, + ThermodynamicDialog, + COBRAK_AVAILABLE, + COBRAK_IMPORT_ERROR, +) import cnapy.utils as utils SBML_suffixes = "*.xml *.sbml *.xml.gz *.sbml.gz *.xml.zip *.sbml.zip" @@ -424,6 +429,18 @@ def __init__(self, appdata: AppData, project_path: str | None, scenario_path: st bottleneck_action.triggered.connect(self.perform_bottleneck_analysis) self.thermodynamic_menu.addAction(bottleneck_action) + # These three analyses require the optional COBRA-k package. When it is not + # installed, grey the entries out and explain via tooltip instead of letting + # the user open a dialog that cannot run. (perform_* still guard defensively.) + if not COBRAK_AVAILABLE: + for _cobrak_action in (optmdf_action, tfba_action, bottleneck_action): + _cobrak_action.setEnabled(False) + _cobrak_action.setToolTip( + "Requires the optional COBRA-k package " + "(install with: pip install cnapy[thermodynamics])" + ) + self.thermodynamic_menu.setToolTipsVisible(True) + self.thermodynamic_menu.addSeparator() dG0_menu = self.thermodynamic_menu.addMenu("Load dG'° values [in kJ/mol] (replacing all current values)...") @@ -2163,8 +2180,29 @@ def set_status_unknown(self): self.solver_status_symbol.setStyleSheet("color: black") self.solver_status_symbol.setText("?") + def _require_cobrak(self) -> bool: + """Warn (and return False) if the optional COBRA-k package is missing. + + The thermodynamic methods depend on cobrak, which is an optional + dependency. Guarding here lets CNApy run even when it is not installed. + """ + if COBRAK_AVAILABLE: + return True + QMessageBox.warning( + self, + "COBRA-k not installed", + "The thermodynamic analyses require the optional 'cobrak' (COBRA-k) " + "package, which is not installed.\n\n" + "Install it with 'pip install cnapy[thermodynamics]' or from " + "https://github.com/klamt-lab/COBRA-k\n\n" + f"Import error: {COBRAK_IMPORT_ERROR}", + ) + return False + @Slot() def perform_optmdfpathway(self): + if not self._require_cobrak(): + return # Has to be in self to keep computation thread self.optmdfpathway_dialog = ThermodynamicDialog( self.appdata, @@ -2175,6 +2213,8 @@ def perform_optmdfpathway(self): @Slot() def perform_thermodynamic_fba(self): + if not self._require_cobrak(): + return # Has to be in self to keep computation thread self.thermodynamic_fba_dialog = ThermodynamicDialog( self.appdata, @@ -2185,6 +2225,8 @@ def perform_thermodynamic_fba(self): @Slot() def perform_bottleneck_analysis(self): + if not self._require_cobrak(): + return # Has to be in self to keep computation thread self.bottleneck_dialog = ThermodynamicDialog( self.appdata, diff --git a/cnapy/gui_elements/thermodynamics_dialog.py b/cnapy/gui_elements/thermodynamics_dialog.py index 5c7ac496..f7d5cca4 100644 --- a/cnapy/gui_elements/thermodynamics_dialog.py +++ b/cnapy/gui_elements/thermodynamics_dialog.py @@ -18,11 +18,22 @@ from cnapy.gui_elements.central_widget import CentralWidget from cnapy.gui_elements.solver_buttons import get_solver_buttons from enum import Enum -from cobrak.constants import LNCONC_VAR_PREFIX, DF_VAR_PREFIX, MDF_VAR_ID, ALL_OK_KEY, OBJECTIVE_VAR_NAME, TERMINATION_CONDITION_KEY -from cobrak.dataclasses import ExtraLinearConstraint, Solver -from cobrak.lps import perform_lp_optimization, perform_lp_thermodynamic_bottleneck_analysis -from cobrak.io import load_annotated_cobrapy_model_as_cobrak_model -from cobrak.cobrapy_model_functionality import get_fullsplit_cobra_model +# cobrak (COBRA-k) powers the thermodynamic methods but is treated as an optional +# dependency so CNApy can be installed and started even when cobrak is unavailable +# in the environment. Import it lazily and gate the "Thermodynamic analyses" menu +# actions on COBRAK_AVAILABLE (see main_window.py). Enable the feature with +# `pip install cnapy[thermodynamics]`. +try: + from cobrak.constants import LNCONC_VAR_PREFIX, DF_VAR_PREFIX, MDF_VAR_ID, ALL_OK_KEY, OBJECTIVE_VAR_NAME, TERMINATION_CONDITION_KEY + from cobrak.dataclasses import ExtraLinearConstraint, Solver + from cobrak.lps import perform_lp_optimization, perform_lp_thermodynamic_bottleneck_analysis + from cobrak.io import load_annotated_cobrapy_model_as_cobrak_model + from cobrak.cobrapy_model_functionality import get_fullsplit_cobra_model + COBRAK_AVAILABLE = True + COBRAK_IMPORT_ERROR = "" +except ModuleNotFoundError as _cobrak_err: + COBRAK_AVAILABLE = False + COBRAK_IMPORT_ERROR = str(_cobrak_err) diff --git a/pyproject.toml b/pyproject.toml index 5aed2607..cfca441d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,9 +35,15 @@ dependencies = [ "jpype1==1.5.0", "setuptools", "nest_asyncio", - "cobrak>=0.0.11", ] +[project.optional-dependencies] +# COBRA-k powers the thermodynamic analyses. It is kept as an optional extra rather +# than a hard dependency so that a plain `pip install cnapy` always succeeds and the +# GUI starts, even if cobrak cannot be installed in a given environment. Enable the +# feature with `pip install cnapy[thermodynamics]`. +thermodynamics = ["cobrak>=0.0.11"] + [project.scripts] cnapy = "cnapy.__main__:main_cnapy" [project.urls]