Source code for ycleptic.src.yclept

# Author: Cameron F. Abrams, <cfa22@drexel.edu>
"""
A class for handling specialized YAML-format input files
"""
from __future__ import annotations
import logging
import sys
import textwrap
from pathlib import Path
import yaml
from collections import UserDict
from argparse import Namespace
from .. import __version__
from .makedoc import make_doc
from .walkers import make_def, mwalk, dwalk

logger=logging.getLogger(__name__)

[docs] class Yclept(UserDict): """ A class for handling specialized YAML-format input files, including a base config file and an optional user config file. Inherits from :class:`collections.UserDict`. This class reads a base config file and an optional user config file to generate an overal instance configuration state. It allows for recursive processing of attributes and subattributes, and provides methods for generating documentation and interactive help. Parameters ---------- basefile : str The path to the base config file. userfile : str The path to the user config file. Optional; if omitted an empty user config is created. userdict : dict A dictionary of user-defined configurations. Optional; used instead of ``userfile`` when provided. rcfile : str The path to a resource config file that extends the base config. Optional. """ def __init__(self, basefile: str, userfile: str = '', userdict: dict = None, rcfile: str = ''): data = {} with open(basefile, 'r') as f: data["base"] = yaml.safe_load(f) if rcfile: with open(rcfile, 'r') as f: rc = yaml.safe_load(f) mwalk(data["base"], rc) super().__init__(data) if userdict is None: userdict = {} self["user"] = {} if userfile: with open(userfile, 'r') as f: self["user"] = yaml.safe_load(f) elif userdict: self["user"] = userdict dwalk(self["base"], self["user"]) self["basefile"] = basefile self["userfile"] = userfile self["rcfile"] = rcfile
[docs] def update_user(self, new_data: dict = None): """ Update the user configuration with new data. Parameters ---------- new_data : dict A dictionary containing the new user configuration data. """ if new_data is None: new_data = {} self["user"].update(new_data) dwalk(self["base"], self["user"])
[docs] def console_help(self, arglist: list[str], end: str = '', **kwargs): """ Interactive help with base config structure If Y is an initialized instance of Yclept, then >>> Y.console_help() will show the name of the top-level attributes and their respective help strings. Each positional argument will drill down another level in the base-config structure. """ f = kwargs.get('write_func', print) interactive_prompt = kwargs.get('interactive_prompt', '') exit_at_end = kwargs.get('exit_at_end', False) self.H = Namespace(base=self['base']['attributes'], write_func=f, arglist=arglist, end=end, interactive_prompt=interactive_prompt, exit=exit_at_end) self._help()
[docs] def make_doctree(self, topname: str = 'config_ref', footer_style: str = 'paragraph'): """ Generates a Sphinx-style documentation tree from the base config file, including a root node. ``topname`` may be a bare name (``config_ref``) or a path (``docs/source/config_ref``), allowing the command to be run from any directory without a prior ``cd``. """ top = Path(topname) rootdir = str(top.parent.resolve()) doc = self['base'].get('docs', {}) with open(f'{topname}.rst', 'w') as f: make_doc(self['base']['attributes'], top.name, 'Top-level attributes', f, docname=doc.get('title', ''), doctext=doc.get('text', ''), docexample=doc.get('example', {}), rootdir=rootdir, footer_style=footer_style)
[docs] def dump_user(self, filename: str = 'complete-user.yaml'): """ Generates a full dump of the processed user config, including all implied default values """ with open(filename, 'w') as f: f.write(f'# Ycleptic v {__version__}\n') f.write('# Dump of complete user config file\n') yaml.dump(self['user'], f)
[docs] def make_default_specs(self, *args): """ Generates a partial config based on NULL user input and specified hierarchy Parameters ---------- args : str The names of the attributes to include in the partial config. """ holder = {} make_def(self['base']['attributes'], holder, *args) return holder
def _show_item(self, idx: int): H: Namespace = self.H item = H.base[idx] end = H.end H.write_func(f'\n{item["name"]}:{end}') H.write_func(f' {textwrap.fill(item["text"], subsequent_indent=" ")}{end}') if item["type"] != "dict": if "default" in item: H.write_func(f' default: {item["default"]}{end}') if "choices" in item: H.write_func(f' allowed values: {", ".join([str(_) for _ in item["choices"]])}{end}') if item.get("required",False): H.write_func(f' A value is required.{end}') else: if "default" in item: H.write_func(f' default:{end}') for k,v in item["default"].items(): H.write_func(f' {k}: {v}{end}') def _endhelp(self): self.H.write_func('Thank you for using ycleptic\'s interactive help!') sys.exit(0) def _show_path(self): self.H.write_func('\nbase|' + '->'.join(self.path)) def _show_branch(self, idx: int, interactive: bool = False): self._show_path() self._show_item(idx) self._show_subattributes(interactive=interactive) def _show_leaf(self, idx: int): self._show_path() self._show_item(idx) def _show_subattributes(self, interactive: bool = False): H: Namespace = self.H subds = [x["name"] for x in H.base] hassubs = ['attributes' in x for x in H.base] att = [''] * len(subds) if interactive: subds += ['..', '!'] att += [' up', ' quit'] hassubs += [False, False] for m, h, a in zip(subds, hassubs, att): if h: c = ' ->' else: c = '' H.write_func(f' {m}{c}{a}') def _get_help_choice(self, init_list: list[str]): H: Namespace = self.H if len(init_list) > 0: choice = init_list.pop() else: choice = '!' if H.interactive_prompt != '': choice = input(H.interactive_prompt) while choice == '' or not choice in [x["name"] for x in H.base] + ['..', '!']: if choice != '': H.write_func(f'{choice} not recognized.') if len(init_list) > 0: choice = init_list.pop() else: choice = '!' if H.interactive_prompt != '': choice = input(H.interactive_prompt) return choice def _help(self): H: Namespace = self.H self.basestack = [] self.path = [] init_keylist = H.arglist[::-1] if len(init_keylist) == 0: self._show_subattributes(H.interactive_prompt != '') choice = self._get_help_choice(init_keylist) while choice != '!': if choice == '..': if len(self.basestack) == 0: if H.exit: self._endhelp() return H.base = self.basestack.pop() if len(self.path) > 0: self.path.pop() else: downs = [x["name"] for x in H.base] idx = downs.index(choice) if len(init_keylist) == 0: self._show_item(idx) if 'attributes' in H.base[idx]: # this is not a leaf, but we just showed it # so we history the base and reassign it self.basestack.append(H.base) self.path.append(choice) H.base = H.base[idx]['attributes'] else: # this is a leaf, and we just showed it, # so we can dehistory it but keep the base # since it might have more leaves to select H.write_func(f'\nAll subattributes at the same level as \'{choice}\':') if len(init_keylist) == 0: self._show_path() self._show_subattributes(H.interactive_prompt != '') if H.interactive_prompt == '': return choice = self._get_help_choice(init_keylist) if H.exit: self._endhelp() return