Source code for utopya.eval._plot_func_resolver
"""Implements a plot function resolver that takes model-specific information
into account."""
import logging
import os
import traceback
from types import ModuleType
from typing import Dict
import dantro._import_tools
import dantro.plot.utils
from ..model_registry import ModelInfoBundle
log = logging.getLogger(__name__)
# -----------------------------------------------------------------------------
[docs]class PlotFuncResolver(dantro.plot.utils.PlotFuncResolver):
"""A utopya-specific plot function resolver for
:py:class:`~utopya.eval.plotmanager.PlotManager` that takes information
from the model info bundle into account.
"""
BASE_PKG = "utopya.eval.plots"
"""Which package to use as base package for relative module imports"""
[docs] def __init__(
self,
*,
_model_info_bundle: ModelInfoBundle,
**kwargs,
):
"""Initializes the plot function resolver and additionally stores the
model info bundle.
"""
super().__init__(**kwargs)
self._model_info_bundle = _model_info_bundle
[docs] def _get_custom_module_paths(self) -> Dict[str, str]:
"""Aggregates a dict of module paths from which imports are attempted.
Uses model- or project-specific information, if available:
- A model's ``py_plots_dir``
- A project's ``py_plots_dir``
- A project's additional ``py_modules``
- The framework's additional ``py_modules``
- The framework's ``py_plots_dir``
"""
p = dict()
mib = self._model_info_bundle
if mib is None:
return p
if mib.paths.get("py_plots_dir"):
p["model's py_plots_dir"] = mib.paths["py_plots_dir"]
project = mib.project
if project:
if project.paths.py_plots_dir:
p["project's py_plots_dir"] = project.paths.py_plots_dir
if project.custom_py_modules:
for label, mod_path in project.custom_py_modules.items():
p[f"custom module '{label}'"] = mod_path
if project.framework_project is not None:
fw = project.framework_project
if fw.paths.py_plots_dir:
p["framework's py_plots_dir"] = fw.paths.py_plots_dir
if fw.custom_py_modules:
for label, mod_path in fw.custom_py_modules.items():
p[f"framework's custom module '{label}'"] = mod_path
return p
[docs] def _get_module_via_import(self, *, module: str, **kwargs) -> ModuleType:
"""Extends the parent method by making the custom modules available if
the regular import failed.
"""
try:
return super()._get_module_via_import(module=module, **kwargs)
except ModuleNotFoundError as err:
# Are there additional modules that are to be searched for imports?
custom_module_paths = self._get_custom_module_paths()
if not custom_module_paths:
log.note(
"No custom module paths available to import '%s' from.",
module,
)
raise
log.debug(
"Module '%s' could not be imported with a default sys.path, "
"but custom plot modules are available. Attempting to "
"import it with %d additional path(s) being available.",
module,
len(custom_module_paths),
)
# Go over the specified custom paths and try to import them, gathering
# detailed error information if that fails
errors = dict()
for key, mod_path in custom_module_paths.items():
# In order to be able to import modules at the given path, the
# sys.path needs to include the _parent_ directory of this path.
parent_dir = os.path.dirname(mod_path)
# Enter two context managers, taking care to return both sys.path
# and sys.modules back to the same state as they were before their
# invocation.
# The latter context manager is crucial because module imports lead
# to a cache entry even if a subsequent attempt to import a part of
# the module string was the cause of an error (which makes sense).
# Example: a failing `model_plots.foo` import would still lead to a
# cache entry of the `model_plots` module; however, attempting to
# then import `model_plots.bar` will make the lookup _only_ in the
# cached module. As we want several import attempts here, the cache
# is not desired.
add_sys_path = dantro._import_tools.added_sys_path(parent_dir)
tmp_sys_modules = dantro._import_tools.temporary_sys_modules(
reset_only_on_fail=True
)
with add_sys_path, tmp_sys_modules:
try:
mod = super()._get_module_via_import(
module=module, **kwargs
)
except ModuleNotFoundError as err:
_tb = err.__traceback__
errors[parent_dir] = dict(
err=err,
tb=_tb,
tb_lines=traceback.format_tb(_tb),
)
else:
log.debug(
"Found module '%s' after having added custom plot "
"module path labelled '%s' (%s) to the sys.path.",
mod,
key,
mod_path,
)
return mod
# All imports failed. Inform extensively about errors to help debugging
err_info = "\n".join(
f"-- Error at custom plot module path {p} : {e['err']}\n\n"
" Abbreviated traceback:\n"
f"{e['tb_lines'][0]} ...\n"
f"{e['tb_lines'][-1]}"
for p, e in errors.items()
)
raise ModuleNotFoundError(
f"Could not import module '{module}'! It was found neither among "
"the installed packages nor among the custom plot modules.\n"
"\n"
"The following errors were encountered at the respective custom "
"plot module search paths:\n\n"
f"{err_info}\n"
"NOTE: This error can have two reasons:\n"
f" (1) the '{module}' module does not exist in the specified "
" search location.\n"
" (2) during import of the plot module you specified, an "
"_unrelated_ ModuleNotFoundError occurred somewhere inside _your_ "
"code.\n"
"To debug, check the error messages and tracebacks above to find "
"out which of the two is preventing module import."
)