Skip to content

using typer in ipython magics¤

  • wrap function to consume system arguments using typer
  • register the magic with IPython
  • examples
  • ipython extensions

wrapping the function¤

we needs to process system arguments as a string when use magics. the line in a line magic or the first line in a cell magic may take arguments.

wrap_magic transforms our function into a magic style method that has the following signature.

def magicish(line: str, cell: str =None): ...

it uses click by way of typer to deserialize the line of system arguments. these parameters are passed to the original function. the magic function's help/docstring is replaced with the help from the command line interface.

    def wrap_magic(function, cell_key="cell"):
        import typer, click, shlex, functools
        app = typer.Typer(add_completion=False, context_settings={"help_option_names": ["-h", "--help"]})
        app.command()(function)
        ctx = click.Context(typer.main.get_command(app))

        @functools.wraps(function)
        def magic(line, cell=None):
            try:
                ctx.command.parse_args(ctx, shlex.split(line))
            except click.exceptions.Exit:
                return
            if cell_key in ctx.params:
                ctx.params[cell_key] = cell
            return function(**ctx.params)

        magic.__doc__ = "\n".join((function.__doc__ or "", get_help(ctx)))
        return magic

register the magic with IPython¤

register a line, cell or line/cell magic method.

    def register_magic(function, name=None,  cell_key = "cell"):
        import inspect
        from IPython import get_ipython
        shell = get_ipython()
        cache_rich_console()
        signature = inspect.signature(function)
        wrapper = wrap_magic(function, cell_key=cell_key)
        kind = "line"
        if cell_key in signature.parameters:
            kind = "line_cell"
            if signature.parameters[cell_key].default is inspect._empty:
                kind = "cell"
        shell.register_magic_function(wrapper, kind, name)
        shell.log.info(F"registered {repr(function)} as magic named {name or function.__name__}")
        return function

getting the help¤

when rich is installed, which it often is, we need to do some tomfoolery

in our interactive condition we want access to the rich console that typer uses. we're going to wrap a cache around typers method so that each we recieve the same rich.console.Console each time the function is invoked.

    def cache_rich_console(cache={}):
        import typer, functools
        if not cache:
            cache.setdefault("_get_rich_console", typer.rich_utils._get_rich_console)
        typer.rich_utils._get_rich_console = functools.lru_cache(cache["_get_rich_console"])

when the Console is consistent we can capture it's output and reclaim the help information

    def get_help(ctx):
        with typer.rich_utils._get_rich_console().capture() as console: ctx.get_help()
        return console.get()

examples¤

  • register a line magic
    if (Ø := "__file__" not in locals()):
        import typer
        @register_magic
        def hello(count:int = 5, name:str = typer.Option("world", help="a name to repeat"), msg: str = "<3"):
            """a function that says hello"""
            print(name*count, msg)
        assert "hello" not in (shell := get_ipython()).magics_manager.magics["cell"]
        %hello 

worldworldworldworldworld <3
  • register a cell magic because the cell parameter was found in the signature. this is by convention, and should be configurable.
    if Ø:
        @register_magic
        def yall(count:int = 5, name:str = typer.Option("world", help="a name to repeat"),  cell: str = "xoxo"):
            """a function that says hello to yall"""
            hello(count, name, msg=cell)
        assert "yall" in shell.magics_manager.magics["cell"], "the method didn't get registered."

verify that we can import register_magic for reuse.

    if Ø:
        with __import__("importnb").Notebook():
            from tonyfast.xxii.__typer_magic import register_magic as imported_register_magic
    def load_ipython_extension(shell): shell.user_ns.setdefault(register_magic.__name__, register_magic)
    def unload_ipython_extension(shell): pass