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

Check warning on line 13 in pyoptsparse/pyALPSO/pyALPSO.py

View check run for this annotation

Codecov / codecov/patch

pyoptsparse/pyALPSO/pyALPSO.py#L13

Added line #L13 was not covered by tests
from ..pyOpt_error import Error
from ..pyOpt_optimizer import Optimizer

Expand All @@ -25,9 +26,7 @@
- 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={}):

Check warning on line 29 in pyoptsparse/pyALPSO/pyALPSO.py

View check run for this annotation

Codecov / codecov/patch

pyoptsparse/pyALPSO/pyALPSO.py#L29

Added line #L29 was not covered by tests
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

Check warning on line 16 in pyoptsparse/pyCONMIN/pyCONMIN.py

View check run for this annotation

Codecov / codecov/patch

pyoptsparse/pyCONMIN/pyCONMIN.py#L16

Added line #L16 was not covered by tests

# import the compiled module
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
conmin = try_import_compiled_module_from_path("conmin", THIS_DIR)

Check warning on line 20 in pyoptsparse/pyCONMIN/pyCONMIN.py

View check run for this annotation

Codecov / codecov/patch

pyoptsparse/pyCONMIN/pyCONMIN.py#L19-L20

Added lines #L19 - L20 were not covered by tests


class CONMIN(Optimizer):
Expand All @@ -30,9 +30,8 @@
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)

Check warning on line 34 in pyoptsparse/pyCONMIN/pyCONMIN.py

View check run for this annotation

Codecov / codecov/patch

pyoptsparse/pyCONMIN/pyCONMIN.py#L34

Added line #L34 was not covered by tests

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

Check warning on line 8 in pyoptsparse/pyIPOPT/pyIPOPT.py

View check run for this annotation

Codecov / codecov/patch

pyoptsparse/pyIPOPT/pyIPOPT.py#L8

Added line #L8 was not covered by tests
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 (

Check warning on line 16 in pyoptsparse/pyIPOPT/pyIPOPT.py

View check run for this annotation

Codecov / codecov/patch

pyoptsparse/pyIPOPT/pyIPOPT.py#L16

Added line #L16 was not covered by tests
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)

Check warning on line 28 in pyoptsparse/pyIPOPT/pyIPOPT.py

View check run for this annotation

Codecov / codecov/patch

pyoptsparse/pyIPOPT/pyIPOPT.py#L27-L28

Added lines #L27 - L28 were not covered by tests


class IPOPT(Optimizer):
Expand All @@ -36,9 +43,8 @@
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)

Check warning on line 47 in pyoptsparse/pyIPOPT/pyIPOPT.py

View check run for this annotation

Codecov / codecov/patch

pyoptsparse/pyIPOPT/pyIPOPT.py#L47

Added line #L47 was not covered by tests

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

Check warning on line 16 in pyoptsparse/pyNLPQLP/pyNLPQLP.py

View check run for this annotation

Codecov / codecov/patch

pyoptsparse/pyNLPQLP/pyNLPQLP.py#L16

Added line #L16 was not covered by tests

# import the compiled module
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
nlpqlp = try_import_compiled_module_from_path("nlpqlp", THIS_DIR)

Check warning on line 20 in pyoptsparse/pyNLPQLP/pyNLPQLP.py

View check run for this annotation

Codecov / codecov/patch

pyoptsparse/pyNLPQLP/pyNLPQLP.py#L19-L20

Added lines #L19 - L20 were not covered by tests


class NLPQLP(Optimizer):
Expand All @@ -30,9 +30,8 @@
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)

Check warning on line 34 in pyoptsparse/pyNLPQLP/pyNLPQLP.py

View check run for this annotation

Codecov / codecov/patch

pyoptsparse/pyNLPQLP/pyNLPQLP.py#L34

Added line #L34 was not covered by tests

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

Check warning on line 6 in pyoptsparse/pyNSGA2/pyNSGA2.py

View check run for this annotation

Codecov / codecov/patch

pyoptsparse/pyNSGA2/pyNSGA2.py#L6

Added line #L6 was not covered by tests
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

Check warning on line 15 in pyoptsparse/pyNSGA2/pyNSGA2.py

View check run for this annotation

Codecov / codecov/patch

pyoptsparse/pyNSGA2/pyNSGA2.py#L15

Added line #L15 was not covered by tests

# import the compiled module
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
nsga2 = try_import_compiled_module_from_path("nsga2", THIS_DIR)

Check warning on line 19 in pyoptsparse/pyNSGA2/pyNSGA2.py

View check run for this annotation

Codecov / codecov/patch

pyoptsparse/pyNSGA2/pyNSGA2.py#L18-L19

Added lines #L18 - L19 were not covered by tests


class NSGA2(Optimizer):
Expand All @@ -30,9 +31,8 @@
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)

Check warning on line 35 in pyoptsparse/pyNSGA2/pyNSGA2.py

View check run for this annotation

Codecov / codecov/patch

pyoptsparse/pyNSGA2/pyNSGA2.py#L35

Added line #L35 was not covered by tests

@staticmethod
def _getInforms():
Expand Down
42 changes: 41 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

Check warning on line 16 in pyoptsparse/pyOpt_utils.py

View check run for this annotation

Codecov / codecov/patch

pyoptsparse/pyOpt_utils.py#L12-L16

Added lines #L12 - L16 were not covered by tests
import warnings

# External modules
Expand Down Expand Up @@ -570,3 +574,39 @@
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]:

Check warning on line 579 in pyoptsparse/pyOpt_utils.py

View check run for this annotation

Codecov / codecov/patch

pyoptsparse/pyOpt_utils.py#L579

Added line #L579 was not covered by tests
"""
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}.",
ImportWarning,
Copy link
Contributor

Choose a reason for hiding this comment

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

Not related directly to this PR, but is testflo raising ImportWarning by default? I tested just this function locally and the string above (which can be pretty useful) is not printed out by default - see here

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Hmm this is a good point. Maybe I will switch to UserWarning? I hate how a lot of warnings are not displayed by default.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, that's the default but we can set it explicitly.

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

Check warning on line 15 in pyoptsparse/pyPSQP/pyPSQP.py

View check run for this annotation

Codecov / codecov/patch

pyoptsparse/pyPSQP/pyPSQP.py#L15

Added line #L15 was not covered by tests

# import the compiled module
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
psqp = try_import_compiled_module_from_path("psqp", THIS_DIR)

Check warning on line 19 in pyoptsparse/pyPSQP/pyPSQP.py

View check run for this annotation

Codecov / codecov/patch

pyoptsparse/pyPSQP/pyPSQP.py#L18-L19

Added lines #L18 - L19 were not covered by tests


class PSQP(Optimizer):
Expand All @@ -30,9 +30,8 @@
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)

Check warning on line 34 in pyoptsparse/pyPSQP/pyPSQP.py

View check run for this annotation

Codecov / codecov/patch

pyoptsparse/pyPSQP/pyPSQP.py#L34

Added line #L34 was not covered by tests

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 @@
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)

Check warning on line 46 in pyoptsparse/pyParOpt/ParOpt.py

View check run for this annotation

Codecov / codecov/patch

pyoptsparse/pyParOpt/ParOpt.py#L46

Added line #L46 was not covered by tests

# 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

Check warning on line 16 in pyoptsparse/pySLSQP/pySLSQP.py

View check run for this annotation

Codecov / codecov/patch

pyoptsparse/pySLSQP/pySLSQP.py#L16

Added line #L16 was not covered by tests

# import the compiled module
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
slsqp = try_import_compiled_module_from_path("slsqp", THIS_DIR)

Check warning on line 20 in pyoptsparse/pySLSQP/pySLSQP.py

View check run for this annotation

Codecov / codecov/patch

pyoptsparse/pySLSQP/pySLSQP.py#L19-L20

Added lines #L19 - L20 were not covered by tests


class SLSQP(Optimizer):
Expand All @@ -30,9 +30,8 @@
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)

Check warning on line 34 in pyoptsparse/pySLSQP/pySLSQP.py

View check run for this annotation

Codecov / codecov/patch

pyoptsparse/pySLSQP/pySLSQP.py#L34

Added line #L34 was not covered by tests

self.set_options = []
super().__init__(name, category, defaultOptions=defOpts, informs=informs, options=options)
Expand Down
Loading
Loading