Skip to content
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,5 @@ pyoptsparse/pyNLPQLP/source
*.pdb

*.pyd

.DS_Store
2 changes: 1 addition & 1 deletion pyoptsparse/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = "2.14.2"
__version__ = "2.14.3"

from .pyOpt_history import History
from .pyOpt_variable import Variable
Expand Down
8 changes: 4 additions & 4 deletions pyoptsparse/pyCONMIN/pyCONMIN.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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)
Expand Down
14 changes: 5 additions & 9 deletions pyoptsparse/pyIPOPT/pyIPOPT.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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,
Expand Down
8 changes: 4 additions & 4 deletions pyoptsparse/pyNLPQLP/pyNLPQLP.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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
Expand Down
8 changes: 4 additions & 4 deletions pyoptsparse/pyNSGA2/pyNSGA2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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")
Expand Down
68 changes: 41 additions & 27 deletions pyoptsparse/pyOpt_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@
"""

# Standard Python modules
import contextlib
import importlib
import os
import sys
import types
from typing import Optional, Tuple, Union
from typing import Literal, Sequence, Tuple, Union
import warnings

# External modules
Expand Down Expand Up @@ -361,9 +362,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
----------
Expand Down Expand Up @@ -576,40 +577,53 @@ 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]:
@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]] = (),
on_error: Literal["raise", "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 : 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 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]
If importable, the imported module is returned.
If not importable, the error message is instead returned.
If not importable, the exception is 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 raise_warning:
warnings.warn(
f"{module_name} module could not be imported from {path}.",
stacklevel=2,
)
module = str(e)
finally:
sys.path = orig_path
if on_error.lower() not in ("raise", "return"):
raise ValueError("`on_error` must be 'raise' or 'return'.")

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
8 changes: 4 additions & 4 deletions pyoptsparse/pyPSQP/pyPSQP.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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)

Expand Down
37 changes: 21 additions & 16 deletions pyoptsparse/pyParOpt/ParOpt.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,25 @@
try:
# External modules
from paropt.paropt_pyoptsparse import ParOptSparse as ParOpt
except ImportError:
except ImportError as 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 ImportError("There was an error importing ParOpt")
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)
8 changes: 4 additions & 4 deletions pyoptsparse/pySLSQP/pySLSQP.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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)
Expand Down
10 changes: 5 additions & 5 deletions pyoptsparse/pySNOPT/pySNOPT.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@
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)
_IMPORT_SNOPT_FROM = os.environ.get("PYOPTSPARSE_IMPORT_SNOPT_FROM", None) or THIS_DIR
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is to handle an empty str set to the var?

snopt = import_module("snopt", [_IMPORT_SNOPT_FROM])


class SNOPT(Optimizer):
Expand Down Expand Up @@ -68,9 +68,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:
Expand Down
21 changes: 16 additions & 5 deletions tests/test_other.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -19,13 +19,24 @@ 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):
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)
try_import_compiled_module_from_path("snopt", "/some/path")
import_module("somemodule", ["/some/path"])
self.assertEqual(tuple(sys.path), path)


Expand Down
Loading