From 701fa9065338f19a6433f047a12e558bc04f6a37 Mon Sep 17 00:00:00 2001 From: Ella Wu <602725+nwu63@users.noreply.github.com> Date: Sat, 16 Mar 2024 20:57:44 -0700 Subject: [PATCH 01/13] add util for importing module --- pyoptsparse/pyOpt_utils.py | 37 +++++++++++++++++++++++++++++++++++++ tests/test_other.py | 11 +++-------- 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/pyoptsparse/pyOpt_utils.py b/pyoptsparse/pyOpt_utils.py index e67eabb7..bbcb9836 100644 --- a/pyoptsparse/pyOpt_utils.py +++ b/pyoptsparse/pyOpt_utils.py @@ -9,6 +9,10 @@ mat = {'csc':[colp, rowind, data], 'shape':[nrow, ncols]} # A csc matrix """ # Standard Python modules +import importlib +import os +import sys +import types from typing import Tuple, Union import warnings @@ -570,3 +574,36 @@ def _broadcast_to_array(name: str, value: ArrayType, n_values: int, allow_none: if not allow_none and any([i is None for i in value]): raise Error(f"The {name} argument cannot be 'None'.") return value + +def try_import_compiled_module_from_path(module_name: str, path: str) -> types.ModuleType | str: + """ + Attempt to import a module from a given path. + + Parameters + ---------- + module_name : str + The name of the module + path : str + The path to import from + + Returns + ------- + types.ModuleType | str + If importable, the imported module is returned. + If not importable, the error message is instead returned. + """ + path = os.path.abspath(os.path.expandvars(os.path.expanduser(path))) + orig_path = sys.path + sys.path = [path] + try: + module = importlib.import_module(module_name) + except ImportError as e: + # warnings.warn( + # f"`{module_name}` module could not be imported from {path}.", + # ImportWarning, + # stacklevel=2, + # ) + module = str(e) + finally: + sys.path = orig_path + return module diff --git a/tests/test_other.py b/tests/test_other.py index 98d0996d..f0e7f90f 100644 --- a/tests/test_other.py +++ b/tests/test_other.py @@ -1,22 +1,17 @@ # Standard Python modules -import os import sys import unittest -# we have to unset this environment variable because otherwise when we import `_import_snopt_from_path` -# the snopt module gets automatically imported, thus failing the import test below -os.environ.pop("PYOPTSPARSE_IMPORT_SNOPT_FROM", None) - # First party modules -from pyoptsparse.pySNOPT.pySNOPT import _import_snopt_from_path # noqa: E402 +from pyoptsparse.pyOpt_utils import try_import_compiled_module_from_path # noqa: E402 class TestImportSnoptFromPath(unittest.TestCase): def test_nonexistent_path(self): with self.assertWarns(ImportWarning): - self.assertIsNone(_import_snopt_from_path("/a/nonexistent/path")) + self.assertIsNone(try_import_compiled_module_from_path("snopt", "/a/nonexistent/path")) def test_sys_path_unchanged(self): path = tuple(sys.path) - _import_snopt_from_path("/some/path") + try_import_compiled_module_from_path("snopt", "/some/path") self.assertEqual(tuple(sys.path), path) From a9fc02865d42a46ddba1d047e760893f6788eabe Mon Sep 17 00:00:00 2001 From: Ella Wu <602725+nwu63@users.noreply.github.com> Date: Sat, 16 Mar 2024 20:58:03 -0700 Subject: [PATCH 02/13] use util function for importing --- pyoptsparse/pySNOPT/pySNOPT.py | 52 +++++++++++----------------------- 1 file changed, 17 insertions(+), 35 deletions(-) diff --git a/pyoptsparse/pySNOPT/pySNOPT.py b/pyoptsparse/pySNOPT/pySNOPT.py index 249f4ddd..9c9feeb5 100644 --- a/pyoptsparse/pySNOPT/pySNOPT.py +++ b/pyoptsparse/pySNOPT/pySNOPT.py @@ -21,39 +21,21 @@ from ..pyOpt_error import Error from ..pyOpt_optimization import Optimization from ..pyOpt_optimizer import Optimizer -from ..pyOpt_utils import ICOL, IDATA, INFINITY, IROW, extractRows, mapToCSC, scaleRows - - -def _import_snopt_from_path(path): - """Attempt to import snopt from a specific path. Return the loaded module, or `None` if snopt cannot be imported.""" - path = os.path.abspath(os.path.expandvars(os.path.expanduser(path))) - orig_path = sys.path - sys.path = [path] - try: - import snopt # isort: skip - except ImportError: - warnings.warn( - f"`snopt` module could not be imported from {path}.", - ImportWarning, - stacklevel=2, - ) - snopt = None - finally: - sys.path = orig_path - return snopt - - -# Compiled module -_IMPORT_SNOPT_FROM = os.environ.get("PYOPTSPARSE_IMPORT_SNOPT_FROM", None) -if _IMPORT_SNOPT_FROM is not None: - # if a specific import path is specified, attempt to load SNOPT from it - snopt = _import_snopt_from_path(_IMPORT_SNOPT_FROM) -else: - # otherwise, load it relative to this file - try: - from . import snopt # isort: skip - except ImportError: - snopt = None +from ..pyOpt_utils import ( + ICOL, + IDATA, + INFINITY, + IROW, + extractRows, + 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) class SNOPT(Optimizer): @@ -84,9 +66,9 @@ def __init__(self, raiseError=True, options: Dict = {}): informs = self._getInforms() - if snopt is None: + if isinstance(snopt, str): if raiseError: - raise Error("There was an error importing the compiled snopt module") + raise ImportError(snopt) else: version = None else: From 352db3739bad15e8cbb4ffa62d1db51f151ba2a6 Mon Sep 17 00:00:00 2001 From: Ella Wu <602725+nwu63@users.noreply.github.com> Date: Sat, 16 Mar 2024 20:58:11 -0700 Subject: [PATCH 03/13] switch all import to util function --- pyoptsparse/pyALPSO/pyALPSO.py | 5 ++--- pyoptsparse/pyCONMIN/pyCONMIN.py | 15 +++++++-------- pyoptsparse/pyIPOPT/pyIPOPT.py | 26 ++++++++++++++++---------- pyoptsparse/pyNLPQLP/pyNLPQLP.py | 15 +++++++-------- pyoptsparse/pyNSGA2/pyNSGA2.py | 15 +++++++-------- pyoptsparse/pyPSQP/pyPSQP.py | 15 +++++++-------- pyoptsparse/pySLSQP/pySLSQP.py | 15 +++++++-------- 7 files changed, 53 insertions(+), 53 deletions(-) diff --git a/pyoptsparse/pyALPSO/pyALPSO.py b/pyoptsparse/pyALPSO/pyALPSO.py index 465728ca..f684e501 100644 --- a/pyoptsparse/pyALPSO/pyALPSO.py +++ b/pyoptsparse/pyALPSO/pyALPSO.py @@ -10,6 +10,7 @@ import numpy as np # Local modules +from . import alpso from ..pyOpt_error import Error from ..pyOpt_optimizer import Optimizer @@ -25,9 +26,7 @@ class ALPSO(Optimizer): - pll_type -> STR: ALPSO Parallel Implementation (None, SPM- Static, DPM- Dynamic, POA-Parallel Analysis), *Default* = None """ - def __init__(self, raiseError=True, options={}): - from . import alpso - + def __init__(self, options={}): self.alpso = alpso category = "Global Optimizer" diff --git a/pyoptsparse/pyCONMIN/pyCONMIN.py b/pyoptsparse/pyCONMIN/pyCONMIN.py index 21dd0c07..0fbac67d 100644 --- a/pyoptsparse/pyCONMIN/pyCONMIN.py +++ b/pyoptsparse/pyCONMIN/pyCONMIN.py @@ -2,11 +2,6 @@ pyCONMIN - A variation of the pyCONMIN wrapper specificially designed to work with sparse optimization problems. """ -# Compiled module -try: - from . import conmin # isort: skip -except ImportError: - conmin = None # Standard Python modules import datetime import os @@ -18,6 +13,11 @@ # Local modules from ..pyOpt_error import Error from ..pyOpt_optimizer import Optimizer +from ..pyOpt_utils import try_import_compiled_module_from_path + +# import the compiled module +THIS_DIR = os.path.dirname(os.path.abspath(__file__)) +conmin = try_import_compiled_module_from_path("conmin", THIS_DIR) class CONMIN(Optimizer): @@ -30,9 +30,8 @@ def __init__(self, raiseError=True, options={}): category = "Local Optimizer" defOpts = self._getDefaultOptions() informs = self._getInforms() - if conmin is None: - if raiseError: - raise Error("There was an error importing the compiled conmin module") + if isinstance(conmin, str) and raiseError: + raise ImportError(conmin) self.set_options = [] super().__init__(name, category, defaultOptions=defOpts, informs=informs, options=options) diff --git a/pyoptsparse/pyIPOPT/pyIPOPT.py b/pyoptsparse/pyIPOPT/pyIPOPT.py index f5233292..e9ff43a8 100644 --- a/pyoptsparse/pyIPOPT/pyIPOPT.py +++ b/pyoptsparse/pyIPOPT/pyIPOPT.py @@ -1,15 +1,11 @@ """ pyIPOPT - A python wrapper to the core IPOPT compiled module. """ -# Compiled module -try: - from . import pyipoptcore # isort: skip -except ImportError: - pyipoptcore = None # Standard Python modules import copy import datetime +import os import time # External modules @@ -18,8 +14,19 @@ # Local modules from ..pyOpt_error import Error from ..pyOpt_optimizer import Optimizer -from ..pyOpt_utils import ICOL, INFINITY, IROW, convertToCOO, extractRows, scaleRows - +from ..pyOpt_utils import ( + ICOL, + INFINITY, + IROW, + convertToCOO, + extractRows, + scaleRows, + try_import_compiled_module_from_path, +) + +# import the compiled module +THIS_DIR = os.path.dirname(os.path.abspath(__file__)) +pyipoptcore = try_import_compiled_module_from_path("pyipoptcore", THIS_DIR) class IPOPT(Optimizer): """ @@ -36,9 +43,8 @@ def __init__(self, raiseError=True, options={}): defOpts = self._getDefaultOptions() informs = self._getInforms() - if pyipoptcore is None: - if raiseError: - raise Error("There was an error importing the compiled IPOPT module") + if isinstance(pyipoptcore, str) and raiseError: + raise ImportError(pyipoptcore) super().__init__( name, diff --git a/pyoptsparse/pyNLPQLP/pyNLPQLP.py b/pyoptsparse/pyNLPQLP/pyNLPQLP.py index 348f8e4f..c9f0fff2 100644 --- a/pyoptsparse/pyNLPQLP/pyNLPQLP.py +++ b/pyoptsparse/pyNLPQLP/pyNLPQLP.py @@ -2,11 +2,6 @@ pyNLPQLP - A pyOptSparse wrapper for Schittkowski's NLPQLP optimization algorithm. """ -# Compiled module -try: - from . import nlpqlp # isort: skip -except ImportError: - nlpqlp = None # Standard Python modules import datetime import os @@ -18,6 +13,11 @@ # Local modules from ..pyOpt_error import Error from ..pyOpt_optimizer import Optimizer +from ..pyOpt_utils import try_import_compiled_module_from_path + +# import the compiled module +THIS_DIR = os.path.dirname(os.path.abspath(__file__)) +nlpqlp = try_import_compiled_module_from_path("nlpqlp", THIS_DIR) class NLPQLP(Optimizer): @@ -30,9 +30,8 @@ def __init__(self, raiseError=True, options={}): category = "Local Optimizer" defOpts = self._getDefaultOptions() informs = self._getInforms() - if nlpqlp is None: - if raiseError: - raise Error("There was an error importing the compiled nlpqlp module") + if isinstance(nlpqlp, str) and raiseError: + raise ImportError(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 125e3b0f..7d5a972e 100644 --- a/pyoptsparse/pyNSGA2/pyNSGA2.py +++ b/pyoptsparse/pyNSGA2/pyNSGA2.py @@ -2,12 +2,8 @@ pyNSGA2 - A variation of the pyNSGA2 wrapper specificially designed to work with sparse optimization problems. """ -# Compiled module -try: - from . import nsga2 # isort: skip -except ImportError: - nsga2 = None # Standard Python modules +import os import time # External modules @@ -16,7 +12,11 @@ # Local modules from ..pyOpt_error import Error from ..pyOpt_optimizer import Optimizer +from ..pyOpt_utils import try_import_compiled_module_from_path +# import the compiled module +THIS_DIR = os.path.dirname(os.path.abspath(__file__)) +nsga2 = try_import_compiled_module_from_path("nsga2", THIS_DIR) class NSGA2(Optimizer): """ @@ -30,9 +30,8 @@ def __init__(self, raiseError=True, options={}): informs = self._getInforms() super().__init__(name, category, defaultOptions=defOpts, informs=informs, options=options) - if nsga2 is None: - if raiseError: - raise Error("There was an error importing the compiled nsga2 module") + if isinstance(nsga2, str) and raiseError: + raise ImportError(nsga2) @staticmethod def _getInforms(): diff --git a/pyoptsparse/pyPSQP/pyPSQP.py b/pyoptsparse/pyPSQP/pyPSQP.py index ca495eb8..e69ed8de 100644 --- a/pyoptsparse/pyPSQP/pyPSQP.py +++ b/pyoptsparse/pyPSQP/pyPSQP.py @@ -1,11 +1,6 @@ """ pyPSQP - the pyPSQP wrapper """ -# Compiled module -try: - from . import psqp # isort: skip -except ImportError: - psqp = None # Standard Python modules import datetime import os @@ -17,6 +12,11 @@ # Local modules from ..pyOpt_error import Error from ..pyOpt_optimizer import Optimizer +from ..pyOpt_utils import try_import_compiled_module_from_path + +# import the compiled module +THIS_DIR = os.path.dirname(os.path.abspath(__file__)) +psqp = try_import_compiled_module_from_path("psqp", THIS_DIR) class PSQP(Optimizer): @@ -30,9 +30,8 @@ def __init__(self, raiseError=True, options={}): defOpts = self._getDefaultOptions() informs = self._getInforms() - if psqp is None: - if raiseError: - raise Error("There was an error importing the compiled psqp module") + if isinstance(psqp, str) and raiseError: + raise ImportError(psqp) super().__init__(name, category, defaultOptions=defOpts, informs=informs, options=options) diff --git a/pyoptsparse/pySLSQP/pySLSQP.py b/pyoptsparse/pySLSQP/pySLSQP.py index 58f3ee6e..e5ec91fa 100644 --- a/pyoptsparse/pySLSQP/pySLSQP.py +++ b/pyoptsparse/pySLSQP/pySLSQP.py @@ -2,11 +2,7 @@ pySLSQP - A variation of the pySLSQP wrapper specificially designed to work with sparse optimization problems. """ -# Compiled module -try: - from . import slsqp # isort: skip -except ImportError: - slsqp = None + # Standard Python modules import datetime import os @@ -18,7 +14,11 @@ # Local modules from ..pyOpt_error import Error from ..pyOpt_optimizer import Optimizer +from ..pyOpt_utils import try_import_compiled_module_from_path +# import the compiled module +THIS_DIR = os.path.dirname(os.path.abspath(__file__)) +slsqp = try_import_compiled_module_from_path("slsqp", THIS_DIR) class SLSQP(Optimizer): """ @@ -30,9 +30,8 @@ def __init__(self, raiseError=True, options={}): category = "Local Optimizer" defOpts = self._getDefaultOptions() informs = self._getInforms() - if slsqp is None: - if raiseError: - raise Error("There was an error importing the compiled slsqp module") + if isinstance(slsqp, str) and raiseError: + raise ImportError(slsqp) self.set_options = [] super().__init__(name, category, defaultOptions=defOpts, informs=informs, options=options) From 1fc3dfd62bc12397b4f8de8df2d797c05bbda807 Mon Sep 17 00:00:00 2001 From: Ella Wu <602725+nwu63@users.noreply.github.com> Date: Sat, 16 Mar 2024 21:25:12 -0700 Subject: [PATCH 04/13] update tests to catch ImportError --- pyoptsparse/pyOpt_utils.py | 20 +++++++++++--------- pyoptsparse/pyParOpt/ParOpt.py | 5 ++--- tests/test_other.py | 5 +++-- tests/test_snopt_bugfix.py | 18 ++++++------------ tests/test_user_termination.py | 12 ++++-------- tests/testing_utils.py | 2 +- 6 files changed, 27 insertions(+), 35 deletions(-) diff --git a/pyoptsparse/pyOpt_utils.py b/pyoptsparse/pyOpt_utils.py index bbcb9836..5c5826d8 100644 --- a/pyoptsparse/pyOpt_utils.py +++ b/pyoptsparse/pyOpt_utils.py @@ -575,7 +575,7 @@ def _broadcast_to_array(name: str, value: ArrayType, n_values: int, allow_none: raise Error(f"The {name} argument cannot be 'None'.") return value -def try_import_compiled_module_from_path(module_name: str, path: str) -> types.ModuleType | str: +def try_import_compiled_module_from_path(module_name: str, path: str|None = None) -> types.ModuleType | str: """ Attempt to import a module from a given path. @@ -583,8 +583,8 @@ def try_import_compiled_module_from_path(module_name: str, path: str) -> types.M ---------- module_name : str The name of the module - path : str - The path to import from + path : str | None + The path to import from. If None, the default ``sys.path`` is used. Returns ------- @@ -594,15 +594,17 @@ def try_import_compiled_module_from_path(module_name: str, path: str) -> types.M """ path = os.path.abspath(os.path.expandvars(os.path.expanduser(path))) orig_path = sys.path - sys.path = [path] + if path is not None: + sys.path = [path] try: module = importlib.import_module(module_name) except ImportError as e: - # warnings.warn( - # f"`{module_name}` module could not be imported from {path}.", - # ImportWarning, - # stacklevel=2, - # ) + if path is not None: + warnings.warn( + f"{module_name} module could not be imported from {path}.", + ImportWarning, + stacklevel=2, + ) module = str(e) finally: sys.path = orig_path diff --git a/pyoptsparse/pyParOpt/ParOpt.py b/pyoptsparse/pyParOpt/ParOpt.py index 3b64140b..46fb92f7 100644 --- a/pyoptsparse/pyParOpt/ParOpt.py +++ b/pyoptsparse/pyParOpt/ParOpt.py @@ -48,9 +48,8 @@ class ParOpt(Optimizer): def __init__(self, raiseError=True, options={}): name = "ParOpt" category = "Local Optimizer" - if _ParOpt is None: - if raiseError: - raise Error("There was an error importing ParOpt") + if _ParOpt is None and raiseError: + raise ImportError("There was an error importing ParOpt") # Create and fill-in the dictionary of default option values self.defOpts = {} diff --git a/tests/test_other.py b/tests/test_other.py index f0e7f90f..b9c47c7c 100644 --- a/tests/test_other.py +++ b/tests/test_other.py @@ -3,13 +3,14 @@ import unittest # First party modules -from pyoptsparse.pyOpt_utils import try_import_compiled_module_from_path # noqa: E402 +from pyoptsparse.pyOpt_utils import try_import_compiled_module_from_path class TestImportSnoptFromPath(unittest.TestCase): def test_nonexistent_path(self): with self.assertWarns(ImportWarning): - self.assertIsNone(try_import_compiled_module_from_path("snopt", "/a/nonexistent/path")) + module = try_import_compiled_module_from_path("snopt", "/a/nonexistent/path") + self.assertTrue(isinstance(module, str)) def test_sys_path_unchanged(self): path = tuple(sys.path) diff --git a/tests/test_snopt_bugfix.py b/tests/test_snopt_bugfix.py index e6460452..40b00d12 100644 --- a/tests/test_snopt_bugfix.py +++ b/tests/test_snopt_bugfix.py @@ -104,10 +104,8 @@ def test_opt(self): # Optimizer try: opt = SNOPT(options=optOptions) - except Error as e: - if "There was an error importing" in e.message: - raise unittest.SkipTest("Optimizer not available: SNOPT") - raise e + except ImportError: + raise unittest.SkipTest("Optimizer not available: SNOPT") sol = opt(optProb, sens=sens) @@ -137,10 +135,8 @@ def test_opt_bug1(self): # Optimizer try: opt = SNOPT(options=optOptions) - except Error as e: - if "There was an error importing" in e.message: - raise unittest.SkipTest("Optimizer not available: SNOPT") - raise e + except ImportError: + raise unittest.SkipTest("Optimizer not available: SNOPT") opt(optProb, sens=sens) @@ -180,10 +176,8 @@ def test_opt_bug_print_2con(self): # Optimizer try: opt = SNOPT(options=optOptions) - except Error as e: - if "There was an error importing" in e.message: - raise unittest.SkipTest("Optimizer not available: SNOPT") - raise e + except ImportError: + raise unittest.SkipTest("Optimizer not available: SNOPT") sol = opt(optProb, sens=sens) diff --git a/tests/test_user_termination.py b/tests/test_user_termination.py index ca40bad9..bf47f007 100644 --- a/tests/test_user_termination.py +++ b/tests/test_user_termination.py @@ -105,10 +105,8 @@ def test_obj(self, optName): try: opt = OPT(optName, options=optOptions) - except Error as e: - if "There was an error importing" in e.message: - raise unittest.SkipTest(f"Optimizer not available: {optName}") - raise e + except ImportError: + raise unittest.SkipTest(f"Optimizer not available: {optName}") sol = opt(optProb, sens=termcomp.sens) @@ -128,10 +126,8 @@ def test_sens(self, optName): try: opt = OPT(optName, options=optOptions) - except Error as e: - if "There was an error importing" in e.message: - raise unittest.SkipTest("Optimizer not available: SNOPT") - raise e + except ImportError: + raise unittest.SkipTest("Optimizer not available: SNOPT") sol = opt(optProb, sens=termcomp.sens) diff --git a/tests/testing_utils.py b/tests/testing_utils.py index 08017b5f..667a2942 100644 --- a/tests/testing_utils.py +++ b/tests/testing_utils.py @@ -231,7 +231,7 @@ def optimize(self, sens=None, setDV=None, optOptions=None, storeHistory=False, h try: opt = OPT(self.optName, options=optOptions) self.optVersion = opt.version - except Error as e: + except ImportError as e: if self.optName in DEFAULT_OPTIMIZERS: raise e else: From e72769ac1ffdec907816e8f6091effbaf04d754b Mon Sep 17 00:00:00 2001 From: Ella Wu <602725+nwu63@users.noreply.github.com> Date: Sat, 16 Mar 2024 21:34:51 -0700 Subject: [PATCH 05/13] cleanup paropt import --- pyoptsparse/pyIPOPT/pyIPOPT.py | 1 - pyoptsparse/pyOpt_utils.py | 2 +- pyoptsparse/pyParOpt/ParOpt.py | 38 ++++++++++++---------------------- pyoptsparse/pySLSQP/pySLSQP.py | 1 - tests/test_snopt_bugfix.py | 1 - tests/test_user_termination.py | 1 - tests/testing_utils.py | 1 - 7 files changed, 14 insertions(+), 31 deletions(-) diff --git a/pyoptsparse/pyIPOPT/pyIPOPT.py b/pyoptsparse/pyIPOPT/pyIPOPT.py index e9ff43a8..2e831678 100644 --- a/pyoptsparse/pyIPOPT/pyIPOPT.py +++ b/pyoptsparse/pyIPOPT/pyIPOPT.py @@ -12,7 +12,6 @@ import numpy as np # Local modules -from ..pyOpt_error import Error from ..pyOpt_optimizer import Optimizer from ..pyOpt_utils import ( ICOL, diff --git a/pyoptsparse/pyOpt_utils.py b/pyoptsparse/pyOpt_utils.py index 5c5826d8..05dd87d1 100644 --- a/pyoptsparse/pyOpt_utils.py +++ b/pyoptsparse/pyOpt_utils.py @@ -592,9 +592,9 @@ def try_import_compiled_module_from_path(module_name: str, path: str|None = None If importable, the imported module is returned. If not importable, the error message is instead returned. """ - path = os.path.abspath(os.path.expandvars(os.path.expanduser(path))) orig_path = sys.path if path is not None: + path = os.path.abspath(os.path.expandvars(os.path.expanduser(path))) sys.path = [path] try: module = importlib.import_module(module_name) diff --git a/pyoptsparse/pyParOpt/ParOpt.py b/pyoptsparse/pyParOpt/ParOpt.py index 46fb92f7..4198454e 100644 --- a/pyoptsparse/pyParOpt/ParOpt.py +++ b/pyoptsparse/pyParOpt/ParOpt.py @@ -6,35 +6,22 @@ # External modules import numpy as np -# isort: off -# Attempt to import mpi4py. +# Local modules +from ..pyOpt_optimizer import Optimizer +from ..pyOpt_utils import INFINITY, try_import_compiled_module_from_path + +# Attempt to import ParOpt/mpi4py # If PYOPTSPARSE_REQUIRE_MPI is set to a recognized positive value, attempt import # and raise exception on failure. If set to anything else, no import is attempted. -if "PYOPTSPARSE_REQUIRE_MPI" in os.environ: - if os.environ["PYOPTSPARSE_REQUIRE_MPI"].lower() in ["always", "1", "true", "yes"]: - try: - from paropt import ParOpt as _ParOpt - from mpi4py import MPI - except ImportError: - _ParOpt = None - else: - _ParOpt = None +if "PYOPTSPARSE_REQUIRE_MPI" in os.environ and os.environ["PYOPTSPARSE_REQUIRE_MPI"].lower() not in ["always", "1", "true", "yes"]: + _ParOpt = "ParOpt was not imported, as requested by the environment variable 'PYOPTSPARSE_REQUIRE_MPI'" + MPI = "mpi4py was not imported, as requested by the environment variable 'PYOPTSPARSE_REQUIRE_MPI'" # If PYOPTSPARSE_REQUIRE_MPI is unset, attempt to import mpi4py. # Since ParOpt requires mpi4py, if either _ParOpt or mpi4py is unavailable # we disable the optimizer. else: - try: - from paropt import ParOpt as _ParOpt - from mpi4py import MPI - except ImportError: - _ParOpt = None -# isort: on - -# Local modules -from ..pyOpt_error import Error -from ..pyOpt_optimizer import Optimizer -from ..pyOpt_utils import INFINITY - + _ParOpt = try_import_compiled_module_from_path("paropt.ParOpt") + MPI = try_import_compiled_module_from_path("mpi4py.MPI") class ParOpt(Optimizer): """ @@ -48,8 +35,9 @@ class ParOpt(Optimizer): def __init__(self, raiseError=True, options={}): name = "ParOpt" category = "Local Optimizer" - if _ParOpt is None and raiseError: - raise ImportError("There was an error importing ParOpt") + for mod in [_ParOpt, MPI]: + if isinstance(mod, str) and raiseError: + raise ImportError(mod) # Create and fill-in the dictionary of default option values self.defOpts = {} diff --git a/pyoptsparse/pySLSQP/pySLSQP.py b/pyoptsparse/pySLSQP/pySLSQP.py index e5ec91fa..e5a5eac1 100644 --- a/pyoptsparse/pySLSQP/pySLSQP.py +++ b/pyoptsparse/pySLSQP/pySLSQP.py @@ -12,7 +12,6 @@ import numpy as np # Local modules -from ..pyOpt_error import Error from ..pyOpt_optimizer import Optimizer from ..pyOpt_utils import try_import_compiled_module_from_path diff --git a/tests/test_snopt_bugfix.py b/tests/test_snopt_bugfix.py index 40b00d12..d3c8b598 100644 --- a/tests/test_snopt_bugfix.py +++ b/tests/test_snopt_bugfix.py @@ -12,7 +12,6 @@ # First party modules from pyoptsparse import SNOPT, Optimization -from pyoptsparse.pyOpt_error import Error def objfunc(xdict): diff --git a/tests/test_user_termination.py b/tests/test_user_termination.py index bf47f007..a857a9f1 100644 --- a/tests/test_user_termination.py +++ b/tests/test_user_termination.py @@ -14,7 +14,6 @@ # First party modules from pyoptsparse import OPT, Optimization -from pyoptsparse.pyOpt_error import Error class TerminateComp: diff --git a/tests/testing_utils.py b/tests/testing_utils.py index 667a2942..fce54c26 100644 --- a/tests/testing_utils.py +++ b/tests/testing_utils.py @@ -9,7 +9,6 @@ # First party modules from pyoptsparse import OPT, History -from pyoptsparse.pyOpt_error import Error def assert_optProb_size(optProb, nObj, nDV, nCon): From de95d6e8eda5f030f885be8ca026857987caabb9 Mon Sep 17 00:00:00 2001 From: Ella Wu <602725+nwu63@users.noreply.github.com> Date: Sat, 16 Mar 2024 22:59:59 -0700 Subject: [PATCH 06/13] do not use pipe character due to old python version --- pyoptsparse/pyOpt_utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyoptsparse/pyOpt_utils.py b/pyoptsparse/pyOpt_utils.py index 05dd87d1..2cf62feb 100644 --- a/pyoptsparse/pyOpt_utils.py +++ b/pyoptsparse/pyOpt_utils.py @@ -13,7 +13,7 @@ import os import sys import types -from typing import Tuple, Union +from typing import Optional, Tuple, Union import warnings # External modules @@ -575,7 +575,7 @@ def _broadcast_to_array(name: str, value: ArrayType, n_values: int, allow_none: raise Error(f"The {name} argument cannot be 'None'.") return value -def try_import_compiled_module_from_path(module_name: str, path: str|None = None) -> types.ModuleType | str: +def try_import_compiled_module_from_path(module_name: str, path: Optional[str] = None) -> Union[types.ModuleType, str]: """ Attempt to import a module from a given path. @@ -583,12 +583,12 @@ def try_import_compiled_module_from_path(module_name: str, path: str|None = None ---------- module_name : str The name of the module - path : str | None + path : Optional[str] The path to import from. If None, the default ``sys.path`` is used. Returns ------- - types.ModuleType | str + Union[types.ModuleType, str] If importable, the imported module is returned. If not importable, the error message is instead returned. """ From 91e8759c01d01a8291993c423466a9001b0be1b5 Mon Sep 17 00:00:00 2001 From: Ella Wu <602725+nwu63@users.noreply.github.com> Date: Sat, 16 Mar 2024 23:16:42 -0700 Subject: [PATCH 07/13] fix tests --- tests/test_other.py | 4 ++++ tests/test_require_mpi_env_var.py | 18 +++++++----------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/test_other.py b/tests/test_other.py index b9c47c7c..dcae4158 100644 --- a/tests/test_other.py +++ b/tests/test_other.py @@ -1,10 +1,14 @@ # Standard Python modules +import os import sys import unittest # First party modules from pyoptsparse.pyOpt_utils import try_import_compiled_module_from_path +# we have to unset this environment variable because otherwise +# the snopt module gets automatically imported, thus failing the import test below +os.environ.pop("PYOPTSPARSE_IMPORT_SNOPT_FROM", None) class TestImportSnoptFromPath(unittest.TestCase): def test_nonexistent_path(self): diff --git a/tests/test_require_mpi_env_var.py b/tests/test_require_mpi_env_var.py index 15e7ee25..e550cfe1 100644 --- a/tests/test_require_mpi_env_var.py +++ b/tests/test_require_mpi_env_var.py @@ -6,10 +6,6 @@ import unittest # isort: off -if sys.version_info[0] == 2: - reload_func = reload # noqa: F821 -else: - reload_func = importlib.reload try: HAS_MPI = True @@ -26,14 +22,14 @@ def test_require_mpi(self): os.environ["PYOPTSPARSE_REQUIRE_MPI"] = "1" import pyoptsparse.pyOpt_MPI - reload_func(pyoptsparse.pyOpt_MPI) + importlib.reload(pyoptsparse.pyOpt_MPI) self.assertTrue(inspect.ismodule(pyoptsparse.pyOpt_MPI.MPI)) def test_no_mpi_requirement_given(self): os.environ.pop("PYOPTSPARSE_REQUIRE_MPI", None) import pyoptsparse.pyOpt_MPI - reload_func(pyoptsparse.pyOpt_MPI) + importlib.reload(pyoptsparse.pyOpt_MPI) if HAS_MPI: self.assertTrue(inspect.ismodule(pyoptsparse.pyOpt_MPI.MPI)) else: @@ -43,7 +39,7 @@ def test_do_not_use_mpi(self): os.environ["PYOPTSPARSE_REQUIRE_MPI"] = "0" import pyoptsparse.pyOpt_MPI - reload_func(pyoptsparse.pyOpt_MPI) + importlib.reload(pyoptsparse.pyOpt_MPI) self.assertFalse(inspect.ismodule(pyoptsparse.pyOpt_MPI.MPI)) @@ -60,22 +56,22 @@ def test_require_mpi_check_paropt(self): os.environ["PYOPTSPARSE_REQUIRE_MPI"] = "1" import pyoptsparse.pyParOpt.ParOpt - reload_func(pyoptsparse.pyParOpt.ParOpt) + importlib.reload(pyoptsparse.pyParOpt.ParOpt) self.assertIsNotNone(pyoptsparse.pyParOpt.ParOpt._ParOpt) def test_no_mpi_requirement_given_check_paropt(self): os.environ.pop("PYOPTSPARSE_REQUIRE_MPI", None) import pyoptsparse.pyParOpt.ParOpt - reload_func(pyoptsparse.pyParOpt.ParOpt) + importlib.reload(pyoptsparse.pyParOpt.ParOpt) self.assertIsNotNone(pyoptsparse.pyParOpt.ParOpt._ParOpt) def test_do_not_use_mpi_check_paropt(self): os.environ["PYOPTSPARSE_REQUIRE_MPI"] = "0" import pyoptsparse.pyParOpt.ParOpt - reload_func(pyoptsparse.pyParOpt.ParOpt) - self.assertIsNone(pyoptsparse.pyParOpt.ParOpt._ParOpt) + importlib.reload(pyoptsparse.pyParOpt.ParOpt) + self.assertTrue(isinstance(pyoptsparse.pyParOpt.ParOpt._ParOpt, str)) if __name__ == "__main__": From 62460643a8d120e94ca211288a7e52e292f923a9 Mon Sep 17 00:00:00 2001 From: Ella Wu <602725+nwu63@users.noreply.github.com> Date: Sat, 16 Mar 2024 23:46:12 -0700 Subject: [PATCH 08/13] very hacky solution with sys.modules --- tests/test_other.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_other.py b/tests/test_other.py index dcae4158..b76b8ec3 100644 --- a/tests/test_other.py +++ b/tests/test_other.py @@ -12,6 +12,10 @@ class TestImportSnoptFromPath(unittest.TestCase): def test_nonexistent_path(self): + # first unload `snopt` from namespace + for key in sys.modules.keys(): + if "snopt" in key: + sys.modules.pop(key) with self.assertWarns(ImportWarning): module = try_import_compiled_module_from_path("snopt", "/a/nonexistent/path") self.assertTrue(isinstance(module, str)) From 7e916b26378a1a97ea81142882681d1bea7c2aac Mon Sep 17 00:00:00 2001 From: Ella Wu <602725+nwu63@users.noreply.github.com> Date: Sat, 16 Mar 2024 23:48:14 -0700 Subject: [PATCH 09/13] fix linting --- pyoptsparse/pySNOPT/pySNOPT.py | 2 -- tests/test_require_mpi_env_var.py | 1 - 2 files changed, 3 deletions(-) diff --git a/pyoptsparse/pySNOPT/pySNOPT.py b/pyoptsparse/pySNOPT/pySNOPT.py index 9c9feeb5..63185e33 100644 --- a/pyoptsparse/pySNOPT/pySNOPT.py +++ b/pyoptsparse/pySNOPT/pySNOPT.py @@ -6,10 +6,8 @@ import datetime import os import re -import sys import time from typing import Any, Dict, Optional, Tuple -import warnings # External modules from baseclasses.utils import CaseInsensitiveSet diff --git a/tests/test_require_mpi_env_var.py b/tests/test_require_mpi_env_var.py index e550cfe1..ca95c0ed 100644 --- a/tests/test_require_mpi_env_var.py +++ b/tests/test_require_mpi_env_var.py @@ -2,7 +2,6 @@ import importlib import inspect import os -import sys import unittest # isort: off From 0b1714de6e016a389b458fc535ecb37ebaf4f631 Mon Sep 17 00:00:00 2001 From: Ella Wu <602725+nwu63@users.noreply.github.com> Date: Sat, 16 Mar 2024 23:48:32 -0700 Subject: [PATCH 10/13] black --- pyoptsparse/pyIPOPT/pyIPOPT.py | 1 + pyoptsparse/pyNSGA2/pyNSGA2.py | 1 + pyoptsparse/pyOpt_utils.py | 1 + pyoptsparse/pyParOpt/ParOpt.py | 8 +++++++- pyoptsparse/pySLSQP/pySLSQP.py | 1 + tests/test_other.py | 1 + 6 files changed, 12 insertions(+), 1 deletion(-) diff --git a/pyoptsparse/pyIPOPT/pyIPOPT.py b/pyoptsparse/pyIPOPT/pyIPOPT.py index 2e831678..b009a4a4 100644 --- a/pyoptsparse/pyIPOPT/pyIPOPT.py +++ b/pyoptsparse/pyIPOPT/pyIPOPT.py @@ -27,6 +27,7 @@ THIS_DIR = os.path.dirname(os.path.abspath(__file__)) pyipoptcore = try_import_compiled_module_from_path("pyipoptcore", THIS_DIR) + class IPOPT(Optimizer): """ IPOPT Optimizer Class - Inherited from Optimizer Abstract Class diff --git a/pyoptsparse/pyNSGA2/pyNSGA2.py b/pyoptsparse/pyNSGA2/pyNSGA2.py index 7d5a972e..173703e9 100644 --- a/pyoptsparse/pyNSGA2/pyNSGA2.py +++ b/pyoptsparse/pyNSGA2/pyNSGA2.py @@ -18,6 +18,7 @@ THIS_DIR = os.path.dirname(os.path.abspath(__file__)) nsga2 = try_import_compiled_module_from_path("nsga2", THIS_DIR) + class NSGA2(Optimizer): """ NSGA2 Optimizer Class - Inherited from Optimizer Abstract Class diff --git a/pyoptsparse/pyOpt_utils.py b/pyoptsparse/pyOpt_utils.py index 2cf62feb..22fe84fa 100644 --- a/pyoptsparse/pyOpt_utils.py +++ b/pyoptsparse/pyOpt_utils.py @@ -575,6 +575,7 @@ def _broadcast_to_array(name: str, value: ArrayType, n_values: int, allow_none: raise Error(f"The {name} argument cannot be 'None'.") return value + def try_import_compiled_module_from_path(module_name: str, path: Optional[str] = None) -> Union[types.ModuleType, str]: """ Attempt to import a module from a given path. diff --git a/pyoptsparse/pyParOpt/ParOpt.py b/pyoptsparse/pyParOpt/ParOpt.py index 4198454e..f08e33b4 100644 --- a/pyoptsparse/pyParOpt/ParOpt.py +++ b/pyoptsparse/pyParOpt/ParOpt.py @@ -13,7 +13,12 @@ # Attempt to import ParOpt/mpi4py # If PYOPTSPARSE_REQUIRE_MPI is set to a recognized positive value, attempt import # and raise exception on failure. If set to anything else, no import is attempted. -if "PYOPTSPARSE_REQUIRE_MPI" in os.environ and os.environ["PYOPTSPARSE_REQUIRE_MPI"].lower() not in ["always", "1", "true", "yes"]: +if "PYOPTSPARSE_REQUIRE_MPI" in os.environ and os.environ["PYOPTSPARSE_REQUIRE_MPI"].lower() not in [ + "always", + "1", + "true", + "yes", +]: _ParOpt = "ParOpt was not imported, as requested by the environment variable 'PYOPTSPARSE_REQUIRE_MPI'" MPI = "mpi4py was not imported, as requested by the environment variable 'PYOPTSPARSE_REQUIRE_MPI'" # If PYOPTSPARSE_REQUIRE_MPI is unset, attempt to import mpi4py. @@ -23,6 +28,7 @@ _ParOpt = try_import_compiled_module_from_path("paropt.ParOpt") MPI = try_import_compiled_module_from_path("mpi4py.MPI") + class ParOpt(Optimizer): """ ParOpt optimizer class diff --git a/pyoptsparse/pySLSQP/pySLSQP.py b/pyoptsparse/pySLSQP/pySLSQP.py index e5a5eac1..d7e6b367 100644 --- a/pyoptsparse/pySLSQP/pySLSQP.py +++ b/pyoptsparse/pySLSQP/pySLSQP.py @@ -19,6 +19,7 @@ THIS_DIR = os.path.dirname(os.path.abspath(__file__)) slsqp = try_import_compiled_module_from_path("slsqp", THIS_DIR) + class SLSQP(Optimizer): """ SLSQP Optimizer Class - Inherited from Optimizer Abstract Class diff --git a/tests/test_other.py b/tests/test_other.py index b76b8ec3..8a1c9fec 100644 --- a/tests/test_other.py +++ b/tests/test_other.py @@ -10,6 +10,7 @@ # the snopt module gets automatically imported, thus failing the import test below os.environ.pop("PYOPTSPARSE_IMPORT_SNOPT_FROM", None) + class TestImportSnoptFromPath(unittest.TestCase): def test_nonexistent_path(self): # first unload `snopt` from namespace From e1f32078026835c26272b598b8408c4a86329180 Mon Sep 17 00:00:00 2001 From: Ella Wu <602725+nwu63@users.noreply.github.com> Date: Sat, 16 Mar 2024 23:56:20 -0700 Subject: [PATCH 11/13] cast to list first --- 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 8a1c9fec..c7f4bd27 100644 --- a/tests/test_other.py +++ b/tests/test_other.py @@ -14,7 +14,7 @@ class TestImportSnoptFromPath(unittest.TestCase): def test_nonexistent_path(self): # first unload `snopt` from namespace - for key in sys.modules.keys(): + for key in list(sys.modules.keys()): if "snopt" in key: sys.modules.pop(key) with self.assertWarns(ImportWarning): From e36655c82532af737bb3e897a0f3520c1862cbad Mon Sep 17 00:00:00 2001 From: Ella Wu <602725+ewu63@users.noreply.github.com> Date: Fri, 22 Mar 2024 10:02:42 -0700 Subject: [PATCH 12/13] Use default UserWarning --- pyoptsparse/pyOpt_utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyoptsparse/pyOpt_utils.py b/pyoptsparse/pyOpt_utils.py index 22fe84fa..35eb7400 100644 --- a/pyoptsparse/pyOpt_utils.py +++ b/pyoptsparse/pyOpt_utils.py @@ -603,7 +603,6 @@ def try_import_compiled_module_from_path(module_name: str, path: Optional[str] = if path is not None: warnings.warn( f"{module_name} module could not be imported from {path}.", - ImportWarning, stacklevel=2, ) module = str(e) From cbfa664086485410b51eb9b73663ea6dfd74cd9d Mon Sep 17 00:00:00 2001 From: Marco Mangano <36549388+marcomangano@users.noreply.github.com> Date: Fri, 22 Mar 2024 13:23:41 -0400 Subject: [PATCH 13/13] Update test_other.py to match new warning --- 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 c7f4bd27..41435b8d 100644 --- a/tests/test_other.py +++ b/tests/test_other.py @@ -17,7 +17,7 @@ def test_nonexistent_path(self): for key in list(sys.modules.keys()): if "snopt" in key: sys.modules.pop(key) - with self.assertWarns(ImportWarning): + with self.assertWarns(UserWarning): module = try_import_compiled_module_from_path("snopt", "/a/nonexistent/path") self.assertTrue(isinstance(module, str))