Source code for law.contrib.ipython.magic

# coding: utf-8

"""
IPython magics for law.
"""

from __future__ import annotations

__all__ = ["register_magics"]

import shlex
import logging

import law.cli
from law.util import law_run, quote_cmd
from law.logger import get_logger
from law._types import Callable, Sequence, Any


logger = get_logger(__name__)


def create_magics(
    init_cmd: str | Sequence[str] | None = None,
    init_fn: Callable | None = None,
    line_cmd: str | Sequence[str] | None = None,
    line_fn: Callable[[str], Any] | None = None,
    log_level: int | str | None = None,
) -> type:
    import IPython.core as ipc  # type: ignore[import-untyped, import-not-found]

    # prepare commands
    if init_cmd:
        init_cmd = init_cmd.strip() if isinstance(init_cmd, str) else quote_cmd(init_cmd)
    if line_cmd:
        line_cmd = line_cmd.strip() if isinstance(line_cmd, str) else quote_cmd(line_cmd)

    # set the log level
    if isinstance(log_level, str):
        log_level = getattr(logging, log_level.upper(), None)
    if isinstance(log_level, int):
        logger.setLevel(log_level)
        logger.debug(f"log level set to {log_level}")

    @ipc.magic.magics_class
    class LawMagics(ipc.magic.Magics):

        def __init__(self, *args, **kwargs) -> None:
            super().__init__(*args, **kwargs)

            # run the init_cmd when set
            if init_cmd:
                logger.info(f"running initialization command '{init_cmd}'")
                self._run_bash(init_cmd)

            # call the init_fn when set
            if callable(init_fn):
                logger.info(f"calling initialization function '{init_fn.__name__}'")
                init_fn()

        @ipc.magic.line_magic
        def law(self, line: str) -> Any:
            """
            Interprets the input *line* as command line arguments to the ``law`` executable and runs
            it in a subprocess using bash. Output and error streams are piped to the cell.
            """
            line = line.strip()
            if not line:
                logger.error(r"the command passed to %law must not be empty")
                return

            # build the full command
            cmd = "law " + line
            if line_cmd:
                cmd = f"{line_cmd} && {cmd}"
            logger.debug(f"running law command '{cmd}'")

            # run it
            return self._run_bash(cmd)

        @ipc.magic.line_magic
        def ilaw(self, line: str) -> int:
            """
            Interprets the input *line* as command line arguments to the ``law`` executable, but
            rather than invoking it in a subprocess, it is evaluated interactively (or inline, thus
            the *i*) within the running process. This is especially useful for programmatically
            running tasks that were defined e.g. in the current notebook.
            """
            line = line.strip()
            if not line:
                logger.error(r"the command passed to %ilaw must not be empty")
                return -1

            argv = shlex.split(line)
            prog = argv.pop(0)

            # prog must be a valid law cli prog
            if prog not in law.cli.cli.progs:
                raise ValueError(f"'{prog}' is not a valid law cli program")

            # forward to the actual prog, run special case
            try:
                # call the line_fn when set
                if callable(line_fn):
                    logger.info(f"calling line function '{line_fn.__name__}'")
                    line_fn(line)

                if prog == "run":
                    # perform the run call interactively
                    return law_run(argv)
                # forward all other progs to the cli interface
                return law.cli.cli.run([prog] + argv)

            except SystemExit as e:
                # reraise when the exit code is non-zero
                if e.code:
                    raise
                return 0

        def _run_bash(self, cmd: str | Sequence[str]) -> Any:
            # build the command
            if not isinstance(cmd, str):
                cmd = quote_cmd(cmd)
            cmd = quote_cmd(["bash", "-c", cmd])

            # run it
            return self.shell.system_piped(cmd)  # type: ignore[attr-defined, union-attr]

    return LawMagics


[docs] def register_magics(*args, **kwargs) -> None: """ register_magics(init_cmd=None, init_fn=None, line_cmd=None, line_fn=None, log_level=None) Registers the two IPython magic methods ``%law`` and ``%ilaw`` which execute law commands either via a subprocess in bash (``%law``) or interactively / inline within the running process (``%ilaw``). *init_cmd* can be a shell command that is called before the magic methods are registered. Similarly, *init_fn* can be a callable that is invoked prior to the method setup. *line_cmd*, a shell command, and *line_fn*, a callable, are executed before a line magic is called. The former is run before ``%law`` is evaluated, while the latter is called before ``%ilaw`` with the line to interpret as the only argument. *log_level* conveniently sets the level of the *law.contrib.ipython.magic* logger that is used within the magic methods. It should be a number, or a string denoting a Python log level. """ ipy = None magics = None try: ipy = get_ipython() # type: ignore[name-defined] except NameError: logger.error("no running notebook kernel found") # create the magics if ipy is not None: magics = create_magics(*args, **kwargs) # register it if ipy is not None and magics is not None: ipy.register_magics(magics) names = list(magics.magics["cell"].keys()) + list(magics.magics["line"].keys()) # type: ignore[attr-defined] # noqa names_str = ", ".join(f"%{name}" for name in names) logger.info(f"magics successfully registered: {names_str}") else: logger.error("no magics registered")