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..b009a4a4 100644 --- a/pyoptsparse/pyIPOPT/pyIPOPT.py +++ b/pyoptsparse/pyIPOPT/pyIPOPT.py @@ -1,24 +1,31 @@ """ 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 import numpy as np # 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..173703e9 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,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__)) +nsga2 = try_import_compiled_module_from_path("nsga2", THIS_DIR) class NSGA2(Optimizer): @@ -30,9 +31,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/pyOpt_utils.py b/pyoptsparse/pyOpt_utils.py index e67eabb7..35eb7400 100644 --- a/pyoptsparse/pyOpt_utils.py +++ b/pyoptsparse/pyOpt_utils.py @@ -9,7 +9,11 @@ mat = {'csc':[colp, rowind, data], 'shape':[nrow, ncols]} # A csc matrix """ # Standard Python modules -from typing import Tuple, Union +import importlib +import os +import sys +import types +from typing import Optional, Tuple, Union import warnings # External modules @@ -570,3 +574,38 @@ 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: Optional[str] = None) -> Union[types.ModuleType, str]: + """ + 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. + + Returns + ------- + Union[types.ModuleType, str] + If importable, the imported module is returned. + If not importable, the error message is instead returned. + """ + 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) + except ImportError as e: + if path is not None: + warnings.warn( + f"{module_name} module could not be imported from {path}.", + stacklevel=2, + ) + module = str(e) + finally: + sys.path = orig_path + return module 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/pyParOpt/ParOpt.py b/pyoptsparse/pyParOpt/ParOpt.py index 3b64140b..f08e33b4 100644 --- a/pyoptsparse/pyParOpt/ParOpt.py +++ b/pyoptsparse/pyParOpt/ParOpt.py @@ -6,34 +6,27 @@ # 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,9 +41,9 @@ 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") + 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 58f3ee6e..d7e6b367 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 @@ -16,8 +12,12 @@ 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 + +# 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) diff --git a/pyoptsparse/pySNOPT/pySNOPT.py b/pyoptsparse/pySNOPT/pySNOPT.py index 249f4ddd..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 @@ -21,39 +19,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 +64,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: diff --git a/tests/test_other.py b/tests/test_other.py index 98d0996d..41435b8d 100644 --- a/tests/test_other.py +++ b/tests/test_other.py @@ -3,20 +3,25 @@ import sys import unittest -# we have to unset this environment variable because otherwise when we import `_import_snopt_from_path` +# 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) -# First party modules -from pyoptsparse.pySNOPT.pySNOPT import _import_snopt_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")) + # first unload `snopt` from namespace + 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") + self.assertTrue(isinstance(module, str)) 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) diff --git a/tests/test_require_mpi_env_var.py b/tests/test_require_mpi_env_var.py index 15e7ee25..ca95c0ed 100644 --- a/tests/test_require_mpi_env_var.py +++ b/tests/test_require_mpi_env_var.py @@ -2,14 +2,9 @@ import importlib import inspect import os -import sys 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 +21,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 +38,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 +55,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__": diff --git a/tests/test_snopt_bugfix.py b/tests/test_snopt_bugfix.py index e6460452..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): @@ -104,10 +103,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 +134,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 +175,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..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: @@ -105,10 +104,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 +125,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..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): @@ -231,7 +230,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: