Skip to content

Commit 7a988f8

Browse files
authored
Merge pull request #57 from kassonlab/b55-logging
Further improve import error reporting.
2 parents 9c2ac90 + 13aa031 commit 7a988f8

File tree

3 files changed

+64
-38
lines changed

3 files changed

+64
-38
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ requires = [
1212
"gmxapi",
1313
"pybind11>=2.6",
1414
"setuptools>=61",
15-
"versioningit~=2.0",
15+
"versioningit>=2.0",
1616
"wheel"
1717
]
1818
build-backend = "backend"

src/brer/run_config.py

Lines changed: 48 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
import collections.abc
55
import dataclasses
66
import functools
7+
import importlib
78
import logging
89
import os
910
import pathlib
1011
import shutil
12+
import sys
1113
import typing
1214
import warnings
1315
from typing import Sequence
@@ -26,47 +28,56 @@
2628
_Path = Union[str, pathlib.Path]
2729

2830

29-
def _gmxapi_missing(*args, exc_info=None, **kwargs):
30-
message = (
31+
def _gmxapi_missing(*args, msg: str = "", **kwargs):
32+
"""Placeholder function for missing gmxapi functionality.
33+
34+
Allows import errors to be deferred until run time to aid in docs builds
35+
and troubleshooting. Try to provide a useful RuntimeError that includes
36+
the details of the import error(s).
37+
"""
38+
_message = (
3139
"brer requires gmxapi. See https://github.com/kassonlab/brer_md#requirements"
3240
)
33-
if exc_info:
34-
message += f" {exc_info}"
35-
raise RuntimeError(message)
41+
if msg:
42+
_message = "\n".join((_message, msg))
43+
raise RuntimeError(_message)
3644

3745

38-
try:
39-
# noinspection PyPep8Naming,PyUnresolvedReferences
40-
from gmxapi.simulation.context import Context as _context
41-
except (ImportError, ModuleNotFoundError) as e:
42-
try:
43-
# noinspection PyPep8Naming
44-
from gmx.context import Context as _context
45-
except (ImportError, ModuleNotFoundError) as e:
46-
missing = functools.partial(_gmxapi_missing, exception=str(e))
47-
_context = missing
48-
49-
try:
50-
# noinspection PyUnresolvedReferences
51-
from gmxapi.simulation.workflow import from_tpr
52-
except (ImportError, ModuleNotFoundError) as e:
53-
try:
54-
# noinspection PyPep8Naming
55-
from gmx.workflow import from_tpr
56-
except (ImportError, ModuleNotFoundError) as e:
57-
missing = functools.partial(_gmxapi_missing, exception=str(e))
58-
from_tpr = missing
59-
60-
try:
61-
# noinspection PyUnresolvedReferences
62-
from gmxapi.simulation.workflow import WorkElement
63-
except (ImportError, ModuleNotFoundError) as e:
64-
try:
65-
# noinspection PyPep8Naming
66-
from gmx.workflow import WorkElement
67-
except (ImportError, ModuleNotFoundError) as e:
68-
missing = functools.partial(_gmxapi_missing, exception=str(e))
69-
WorkElement = missing
46+
def get_api_callable(attr: str, modules: typing.Iterable[str]):
47+
"""Get a gmxapi callable or placeholder.
48+
49+
Try to import *attr* from successive *modules*. Return the first callable
50+
found, else return a placeholder that emits a RuntimeError when called.
51+
"""
52+
message = ""
53+
version = ""
54+
func = None
55+
for module in modules:
56+
try:
57+
mod = importlib.import_module(module)
58+
func = getattr(mod, attr)
59+
base = module.split(".")[0]
60+
version = sys.modules[base].__version__
61+
except ImportError as e:
62+
message = "\n".join((message, f"Could not import {module}: {str(e)}"))
63+
else:
64+
break
65+
if callable(func):
66+
qualname = ".".join((func.__module__, func.__name__))
67+
else:
68+
func = functools.partial(_gmxapi_missing, msg=message)
69+
qualname = ".".join((_gmxapi_missing.__module__, "_gmxapi_missing"))
70+
71+
report = "Using" \
72+
+ " ".join((qualname, version)) \
73+
+ " for {attr}."
74+
logging.info(report)
75+
return func
76+
77+
78+
_context = get_api_callable("Context", ("gmxapi.simulation.context", "gmx.context"))
79+
from_tpr = get_api_callable("from_tpr", ("gmxapi.simulation.workflow", "gmx.workflow"))
80+
WorkElement = get_api_callable("WorkElement", ("gmxapi.simulation.workflow", "gmx.workflow"))
7081

7182

7283
def check_consistency(*, data: PairDataCollection, state: RunData):

tests/test_run_config.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,21 @@
4141
num_cpus = 4
4242

4343

44+
def test_import_utility(simulation_input):
45+
from brer.run_config import _context, from_tpr, WorkElement, get_api_callable
46+
_context()
47+
element = from_tpr(simulation_input)
48+
assert isinstance(element, WorkElement)
49+
func = get_api_callable("nonsense", ("missing1",))
50+
with pytest.raises(RuntimeError, match="Could not import missing1"):
51+
func()
52+
func = get_api_callable("nonsense", ("missing1", "missing2"))
53+
with pytest.raises(RuntimeError, match="Could not import missing2"):
54+
func()
55+
with pytest.raises(RuntimeError, match="Could not import missing1"):
56+
func()
57+
58+
4459
@contextlib.contextmanager
4560
def working_directory_fence():
4661
"""Ensure restoration of working directory when leaving context manager."""

0 commit comments

Comments
 (0)