Source code for utopya_backend.tools
"""This module implements various generic tools"""
import importlib
import os
import sys
from types import ModuleType
from typing import Any, Union
from .logging import backend_logger as _log
# -- YAML ---------------------------------------------------------------------
[docs]def load_cfg_file(fpath: str, *, loader: str = None) -> Any:
"""Loads a configuration file from the given file. Allows to automatically
determine which kind of loading function to use.
Currently supported loading functions: YAML
Args:
fpath (str): Path to the configuration file to load
loader (str, optional): Name of the loader to use. If not given, will
determine it from the file extension of ``fpath``.
Returns:
Any: The return value of the load function.
Raises:
ValueError: On invalid ``loader`` argument or a file extension that
does not map to a supported loader.
"""
if loader is None:
# Determine from file extension
_, ext = os.path.splitext(fpath)
loader = ext.replace(".", "")
loader = loader.lower()
# Determine load function
# TODO add toml, json, …
if loader in ("yaml", "yml"):
from paramspace.yaml import yaml
load = yaml.load
else:
raise ValueError(
f"Unsupported loader '{loader}' for configuration file! Check the "
"file path and the `loader` argument.\n"
f" File path: {fpath}\n"
f" Available loaders: yml, yaml"
)
# Load now
with open(os.path.expanduser(fpath), "r") as f:
return load(f)
# -----------------------------------------------------------------------------
[docs]def import_package_from_dir(
mod_dir: str, *, mod_str: str = None
) -> ModuleType:
"""Helper function to import a package-like module that is importable only
when adding the module's parent directory to :py:data:`sys.path`.
The ``mod_dir`` directory needs to contain an ``__init__.py`` file.
If that is not the case, you cannot use this function, because the
directory does not represent a package.
.. hint::
This function is very useful to get access to a local package that is
*not* installed, as might be the case for your model implementation.
Assuming you have an ``impl`` package right beside the current
``__file__`` and that package includes your ``Model`` class
implementation:
.. code-block:: text
- run_model.py # Current __file__
- impl/ # Implementation package
|-- __init__.py # Exposes impl.model.Model
|-- model.py # Implements Model class
|-- ...
You can get access to it like this from within ``run_model.py``:
.. code-block:: python
import os
from utopya_backend import import_package_from_dir
impl = import_package_from_dir(
os.path.join(os.path.dirname(__file__), "impl")
)
Model = impl.Model
Args:
mod_dir (str): Path to the module's root *directory*, ``~`` expanded.
For robustness, relative paths are *not* allowed.
mod_str (str, optional): Name under which the module can be imported
with the *parent* of ``mod_dir`` being in :py:data:`sys.path`.
If not given, will assume it is equal to the last segment of
``mod_dir``.
Returns:
ModuleType: The imported module.
Raises:
ImportError: If ``debug`` is set and import failed for whatever reason
FileNotFoundError: If ``mod_dir`` did not point to an existing
*directory*
"""
mod_dir = os.path.expanduser(mod_dir)
if not os.path.isabs(mod_dir):
raise ValueError(
f"Need an absolute path for argument `mod_dir` but got: {mod_dir}"
)
elif not os.path.isdir(mod_dir):
raise FileNotFoundError(
"The `mod_dir` argument to import a module from a path should be "
f"the path to an existing directory! Given path: {mod_dir}"
)
# Normalize it to ensure that it does not have a trailing slash
mod_dir = os.path.realpath(mod_dir)
# May need to infer module string
if mod_str is None:
mod_str = os.path.basename(mod_dir)
# Need the parent directory in the path, because the import is only
# possible from there. This, in turn, depends on the depth of the module
# string, so the parent directory should be chosen accordingly.
mod_parent_dir = mod_dir
for _ in mod_str.split("."):
mod_parent_dir = os.path.dirname(mod_parent_dir)
_log.debug("Importing module '%s' from directory ...", mod_str)
try:
sys.path.insert(0, mod_parent_dir)
mod = importlib.import_module(mod_str)
except Exception as exc:
raise ImportError(
f"Failed importing module '{mod_str}'!\n"
f"Make sure that {mod_dir}/__init__.py can be loaded without "
"errors (with its parent directory being part of sys.path) "
"and that the `mod_str` argument is correct; if you did not "
"specify `mod_str` explicitly, consider doing so."
) from exc
_log.debug("Successfully imported module from directory: %s", mod)
return mod