Source code for aiida.tools.ipython.ipython_magics

###########################################################################
# Copyright (c), The AiiDA team. All rights reserved.                     #
# This file is part of the AiiDA code.                                    #
#                                                                         #
# The code is hosted on GitHub at https://github.com/aiidateam/aiida-core #
# For further information on the license, see the LICENSE.txt file        #
# For further information please visit http://www.aiida.net               #
###########################################################################
"""An IPython extension that provides a magic command to load
basic aiida commands.

This makes it much easier to start.

Produces output in:

* Plaintext (IPython [qt]console)
* HTML (IPython notebook, ``nbconvert --to html``, ``--to slides``)
* JSON (IPython notebook ``.ipynb`` files)
* LaTeX (e.g. ``ipython nbconvert example.ipynb --to LaTeX --post PDF``)


Notes on how to load it at start:
https://ipython.org/ipython-doc/3/config/intro.html

Usage
======

.. sourcecode:: ipython

   In [1]: %load_ext aiida_magic

   In [2]: %aiida
"""

import json
import shlex

from IPython import get_ipython, version_info  # type: ignore[attr-defined]
from IPython.core import magic


[docs] def add_to_ns(local_ns, name, obj): """Add a new variable with name ``name`` and value ``obj`` to the namespace ``local_ns``, optionally showing a warning if we are hiding an existing variable. .. todo:: implement the warning. Example:: # assuming that local_ns is a dictionary, e.g. from locals() import sys add_to_ns(local_ns, 'sys', sys) """ if name in local_ns: pass local_ns[name] = obj
[docs] @magic.magics_class class AiiDALoaderMagics(magic.Magics): """AiiDA magic loader."""
[docs] @magic.needs_local_scope @magic.line_magic def verdi(self, line='', local_ns=None): """Run the AiiDA command line tool, using the currently loaded configuration and profile. Invoking ``verdi`` normally through the command line follows a different code path, compared to calling it directly from within an active Python interpreter. Some of those code paths we actually need here, and others can actually cause problems: * The ``VerdiCommandGroup`` group ensures that the context ``obj`` is set with the loaded ``Config`` and ``Profile``, but this is not done in this manual call, so we have to built it ourselves and pass it in with the ``obj`` keyword. * We cannot call the ``aiida.cmdline.commands.cmd_verdi.verdi`` command directly, as that will invoke the ``aiida.cmdline.parameters.types.profile.ProfileParamType`` parameter used for the ``-p/--profile`` option that is defined on the ``verdi`` command. This will attempt to load the default profile, but here a profile has actually already been loaded. In principle, reloading a profile should not be a problem, but this magic is often used in demo notebooks where a temporary profile was created and loaded, which is not properly added to the config file (since it is temporary) and so loading the profile would fail. The solution is to not call the top-level ``verdi`` command, but the subcommand, which is the first parameter in the command line arguments defined by the ``line`` argument. """ from click import Context from aiida.cmdline.commands.cmd_verdi import verdi from aiida.common import AttributeDict from aiida.manage import get_config, get_profile config = get_config() profile = get_profile() # The ``line`` should be of the form ``process list -a -p1``, i.e., a ``verdi`` subcommand with some optional # parameters. Validate that the first entry is indeed a subcommand and not the flag to specify the profile which # is not supported in this magic method. cmdline_arguments = shlex.split(line) command_name = cmdline_arguments[0] if command_name in ('-p', '--profile'): raise ValueError( 'The `-p/--profile` option is not supported for the `%verdi` magic operator. It will use the currently ' f'loaded profile `{profile}`' ) # Construct the subcommand that will be executed, thereby circumventing the profile option of ``verdi`` itself. # If the caller specified a subcommand that doesn't exist, the following will raise an exception. context = Context(verdi) command = verdi.get_command(context, command_name) if command is None: raise RuntimeError(f'command `{command_name}` not found.') return command( cmdline_arguments[1:], prog_name='%verdi', obj=AttributeDict({'config': config, 'profile': profile}), standalone_mode=False, )
[docs] @magic.needs_local_scope @magic.line_magic def aiida(self, line='', local_ns=None): """Load AiiDA in ipython (checking if it was already loaded), and inserts in the namespace the main AiiDA classes (the same that are loaded in ``verdi shell``. Usage:: %aiida [optional parameters] .. todo:: implement parameters, e.g. for the profile to load. """ from aiida.cmdline.utils.shell import get_start_namespace from aiida.manage.configuration import load_profile self.is_warning = False lcontent = line.strip() if lcontent: profile = load_profile(lcontent) else: profile = load_profile() self.current_state = f'Loaded AiiDA DB environment - profile name: {profile.name}.' user_ns = get_start_namespace() for key, value in user_ns.items(): add_to_ns(local_ns, key, value) return self
[docs] def _repr_json_(self): """Output in JSON format.""" obj = {'current_state': self.current_state} if version_info[0] >= 3: return obj return json.dumps(obj)
[docs] def _repr_html_(self): """Output in HTML format.""" html = '<p>' if self.is_warning: html += '<strong>' html += self.current_state if self.is_warning: html += '</strong>' html += '</p>' return html
[docs] def _repr_latex_(self): """Output in LaTeX format.""" if self.is_warning: latex = '\\emph{%s}\n' % self.current_state else: latex = f'{self.current_state}\n' return latex
[docs] def _repr_pretty_(self, pretty_print, cycle): """Output in text format.""" if self.is_warning: warning_str = '** ' else: warning_str = '' text = f'{warning_str}{self.current_state}\n' pretty_print.text(text)
[docs] def load_ipython_extension(ipython): """Registers the %aiida IPython extension. .. deprecated:: v3.0.0 Use :py:func:`~aiida.tools.ipython.ipython_magics.register_ipython_extension` instead. """ register_ipython_extension(ipython)
[docs] def register_ipython_extension(ipython=None): """Registers the %aiida IPython extension. The %aiida IPython extension provides the same environment as the `verdi shell`. :param ipython: InteractiveShell instance. If omitted, the global InteractiveShell is used. """ if ipython is None: ipython = get_ipython() ipython.register_magics(AiiDALoaderMagics)