Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better import error #389

Merged
merged 13 commits into from
Mar 26, 2024
5 changes: 2 additions & 3 deletions pyoptsparse/pyALPSO/pyALPSO.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import numpy as np

# Local modules
from . import alpso
from ..pyOpt_error import Error
from ..pyOpt_optimizer import Optimizer

Expand All @@ -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={}):
marcomangano marked this conversation as resolved.
Show resolved Hide resolved
self.alpso = alpso

category = "Global Optimizer"
Expand Down
15 changes: 7 additions & 8 deletions pyoptsparse/pyCONMIN/pyCONMIN.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand All @@ -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)
Expand Down
26 changes: 16 additions & 10 deletions pyoptsparse/pyIPOPT/pyIPOPT.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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,
Expand Down
15 changes: 7 additions & 8 deletions pyoptsparse/pyNLPQLP/pyNLPQLP.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand All @@ -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:
Copy link
Contributor

Choose a reason for hiding this comment

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

Related to my other comment, why check for strings and then throw the same error that we could throw in the try_import_compiled_module_from_path function?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is in the class init, so it's only executed when the class is instantiated.

raise ImportError(nlpqlp)

super().__init__(name, category, defaultOptions=defOpts, informs=informs, options=options)
# NLPQLP needs Jacobians in dense format
Expand Down
16 changes: 8 additions & 8 deletions pyoptsparse/pyNSGA2/pyNSGA2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand All @@ -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():
Expand Down
41 changes: 40 additions & 1 deletion pyoptsparse/pyOpt_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a reason why we delay throwing the ImportError? If we can't import the module, why not throw the error with the descriptive message here instead of a warning?

Do we need this "soft fail" type behavior somewhere that I missed? Otherwise I'm in favor of just throwing the ImportError here with the same message and only returning a module if the import is successful. But again, let me know if I missed something that makes this necessary.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah it's because this is executed as part of the init process, and we don't necessarily want to raise an error. But if they try to instantiate the class and the compiled library is missing, we throw the error.

Copy link
Contributor

Choose a reason for hiding this comment

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

Okay that makes sense. I forgot that python tries to import anything that's involved in the init process. I'll approve this now.

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

Expand Down
43 changes: 18 additions & 25 deletions pyoptsparse/pyParOpt/ParOpt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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 = {}
Expand Down
17 changes: 8 additions & 9 deletions pyoptsparse/pySLSQP/pySLSQP.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand All @@ -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)
Expand Down
Loading
Loading