Source code for damona.config

#  This file is part of Damona software
#
#  Copyright (c) 2020-2021 - Damona Development Team
#
#  File author(s):
#      Thomas Cokelaer <thomas.cokelaer@pasteur.fr>
#
#  Distributed under the terms of the 3-clause BSD license.
#  The full license is in the LICENSE file, distributed with this software.
#
#  website: https://github.com/cokelaer/damona
#  documentation: http://damona.readthedocs.io
#
##############################################################################
"""The Damona configuration"""
import pathlib
import sys

import colorlog
from easydev import CustomConfig

import damona.shell

logger = colorlog.getLogger(__name__)


__all__ = ["Config", "get_damona_commands"]


[docs] class Config: """A place holder to store our configuration file and shell scripts This class is called each time damona is started. The config file, if not present is created, otherwise nothing happens. Same for the bash and fish shell configuration files The damona configuration file looks like:: [general] quiet=False [zenodo] token=APmm6p.... orcid=0000-0001 name='Cokelaer, Thomas' affiliation='Institut Pasteur' [sandbox.zenodo] token=FFmbAEhQbb... orcid=0000-0001 name='Cokelaer, Thomas' affiliation='Institut Pasteur' Where the urls section can be used to store aliases to external registry. When installing software using:: damona install example --url damona if the alias damona is in the [urls] section, it is replaced by its real value (https://...) the URL must end with the expected registry name **registry.txt** The zenodo section is not save by default since it is for developpers only. """ def __init__(self, name="damona", urls={"damona": "https://biomics.pasteur.fr/salsa/damona/registry.txt"}): """.. rubric:: **Constructor** Creates ``~/.config/damona/damona.cfg`` and the shell initialisation scripts the first time it is called; subsequent calls are no-ops for files that already exist and are up-to-date. :param str name: Config name used as the sub-directory under the OS config dir (default ``"damona"``). Override in tests to avoid touching the user's real config. :param dict urls: Mapping of alias → URL to pre-populate the ``[urls]`` section of a freshly created config file. """ configuration = CustomConfig(f"{name}", verbose=True) # let us add a damona.cfg in it. This will store URLs to look for singularities # This is done only once to not overwrite user options self.user_config_dir = pathlib.Path(configuration.user_config_dir) self.config_file = self.user_config_dir / "damona.cfg" self.user_config_dir.mkdir(parents=True, exist_ok=True) if not self.config_file.exists(): with open(self.config_file, "w") as fout: fout.write("[general]\n") fout.write("verbose=True\n\n") fout.write("[urls]\n") for k, v in urls.items(): fout.write("{}={}".format(k, v)) else: logger.debug("damona.cfg file exists already. Reading it") # read the config self.read() # create the shell script once for all bash_created = self.add_bash() fish_created = self.add_fish() zsh_created = self.add_zsh() # Auto-configure shell RC files so users don't need to add source lines manually. # Only do this for the production config (not test configs). if name == "damona": self._init_bash_rc() self._init_fish_rc() self._init_zsh_rc() if bash_created or fish_created or zsh_created: # pragma: no cover logger.critical( "Please start a new shell to benefit from " "the configuration file and activate/deactivate command" ) sys.exit(1)
[docs] def read(self): """Reads the config file""" from configparser import ConfigParser config = ConfigParser() config.read(str(self.config_file)) self.config = config
def _copy_shell_file(self, source, dest): """Copy source shell file to dest, returning True if the file was created or updated.""" shell_path = pathlib.Path(damona.shell.__path__[0]) new_content = (shell_path / source).read_text() dest_path = self.user_config_dir / dest if dest_path.exists(): existing_content = dest_path.read_text() if existing_content == new_content: return False logger.warning(f"Updating {dest} in {self.user_config_dir}.") else: logger.warning(f"Creating {dest} file in {self.user_config_dir}.") dest_path.write_text(new_content) return True
[docs] def add_bash(self): # pragma: no cover """Copy the Bash shell init script to the user config directory. :returns: ``True`` if the file was created or updated, ``False`` otherwise. :rtype: bool """ return self._copy_shell_file("bash/damona.sh", "damona.sh")
[docs] def add_zsh(self): # pragma: no cover """Copy the Zsh shell init script to the user config directory. :returns: ``True`` if the file was created or updated, ``False`` otherwise. :rtype: bool """ return self._copy_shell_file("zsh/damona.zsh", "damona.zsh")
[docs] def add_fish(self): """Copy the Fish shell init script to the user config directory. :returns: ``True`` if the file was created or updated, ``False`` otherwise. :rtype: bool """ return self._copy_shell_file("fish/damona.fish", "damona.fish")
def _update_shell_rc(self, rc_file, source_line, init_block, shell_name): """Append init_block to rc_file if source_line is not already present. Returns True if the file was modified, False if already configured or if the operation failed. """ rc_path = pathlib.Path(rc_file).expanduser() try: rc_path.parent.mkdir(parents=True, exist_ok=True) if rc_path.exists() and source_line in rc_path.read_text(): return False with open(rc_path, "a") as fout: fout.write(init_block) logger.warning( f"Added damona {shell_name} initialization to {rc_file}. " "Please start a new terminal for the changes to take effect." ) return True except Exception as e: logger.debug(f"Could not update {rc_file}: {e}") return False def _init_bash_rc(self): """Add damona bash initialization to ~/.bashrc if not already present.""" block = ( "\n# Added by Damona\n" "if [ -f ~/.config/damona/damona.sh ] ; then\n" " source ~/.config/damona/damona.sh\n" "fi\n" ) return self._update_shell_rc("~/.bashrc", "source ~/.config/damona/damona.sh", block, "bash") def _init_zsh_rc(self): """Add damona zsh initialization to ~/.zshrc if not already present.""" block = ( "\n# Added by Damona\n" "if [ -f ~/.config/damona/damona.zsh ] ; then\n" " source ~/.config/damona/damona.zsh\n" "fi\n" ) return self._update_shell_rc("~/.zshrc", "source ~/.config/damona/damona.zsh", block, "zsh") def _init_fish_rc(self): """Add damona fish initialization to ~/.config/fish/config.fish if not already present.""" block = ( "\n# Added by Damona\n" "if test -f ~/.config/damona/damona.fish\n" " source ~/.config/damona/damona.fish\n" "end\n" ) return self._update_shell_rc( "~/.config/fish/config.fish", "source ~/.config/damona/damona.fish", block, "fish" )
[docs] def get_damona_commands(): """Print commands available in Damona if not hidden. This function is used for the fish completion""" from damona import script commands = [x for x in dir(script) if "allow_interspersed_args" in dir(getattr(script, x))] commands = [x for x in commands if not getattr(script, x).hidden] commands = [x for x in commands if x != "main"] print("\n".join(commands))