From 37cac9f06d3e2ab5e38ceafa087703938b6ab09b Mon Sep 17 00:00:00 2001 From: Phil Chiu Date: Fri, 26 Sep 2025 14:16:21 -0600 Subject: [PATCH 01/12] Improve conditional imports --- .gitignore | 2 ++ pyoptsparse/pyCONMIN/pyCONMIN.py | 4 +-- pyoptsparse/pyIPOPT/pyIPOPT.py | 14 ++++------ pyoptsparse/pyNLPQLP/pyNLPQLP.py | 8 +++--- pyoptsparse/pyNSGA2/pyNSGA2.py | 8 +++--- pyoptsparse/pyOpt_utils.py | 46 +++++++++++++++++--------------- pyoptsparse/pyPSQP/pyPSQP.py | 8 +++--- pyoptsparse/pyParOpt/ParOpt.py | 36 +++++++++++++------------ pyoptsparse/pySLSQP/pySLSQP.py | 8 +++--- pyoptsparse/pySNOPT/pySNOPT.py | 9 +++---- tests/test_other.py | 9 +++---- 11 files changed, 76 insertions(+), 76 deletions(-) diff --git a/.gitignore b/.gitignore index 079a50341..858c91cfe 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,5 @@ pyoptsparse/pyNLPQLP/source *.pdb *.pyd + +.DS_Store diff --git a/pyoptsparse/pyCONMIN/pyCONMIN.py b/pyoptsparse/pyCONMIN/pyCONMIN.py index f5c598001..a39034619 100644 --- a/pyoptsparse/pyCONMIN/pyCONMIN.py +++ b/pyoptsparse/pyCONMIN/pyCONMIN.py @@ -13,11 +13,11 @@ # Local modules from ..pyOpt_optimizer import Optimizer -from ..pyOpt_utils import try_import_compiled_module_from_path +from ..pyOpt_utils import import_module # import the compiled module THIS_DIR = os.path.dirname(os.path.abspath(__file__)) -conmin = try_import_compiled_module_from_path("conmin", THIS_DIR, raise_warning=True) +conmin = import_module("conmin", [THIS_DIR]) class CONMIN(Optimizer): diff --git a/pyoptsparse/pyIPOPT/pyIPOPT.py b/pyoptsparse/pyIPOPT/pyIPOPT.py index 1ef57239c..09a96125c 100644 --- a/pyoptsparse/pyIPOPT/pyIPOPT.py +++ b/pyoptsparse/pyIPOPT/pyIPOPT.py @@ -10,16 +10,12 @@ # External modules import numpy as np -try: - # External modules - import cyipopt -except ImportError: - cyipopt = None - # Local modules from ..pyOpt_optimizer import Optimizer from ..pyOpt_solution import SolutionInform -from ..pyOpt_utils import ICOL, INFINITY, IROW, convertToCOO, extractRows, scaleRows +from ..pyOpt_utils import ICOL, INFINITY, IROW, convertToCOO, extractRows, import_module, scaleRows + +cyipopt = import_module("cyipopt") class IPOPT(Optimizer): @@ -36,8 +32,8 @@ def __init__(self, raiseError=True, options={}): category = "Local Optimizer" defOpts = self._getDefaultOptions() informs = self._getInforms() - if cyipopt is None and raiseError: - raise ImportError("Could not import cyipopt") + if isinstance(cyipopt, Exception) and raiseError: + raise cyipopt super().__init__( name, diff --git a/pyoptsparse/pyNLPQLP/pyNLPQLP.py b/pyoptsparse/pyNLPQLP/pyNLPQLP.py index 16efbb137..c7caec6e1 100644 --- a/pyoptsparse/pyNLPQLP/pyNLPQLP.py +++ b/pyoptsparse/pyNLPQLP/pyNLPQLP.py @@ -14,11 +14,11 @@ # Local modules from ..pyOpt_optimizer import Optimizer from ..pyOpt_solution import SolutionInform -from ..pyOpt_utils import try_import_compiled_module_from_path +from ..pyOpt_utils import import_module # import the compiled module THIS_DIR = os.path.dirname(os.path.abspath(__file__)) -nlpqlp = try_import_compiled_module_from_path("nlpqlp", THIS_DIR) +nlpqlp = import_module("nlpqlp", [THIS_DIR]) class NLPQLP(Optimizer): @@ -31,8 +31,8 @@ def __init__(self, raiseError=True, options={}): category = "Local Optimizer" defOpts = self._getDefaultOptions() informs = self._getInforms() - if isinstance(nlpqlp, str) and raiseError: - raise ImportError(nlpqlp) + if isinstance(nlpqlp, Exception) and raiseError: + raise nlpqlp super().__init__(name, category, defaultOptions=defOpts, informs=informs, options=options) # NLPQLP needs Jacobians in dense format diff --git a/pyoptsparse/pyNSGA2/pyNSGA2.py b/pyoptsparse/pyNSGA2/pyNSGA2.py index d55994c82..b6d8efab0 100644 --- a/pyoptsparse/pyNSGA2/pyNSGA2.py +++ b/pyoptsparse/pyNSGA2/pyNSGA2.py @@ -13,11 +13,11 @@ # Local modules from ..pyOpt_optimizer import Optimizer -from ..pyOpt_utils import try_import_compiled_module_from_path +from ..pyOpt_utils import import_module # import the compiled module THIS_DIR = os.path.dirname(os.path.abspath(__file__)) -nsga2 = try_import_compiled_module_from_path("nsga2", THIS_DIR, raise_warning=True) +nsga2 = import_module("nsga2", [THIS_DIR]) class NSGA2(Optimizer): @@ -32,8 +32,8 @@ def __init__(self, raiseError=True, options={}): informs = self._getInforms() super().__init__(name, category, defaultOptions=defOpts, informs=informs, options=options) - if isinstance(nsga2, str) and raiseError: - raise ImportError(nsga2) + if isinstance(nsga2, Exception) and raiseError: + raise nsga2 if self.getOption("PopSize") % 4 != 0: raise ValueError("Option 'PopSize' must be a multiple of 4") diff --git a/pyoptsparse/pyOpt_utils.py b/pyoptsparse/pyOpt_utils.py index e3572a036..c56ae6a55 100644 --- a/pyoptsparse/pyOpt_utils.py +++ b/pyoptsparse/pyOpt_utils.py @@ -14,7 +14,7 @@ import os import sys import types -from typing import Optional, Tuple, Union +from typing import Literal, Sequence, Tuple, Union import warnings # External modules @@ -361,9 +361,9 @@ def convertToCSC(mat: Union[dict, spmatrix, ndarray]) -> dict: def convertToDense(mat: Union[dict, spmatrix, ndarray]) -> ndarray: """ - Take a pyopsparse sparse matrix definition and convert back to a dense + Take a pyoptsparse sparse matrix definition and convert back to a dense format. This is typically the final step for optimizers with dense constraint - jacibians. + jacobians. Parameters ---------- @@ -576,40 +576,42 @@ def _broadcast_to_array(name: str, value: ArrayType, n_values: int, allow_none: return value -def try_import_compiled_module_from_path( - module_name: str, path: Optional[str] = None, raise_warning: bool = False -) -> Union[types.ModuleType, str]: +def import_module( + module_name: str, + path: Sequence[str] = (), + on_error: Literal["raise"] | Literal["return"] = "return", +) -> Union[types.ModuleType, Exception]: """ Attempt to import a module from a given path. Parameters ---------- module_name : str - The name of the module - path : Optional[str] - The path to import from. If None, the default ``sys.path`` is used. - raise_warning : bool - If true, raise an import warning. By default false. - + The name of the module. + path : Sequence[str] + The search path, which will be prepended to ``sys.path``. + on_error : str + Specify behavior on return. If "raise", any exception raised during the import will be raised. If "return", any + exception during the import will be returned. Returns ------- Union[types.ModuleType, str] If importable, the imported module is returned. - If not importable, the error message is instead returned. + If not importable, the exception is returned. """ + if on_error.lower() not in ("raise", "exception"): + raise ValueError("`on_error` must be 'raise' or 'exception'") orig_path = sys.path - if path is not None: - path = os.path.abspath(os.path.expandvars(os.path.expanduser(path))) - sys.path = [path] + if path: + path = [os.path.abspath(os.path.expandvars(os.path.expanduser(p))) for p in path] + sys.path = path + sys.path try: module = importlib.import_module(module_name) except ImportError as e: - if raise_warning: - warnings.warn( - f"{module_name} module could not be imported from {path}.", - stacklevel=2, - ) - module = str(e) + if on_error.lower() == "raise": + raise e + else: + module = e finally: sys.path = orig_path return module diff --git a/pyoptsparse/pyPSQP/pyPSQP.py b/pyoptsparse/pyPSQP/pyPSQP.py index 87bc156db..841421925 100644 --- a/pyoptsparse/pyPSQP/pyPSQP.py +++ b/pyoptsparse/pyPSQP/pyPSQP.py @@ -13,11 +13,11 @@ # Local modules from ..pyOpt_optimizer import Optimizer from ..pyOpt_solution import SolutionInform -from ..pyOpt_utils import try_import_compiled_module_from_path +from ..pyOpt_utils import import_module # import the compiled module THIS_DIR = os.path.dirname(os.path.abspath(__file__)) -psqp = try_import_compiled_module_from_path("psqp", THIS_DIR) +psqp = import_module("psqp", [THIS_DIR]) class PSQP(Optimizer): @@ -31,8 +31,8 @@ def __init__(self, raiseError=True, options={}): defOpts = self._getDefaultOptions() informs = self._getInforms() - if isinstance(psqp, str) and raiseError: - raise ImportError(psqp) + if isinstance(psqp, Exception) and raiseError: + raise psqp super().__init__(name, category, defaultOptions=defOpts, informs=informs, options=options) diff --git a/pyoptsparse/pyParOpt/ParOpt.py b/pyoptsparse/pyParOpt/ParOpt.py index ddb72bb32..05d6642b5 100644 --- a/pyoptsparse/pyParOpt/ParOpt.py +++ b/pyoptsparse/pyParOpt/ParOpt.py @@ -4,20 +4,22 @@ try: # External modules from paropt.paropt_pyoptsparse import ParOptSparse as ParOpt -except ImportError: - - class ParOpt(Optimizer): - def __init__(self, raiseError=True, options={}): - name = "ParOpt" - category = "Local Optimizer" - self.defOpts = {} - self.informs = {} - super().__init__( - name, - category, - defaultOptions=self.defOpts, - informs=self.informs, - options=options, - ) - if raiseError: - raise ImportError("There was an error importing ParOpt") +except ImportError as e: + def make_cls(e): + class ParOpt(Optimizer): + def __init__(self, raiseError=True, options={}): + name = "ParOpt" + category = "Local Optimizer" + self.defOpts = {} + self.informs = {} + super().__init__( + name, + category, + defaultOptions=self.defOpts, + informs=self.informs, + options=options, + ) + if raiseError: + raise e + return ParOpt + ParOpt = make_cls(e) \ No newline at end of file diff --git a/pyoptsparse/pySLSQP/pySLSQP.py b/pyoptsparse/pySLSQP/pySLSQP.py index 15908b4a0..1bdf9379a 100644 --- a/pyoptsparse/pySLSQP/pySLSQP.py +++ b/pyoptsparse/pySLSQP/pySLSQP.py @@ -15,11 +15,11 @@ from ..pyOpt_error import pyOptSparseWarning from ..pyOpt_optimizer import Optimizer from ..pyOpt_solution import SolutionInform -from ..pyOpt_utils import try_import_compiled_module_from_path +from ..pyOpt_utils import import_module # import the compiled module THIS_DIR = os.path.dirname(os.path.abspath(__file__)) -slsqp = try_import_compiled_module_from_path("slsqp", THIS_DIR, raise_warning=True) +slsqp = import_module("slsqp", [THIS_DIR]) class SLSQP(Optimizer): @@ -32,8 +32,8 @@ def __init__(self, raiseError=True, options={}): category = "Local Optimizer" defOpts = self._getDefaultOptions() informs = self._getInforms() - if isinstance(slsqp, str) and raiseError: - raise ImportError(slsqp) + if isinstance(slsqp, Exception) and raiseError: + raise slsqp self.set_options = [] super().__init__(name, category, defaultOptions=defOpts, informs=informs, options=options) diff --git a/pyoptsparse/pySNOPT/pySNOPT.py b/pyoptsparse/pySNOPT/pySNOPT.py index cfafd3ae5..997eeebdf 100644 --- a/pyoptsparse/pySNOPT/pySNOPT.py +++ b/pyoptsparse/pySNOPT/pySNOPT.py @@ -27,15 +27,14 @@ INFINITY, IROW, extractRows, + import_module, mapToCSC, scaleRows, - try_import_compiled_module_from_path, ) # import the compiled module THIS_DIR = os.path.dirname(os.path.abspath(__file__)) -_IMPORT_SNOPT_FROM = os.environ.get("PYOPTSPARSE_IMPORT_SNOPT_FROM", THIS_DIR) -snopt = try_import_compiled_module_from_path("snopt", _IMPORT_SNOPT_FROM) +snopt = import_module("snopt", [THIS_DIR]) class SNOPT(Optimizer): @@ -68,9 +67,9 @@ def __init__(self, raiseError=True, options: Dict = {}): informs = self._getInforms() - if isinstance(snopt, str): + if isinstance(snopt, Exception): if raiseError: - raise ImportError(snopt) + raise snopt else: version = None else: diff --git a/tests/test_other.py b/tests/test_other.py index eefd4b8f4..c7cc2bb5c 100644 --- a/tests/test_other.py +++ b/tests/test_other.py @@ -6,7 +6,7 @@ # First party modules from pyoptsparse import Optimizers, list_optimizers from pyoptsparse.pyOpt_solution import SolutionInform -from pyoptsparse.pyOpt_utils import try_import_compiled_module_from_path +from pyoptsparse.pyOpt_utils import import_module # we have to unset this environment variable because otherwise # the snopt module gets automatically imported, thus failing the import test below @@ -19,13 +19,12 @@ def test_nonexistent_path(self): for key in list(sys.modules.keys()): if "snopt" in key: sys.modules.pop(key) - with self.assertWarns(UserWarning): - module = try_import_compiled_module_from_path("snopt", "/a/nonexistent/path", raise_warning=True) - self.assertTrue(isinstance(module, str)) + with self.assertRaises(ImportError): + module = import_module("snopt", "/a/nonexistent/path", on_error="raise") def test_sys_path_unchanged(self): path = tuple(sys.path) - try_import_compiled_module_from_path("snopt", "/some/path") + import_module("snopt", "/some/path") self.assertEqual(tuple(sys.path), path) From c63502029983bb36e289656730434f4942d39871 Mon Sep 17 00:00:00 2001 From: Phil Chiu Date: Fri, 26 Sep 2025 14:27:46 -0600 Subject: [PATCH 02/12] Add back env var handling for SNOPT --- pyoptsparse/pySNOPT/pySNOPT.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyoptsparse/pySNOPT/pySNOPT.py b/pyoptsparse/pySNOPT/pySNOPT.py index 997eeebdf..40828fd26 100644 --- a/pyoptsparse/pySNOPT/pySNOPT.py +++ b/pyoptsparse/pySNOPT/pySNOPT.py @@ -34,7 +34,8 @@ # import the compiled module THIS_DIR = os.path.dirname(os.path.abspath(__file__)) -snopt = import_module("snopt", [THIS_DIR]) +_IMPORT_SNOPT_FROM = os.environ.get("PYOPTSPARSE_IMPORT_SNOPT_FROM", None) or THIS_DIR +snopt = import_module("snopt", [_IMPORT_SNOPT_FROM]) class SNOPT(Optimizer): From 31c8ea870816e3e431d2b8e16da62feae48d9c38 Mon Sep 17 00:00:00 2001 From: Phil Chiu Date: Fri, 26 Sep 2025 14:55:57 -0600 Subject: [PATCH 03/12] Fix typehint --- pyoptsparse/pyOpt_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyoptsparse/pyOpt_utils.py b/pyoptsparse/pyOpt_utils.py index c56ae6a55..b3a298c76 100644 --- a/pyoptsparse/pyOpt_utils.py +++ b/pyoptsparse/pyOpt_utils.py @@ -579,7 +579,7 @@ def _broadcast_to_array(name: str, value: ArrayType, n_values: int, allow_none: def import_module( module_name: str, path: Sequence[str] = (), - on_error: Literal["raise"] | Literal["return"] = "return", + on_error: Literal["raise", "return"] = "return", ) -> Union[types.ModuleType, Exception]: """ Attempt to import a module from a given path. From 37c61b88cc7d7f6c5bc108fb8085eda455f94cf0 Mon Sep 17 00:00:00 2001 From: Phil Chiu Date: Fri, 26 Sep 2025 14:57:52 -0600 Subject: [PATCH 04/12] Fix value checking --- pyoptsparse/pyOpt_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyoptsparse/pyOpt_utils.py b/pyoptsparse/pyOpt_utils.py index b3a298c76..1e0627fb2 100644 --- a/pyoptsparse/pyOpt_utils.py +++ b/pyoptsparse/pyOpt_utils.py @@ -599,8 +599,8 @@ def import_module( If importable, the imported module is returned. If not importable, the exception is returned. """ - if on_error.lower() not in ("raise", "exception"): - raise ValueError("`on_error` must be 'raise' or 'exception'") + if on_error.lower() not in ("raise", "return"): + raise ValueError("`on_error` must be 'raise' or 'return'") orig_path = sys.path if path: path = [os.path.abspath(os.path.expandvars(os.path.expanduser(p))) for p in path] From af8016e3f8e734e22a3e3a1f9347ef2cde197b7a Mon Sep 17 00:00:00 2001 From: Phil Chiu Date: Fri, 3 Oct 2025 15:03:20 -0700 Subject: [PATCH 05/12] Permit strs --- pyoptsparse/pyOpt_utils.py | 17 +++++++++++------ tests/test_other.py | 4 ++-- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/pyoptsparse/pyOpt_utils.py b/pyoptsparse/pyOpt_utils.py index 1e0627fb2..4880e0ed1 100644 --- a/pyoptsparse/pyOpt_utils.py +++ b/pyoptsparse/pyOpt_utils.py @@ -578,7 +578,7 @@ def _broadcast_to_array(name: str, value: ArrayType, n_values: int, allow_none: def import_module( module_name: str, - path: Sequence[str] = (), + path: Union[str, Sequence[str]] = (), on_error: Literal["raise", "return"] = "return", ) -> Union[types.ModuleType, Exception]: """ @@ -588,11 +588,12 @@ def import_module( ---------- module_name : str The name of the module. - path : Sequence[str] - The search path, which will be prepended to ``sys.path``. + path : Union[str, Sequence[str]] + The search path, which will be prepended to ``sys.path``. May be a string, or a sequence of strings. on_error : str - Specify behavior on return. If "raise", any exception raised during the import will be raised. If "return", any - exception during the import will be returned. + Specify behavior when import fails. If "raise", any exception raised during the import will be raised. + If "return", any exception during the import will be returned. + Returns ------- Union[types.ModuleType, str] @@ -600,7 +601,11 @@ def import_module( If not importable, the exception is returned. """ if on_error.lower() not in ("raise", "return"): - raise ValueError("`on_error` must be 'raise' or 'return'") + raise ValueError("`on_error` must be 'raise' or 'return'.") + + if isinstance(path, str): + path = [path] + orig_path = sys.path if path: path = [os.path.abspath(os.path.expandvars(os.path.expanduser(p))) for p in path] diff --git a/tests/test_other.py b/tests/test_other.py index c7cc2bb5c..1577b3308 100644 --- a/tests/test_other.py +++ b/tests/test_other.py @@ -20,11 +20,11 @@ def test_nonexistent_path(self): if "snopt" in key: sys.modules.pop(key) with self.assertRaises(ImportError): - module = import_module("snopt", "/a/nonexistent/path", on_error="raise") + module = import_module("snopt", ["/a/nonexistent/path"], on_error="raise") def test_sys_path_unchanged(self): path = tuple(sys.path) - import_module("snopt", "/some/path") + import_module("snopt", ["/some/path"]) self.assertEqual(tuple(sys.path), path) From f51bda9143aa5bc4e641a9019b180a91f7ed0e5a Mon Sep 17 00:00:00 2001 From: Alasdair Gray Date: Mon, 6 Oct 2025 12:40:23 -0400 Subject: [PATCH 06/12] Formatting --- pyoptsparse/pyParOpt/ParOpt.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyoptsparse/pyParOpt/ParOpt.py b/pyoptsparse/pyParOpt/ParOpt.py index 05d6642b5..b2f97ae54 100644 --- a/pyoptsparse/pyParOpt/ParOpt.py +++ b/pyoptsparse/pyParOpt/ParOpt.py @@ -5,6 +5,7 @@ # External modules from paropt.paropt_pyoptsparse import ParOptSparse as ParOpt except ImportError as e: + def make_cls(e): class ParOpt(Optimizer): def __init__(self, raiseError=True, options={}): @@ -21,5 +22,7 @@ def __init__(self, raiseError=True, options={}): ) if raiseError: raise e + return ParOpt - ParOpt = make_cls(e) \ No newline at end of file + + ParOpt = make_cls(e) From 3807ebf1ae0f2dc31ca1359fb2969a07cab607ed Mon Sep 17 00:00:00 2001 From: Alasdair Gray Date: Mon, 6 Oct 2025 12:41:18 -0400 Subject: [PATCH 07/12] Linting --- tests/test_other.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_other.py b/tests/test_other.py index 1577b3308..875d309e4 100644 --- a/tests/test_other.py +++ b/tests/test_other.py @@ -20,7 +20,7 @@ def test_nonexistent_path(self): if "snopt" in key: sys.modules.pop(key) with self.assertRaises(ImportError): - module = import_module("snopt", ["/a/nonexistent/path"], on_error="raise") + import_module("snopt", ["/a/nonexistent/path"], on_error="raise") def test_sys_path_unchanged(self): path = tuple(sys.path) From bb6bd3fde757be3cc9653d73da92cb68671342b4 Mon Sep 17 00:00:00 2001 From: Phil Chiu Date: Mon, 6 Oct 2025 14:42:09 -0700 Subject: [PATCH 08/12] Add better tests --- tests/test_other.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/test_other.py b/tests/test_other.py index 875d309e4..f39eda5f9 100644 --- a/tests/test_other.py +++ b/tests/test_other.py @@ -22,9 +22,21 @@ def test_nonexistent_path(self): with self.assertRaises(ImportError): import_module("snopt", ["/a/nonexistent/path"], on_error="raise") + def test_import_standard(self): + loaded = import_module("os") + assert loaded.__name__ == "os" + + def test_import_nonexistent(self): + with self.assertRaises(ImportError): + _ = import_module("not_a_module", on_error="raise") + + e = import_module("not_a_module", on_error="return") + assert isinstance(e, Exception) + assert "No module" in str(e) + def test_sys_path_unchanged(self): path = tuple(sys.path) - import_module("snopt", ["/some/path"]) + import_module("somemodule", ["/some/path"]) self.assertEqual(tuple(sys.path), path) From 1803a56275fa989cef5e09106efe6773ea25aa42 Mon Sep 17 00:00:00 2001 From: Phil Chiu Date: Mon, 6 Oct 2025 14:44:51 -0700 Subject: [PATCH 09/12] Use context manager for prepend_path --- pyoptsparse/pyOpt_utils.py | 40 ++++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/pyoptsparse/pyOpt_utils.py b/pyoptsparse/pyOpt_utils.py index 6970e1ff3..a933a557c 100644 --- a/pyoptsparse/pyOpt_utils.py +++ b/pyoptsparse/pyOpt_utils.py @@ -8,8 +8,8 @@ mat = {"csr": [rowp, colind, data], "shape": [nrow, ncols]} # A csr matrix mat = {"csc": [colp, rowind, data], "shape": [nrow, ncols]} # A csc matrix """ - # Standard Python modules +import contextlib import importlib import os import sys @@ -576,6 +576,20 @@ def _broadcast_to_array(name: str, value: ArrayType, n_values: int, allow_none: return value +@contextlib.contextmanager +def _prepend_path(path: Union[str, Sequence[str]]): + """Context manager which temporarily prepends to `sys.path`.""" + if isinstance(path, str): + path = [path] + orig_path = sys.path + if path: + path = [os.path.abspath(os.path.expandvars(os.path.expanduser(p))) for p in path] + sys.path = path + sys.path + yield + sys.path = orig_path + return + + def import_module( module_name: str, path: Union[str, Sequence[str]] = (), @@ -603,20 +617,12 @@ def import_module( if on_error.lower() not in ("raise", "return"): raise ValueError("`on_error` must be 'raise' or 'return'.") - if isinstance(path, str): - path = [path] - - orig_path = sys.path - if path: - path = [os.path.abspath(os.path.expandvars(os.path.expanduser(p))) for p in path] - sys.path = path + sys.path - try: - module = importlib.import_module(module_name) - except ImportError as e: - if on_error.lower() == "raise": - raise e - else: - module = e - finally: - sys.path = orig_path + with _prepend_path(path): + try: + module = importlib.import_module(module_name) + except ImportError as e: + if on_error.lower() == "raise": + raise e + else: + module = e return module From 553d8cc1e4adf501cb2cd0808ee8d04994e8c595 Mon Sep 17 00:00:00 2001 From: Phil Chiu Date: Mon, 6 Oct 2025 14:47:57 -0700 Subject: [PATCH 10/12] Fix error check/raise in pyCONMIN --- pyoptsparse/pyCONMIN/pyCONMIN.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyoptsparse/pyCONMIN/pyCONMIN.py b/pyoptsparse/pyCONMIN/pyCONMIN.py index a39034619..a1047f6f6 100644 --- a/pyoptsparse/pyCONMIN/pyCONMIN.py +++ b/pyoptsparse/pyCONMIN/pyCONMIN.py @@ -30,8 +30,8 @@ def __init__(self, raiseError=True, options={}): category = "Local Optimizer" defOpts = self._getDefaultOptions() informs = self._getInforms() - if isinstance(conmin, str) and raiseError: - raise ImportError(conmin) + if isinstance(conmin, Exception) and raiseError: + raise conmin self.set_options = [] super().__init__(name, category, defaultOptions=defOpts, informs=informs, options=options) From 4e5ab3ff0637e1b9c9d2fa4ee618189d547c58cd Mon Sep 17 00:00:00 2001 From: Ella Wu <602725+ewu63@users.noreply.github.com> Date: Tue, 7 Oct 2025 23:20:23 -0700 Subject: [PATCH 11/12] Update pyOpt_utils.py --- pyoptsparse/pyOpt_utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyoptsparse/pyOpt_utils.py b/pyoptsparse/pyOpt_utils.py index a933a557c..1af2a3893 100644 --- a/pyoptsparse/pyOpt_utils.py +++ b/pyoptsparse/pyOpt_utils.py @@ -8,6 +8,7 @@ mat = {"csr": [rowp, colind, data], "shape": [nrow, ncols]} # A csr matrix mat = {"csc": [colp, rowind, data], "shape": [nrow, ncols]} # A csc matrix """ + # Standard Python modules import contextlib import importlib From e7c46aca4ad3534a3fa82733fd7b95df01592bcb Mon Sep 17 00:00:00 2001 From: Ella Wu <602725+ewu63@users.noreply.github.com> Date: Wed, 8 Oct 2025 00:45:05 -0700 Subject: [PATCH 12/12] version bump --- pyoptsparse/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyoptsparse/__init__.py b/pyoptsparse/__init__.py index 0e581ce2f..184c7b208 100644 --- a/pyoptsparse/__init__.py +++ b/pyoptsparse/__init__.py @@ -1,4 +1,4 @@ -__version__ = "2.14.2" +__version__ = "2.14.3" from .pyOpt_history import History from .pyOpt_variable import Variable