Skip to content

Commit 88b7104

Browse files
whophilPhil ChiukanekoshA-CGrayewu63
authored
Improve conditional imports (#458)
* Improve conditional imports * Add back env var handling for SNOPT * Fix typehint * Fix value checking * Permit strs * Formatting * Linting * Add better tests * Use context manager for prepend_path * Fix error check/raise in pyCONMIN * Update pyOpt_utils.py * version bump --------- Co-authored-by: Phil Chiu <pchiu@archer.com> Co-authored-by: Shugo Kaneko <49300827+kanekosh@users.noreply.github.com> Co-authored-by: Alasdair Gray <alachris@umich.edu> Co-authored-by: Ella Wu <602725+ewu63@users.noreply.github.com>
1 parent bdea96c commit 88b7104

File tree

12 files changed

+111
-83
lines changed

12 files changed

+111
-83
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,5 @@ pyoptsparse/pyNLPQLP/source
2828
*.pdb
2929

3030
*.pyd
31+
32+
.DS_Store

pyoptsparse/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = "2.14.2"
1+
__version__ = "2.14.3"
22

33
from .pyOpt_history import History
44
from .pyOpt_variable import Variable

pyoptsparse/pyCONMIN/pyCONMIN.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@
1313

1414
# Local modules
1515
from ..pyOpt_optimizer import Optimizer
16-
from ..pyOpt_utils import try_import_compiled_module_from_path
16+
from ..pyOpt_utils import import_module
1717

1818
# import the compiled module
1919
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
20-
conmin = try_import_compiled_module_from_path("conmin", THIS_DIR, raise_warning=True)
20+
conmin = import_module("conmin", [THIS_DIR])
2121

2222

2323
class CONMIN(Optimizer):
@@ -30,8 +30,8 @@ def __init__(self, raiseError=True, options={}):
3030
category = "Local Optimizer"
3131
defOpts = self._getDefaultOptions()
3232
informs = self._getInforms()
33-
if isinstance(conmin, str) and raiseError:
34-
raise ImportError(conmin)
33+
if isinstance(conmin, Exception) and raiseError:
34+
raise conmin
3535

3636
self.set_options = []
3737
super().__init__(name, category, defaultOptions=defOpts, informs=informs, options=options)

pyoptsparse/pyIPOPT/pyIPOPT.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,12 @@
1010
# External modules
1111
import numpy as np
1212

13-
try:
14-
# External modules
15-
import cyipopt
16-
except ImportError:
17-
cyipopt = None
18-
1913
# Local modules
2014
from ..pyOpt_optimizer import Optimizer
2115
from ..pyOpt_solution import SolutionInform
22-
from ..pyOpt_utils import ICOL, INFINITY, IROW, convertToCOO, extractRows, scaleRows
16+
from ..pyOpt_utils import ICOL, INFINITY, IROW, convertToCOO, extractRows, import_module, scaleRows
17+
18+
cyipopt = import_module("cyipopt")
2319

2420

2521
class IPOPT(Optimizer):
@@ -36,8 +32,8 @@ def __init__(self, raiseError=True, options={}):
3632
category = "Local Optimizer"
3733
defOpts = self._getDefaultOptions()
3834
informs = self._getInforms()
39-
if cyipopt is None and raiseError:
40-
raise ImportError("Could not import cyipopt")
35+
if isinstance(cyipopt, Exception) and raiseError:
36+
raise cyipopt
4137

4238
super().__init__(
4339
name,

pyoptsparse/pyNLPQLP/pyNLPQLP.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@
1414
# Local modules
1515
from ..pyOpt_optimizer import Optimizer
1616
from ..pyOpt_solution import SolutionInform
17-
from ..pyOpt_utils import try_import_compiled_module_from_path
17+
from ..pyOpt_utils import import_module
1818

1919
# import the compiled module
2020
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
21-
nlpqlp = try_import_compiled_module_from_path("nlpqlp", THIS_DIR)
21+
nlpqlp = import_module("nlpqlp", [THIS_DIR])
2222

2323

2424
class NLPQLP(Optimizer):
@@ -31,8 +31,8 @@ def __init__(self, raiseError=True, options={}):
3131
category = "Local Optimizer"
3232
defOpts = self._getDefaultOptions()
3333
informs = self._getInforms()
34-
if isinstance(nlpqlp, str) and raiseError:
35-
raise ImportError(nlpqlp)
34+
if isinstance(nlpqlp, Exception) and raiseError:
35+
raise nlpqlp
3636

3737
super().__init__(name, category, defaultOptions=defOpts, informs=informs, options=options)
3838
# NLPQLP needs Jacobians in dense format

pyoptsparse/pyNSGA2/pyNSGA2.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@
1313

1414
# Local modules
1515
from ..pyOpt_optimizer import Optimizer
16-
from ..pyOpt_utils import try_import_compiled_module_from_path
16+
from ..pyOpt_utils import import_module
1717

1818
# import the compiled module
1919
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
20-
nsga2 = try_import_compiled_module_from_path("nsga2", THIS_DIR, raise_warning=True)
20+
nsga2 = import_module("nsga2", [THIS_DIR])
2121

2222

2323
class NSGA2(Optimizer):
@@ -32,8 +32,8 @@ def __init__(self, raiseError=True, options={}):
3232
informs = self._getInforms()
3333
super().__init__(name, category, defaultOptions=defOpts, informs=informs, options=options)
3434

35-
if isinstance(nsga2, str) and raiseError:
36-
raise ImportError(nsga2)
35+
if isinstance(nsga2, Exception) and raiseError:
36+
raise nsga2
3737

3838
if self.getOption("PopSize") % 4 != 0:
3939
raise ValueError("Option 'PopSize' must be a multiple of 4")

pyoptsparse/pyOpt_utils.py

Lines changed: 41 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@
1010
"""
1111

1212
# Standard Python modules
13+
import contextlib
1314
import importlib
1415
import os
1516
import sys
1617
import types
17-
from typing import Optional, Tuple, Union
18+
from typing import Literal, Sequence, Tuple, Union
1819
import warnings
1920

2021
# External modules
@@ -361,9 +362,9 @@ def convertToCSC(mat: Union[dict, spmatrix, ndarray]) -> dict:
361362

362363
def convertToDense(mat: Union[dict, spmatrix, ndarray]) -> ndarray:
363364
"""
364-
Take a pyopsparse sparse matrix definition and convert back to a dense
365+
Take a pyoptsparse sparse matrix definition and convert back to a dense
365366
format. This is typically the final step for optimizers with dense constraint
366-
jacibians.
367+
jacobians.
367368
368369
Parameters
369370
----------
@@ -576,40 +577,53 @@ def _broadcast_to_array(name: str, value: ArrayType, n_values: int, allow_none:
576577
return value
577578

578579

579-
def try_import_compiled_module_from_path(
580-
module_name: str, path: Optional[str] = None, raise_warning: bool = False
581-
) -> Union[types.ModuleType, str]:
580+
@contextlib.contextmanager
581+
def _prepend_path(path: Union[str, Sequence[str]]):
582+
"""Context manager which temporarily prepends to `sys.path`."""
583+
if isinstance(path, str):
584+
path = [path]
585+
orig_path = sys.path
586+
if path:
587+
path = [os.path.abspath(os.path.expandvars(os.path.expanduser(p))) for p in path]
588+
sys.path = path + sys.path
589+
yield
590+
sys.path = orig_path
591+
return
592+
593+
594+
def import_module(
595+
module_name: str,
596+
path: Union[str, Sequence[str]] = (),
597+
on_error: Literal["raise", "return"] = "return",
598+
) -> Union[types.ModuleType, Exception]:
582599
"""
583600
Attempt to import a module from a given path.
584601
585602
Parameters
586603
----------
587604
module_name : str
588-
The name of the module
589-
path : Optional[str]
590-
The path to import from. If None, the default ``sys.path`` is used.
591-
raise_warning : bool
592-
If true, raise an import warning. By default false.
605+
The name of the module.
606+
path : Union[str, Sequence[str]]
607+
The search path, which will be prepended to ``sys.path``. May be a string, or a sequence of strings.
608+
on_error : str
609+
Specify behavior when import fails. If "raise", any exception raised during the import will be raised.
610+
If "return", any exception during the import will be returned.
593611
594612
Returns
595613
-------
596614
Union[types.ModuleType, str]
597615
If importable, the imported module is returned.
598-
If not importable, the error message is instead returned.
616+
If not importable, the exception is returned.
599617
"""
600-
orig_path = sys.path
601-
if path is not None:
602-
path = os.path.abspath(os.path.expandvars(os.path.expanduser(path)))
603-
sys.path = [path]
604-
try:
605-
module = importlib.import_module(module_name)
606-
except ImportError as e:
607-
if raise_warning:
608-
warnings.warn(
609-
f"{module_name} module could not be imported from {path}.",
610-
stacklevel=2,
611-
)
612-
module = str(e)
613-
finally:
614-
sys.path = orig_path
618+
if on_error.lower() not in ("raise", "return"):
619+
raise ValueError("`on_error` must be 'raise' or 'return'.")
620+
621+
with _prepend_path(path):
622+
try:
623+
module = importlib.import_module(module_name)
624+
except ImportError as e:
625+
if on_error.lower() == "raise":
626+
raise e
627+
else:
628+
module = e
615629
return module

pyoptsparse/pyPSQP/pyPSQP.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@
1313
# Local modules
1414
from ..pyOpt_optimizer import Optimizer
1515
from ..pyOpt_solution import SolutionInform
16-
from ..pyOpt_utils import try_import_compiled_module_from_path
16+
from ..pyOpt_utils import import_module
1717

1818
# import the compiled module
1919
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
20-
psqp = try_import_compiled_module_from_path("psqp", THIS_DIR)
20+
psqp = import_module("psqp", [THIS_DIR])
2121

2222

2323
class PSQP(Optimizer):
@@ -31,8 +31,8 @@ def __init__(self, raiseError=True, options={}):
3131
defOpts = self._getDefaultOptions()
3232
informs = self._getInforms()
3333

34-
if isinstance(psqp, str) and raiseError:
35-
raise ImportError(psqp)
34+
if isinstance(psqp, Exception) and raiseError:
35+
raise psqp
3636

3737
super().__init__(name, category, defaultOptions=defOpts, informs=informs, options=options)
3838

pyoptsparse/pyParOpt/ParOpt.py

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,25 @@
44
try:
55
# External modules
66
from paropt.paropt_pyoptsparse import ParOptSparse as ParOpt
7-
except ImportError:
7+
except ImportError as e:
88

9-
class ParOpt(Optimizer):
10-
def __init__(self, raiseError=True, options={}):
11-
name = "ParOpt"
12-
category = "Local Optimizer"
13-
self.defOpts = {}
14-
self.informs = {}
15-
super().__init__(
16-
name,
17-
category,
18-
defaultOptions=self.defOpts,
19-
informs=self.informs,
20-
options=options,
21-
)
22-
if raiseError:
23-
raise ImportError("There was an error importing ParOpt")
9+
def make_cls(e):
10+
class ParOpt(Optimizer):
11+
def __init__(self, raiseError=True, options={}):
12+
name = "ParOpt"
13+
category = "Local Optimizer"
14+
self.defOpts = {}
15+
self.informs = {}
16+
super().__init__(
17+
name,
18+
category,
19+
defaultOptions=self.defOpts,
20+
informs=self.informs,
21+
options=options,
22+
)
23+
if raiseError:
24+
raise e
25+
26+
return ParOpt
27+
28+
ParOpt = make_cls(e)

pyoptsparse/pySLSQP/pySLSQP.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@
1515
from ..pyOpt_error import pyOptSparseWarning
1616
from ..pyOpt_optimizer import Optimizer
1717
from ..pyOpt_solution import SolutionInform
18-
from ..pyOpt_utils import try_import_compiled_module_from_path
18+
from ..pyOpt_utils import import_module
1919

2020
# import the compiled module
2121
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
22-
slsqp = try_import_compiled_module_from_path("slsqp", THIS_DIR, raise_warning=True)
22+
slsqp = import_module("slsqp", [THIS_DIR])
2323

2424

2525
class SLSQP(Optimizer):
@@ -32,8 +32,8 @@ def __init__(self, raiseError=True, options={}):
3232
category = "Local Optimizer"
3333
defOpts = self._getDefaultOptions()
3434
informs = self._getInforms()
35-
if isinstance(slsqp, str) and raiseError:
36-
raise ImportError(slsqp)
35+
if isinstance(slsqp, Exception) and raiseError:
36+
raise slsqp
3737

3838
self.set_options = []
3939
super().__init__(name, category, defaultOptions=defOpts, informs=informs, options=options)

0 commit comments

Comments
 (0)