Source code for pyssianutils.initialize

"""
The initialize module defines all the parser commands (and their behaviour)
related with the pyssianutils data and provides methods for easy access to said
resources.
"""
import shutil
import platformdirs
import platform
import warnings
import tarfile
import configparser
import importlib.resources

from pathlib import Path
from functools import cache

# Typing imports
import argparse

PKGDEFAULTNAME = 'pyssianutils_appdata'

# Utility Functions
[docs] @cache def get_appdir(use_home:bool=True) -> Path: """ If a pyssianutils app data exists return it. Otherwise return a suitable path for its creation. Parameters ---------- use_home : bool For Linux, to facilitate the access to the configuration files or the templates stored in the app data a True value of this parameter will favor returning a $HOME/.pyssianutils location instead of the XDG recommended one in $HOME/.local/shared/pyssianutils. Returns ------- Path path to the app data folder if it exists. Otherwise returns a suitable path for it. Raises ------ RuntimeError Two pyssianutils appdata directories were found """ system = platform.system() if system == 'Linux': home_path = Path.home()/'.pyssianutils' xdg_path = Path(platformdirs.user_data_dir(str(__package__))) if home_path.exists() and xdg_path.exists(): raise RuntimeError(f"""Two pyssianutils app data directories have been found. Please manually remove one of them: {home_path} and {xdg_path}""") elif home_path.exists(): return home_path elif xdg_path.exists(): return xdg_path elif use_home: return home_path return Path(platformdirs.user_data_dir(str(__package__)))
[docs] @cache def get_resourcesdir() -> Path: """ Returns the path to the resources as they were installed. Files in this directory should not be changed and are removed upon pip uninstall. Returns ------- Path path to the resources folder. """ resourcesdir = importlib.resources.files(__package__)/'resources' return resourcesdir
def pack_appdir(appdir:Path,target:Path=Path(f'{PKGDEFAULTNAME}.tar')): print(f'Creating {target}') with tarfile.open(target,'w') as tar: tar.add(appdir/'defaults.ini','defaults.ini') tar.add(appdir/'templates/','templates/') print(' finished') def unpack_appdir(ifile:Path,location:Path): tar = tarfile.open(ifile) members = ['defaults.ini',] members += [m for m in tar.getmembers() if m.name.startswith("templates/")] tar.extractall(path=location, members=members, filter='data') tar.close()
[docs] @cache def check_initialization(): """ Ensures that pyssianutils was initialized. If it was not it errors. Raises ------ RuntimeError pyssianutils was not initialized """ appdir = get_appdir() if not appdir.exists(): raise RuntimeError(f"{appdir} does not exist. Please ensure to run pyssianutils 'init'")
[docs] @cache def load_app_defaults(only_resources:bool=False) -> configparser.ConfigParser: """ Looks for the configuration files in the resources and in the user's app directory, loads and returns them Parameters ---------- only_resources : bool, optional If True, it ignores the defaults of the user and returns the ones that came packaged with pyssianutils, by default False Returns ------- configparser.ConfigParser configuration defaults of pyssianutils for a given user """ defaults = configparser.ConfigParser(inline_comment_prefixes=(';',)) # Order is important as we want to override any packaged defaults with the # User's defaults if only_resources: defaults.read([get_resourcesdir()/'defaults.ini']) else: defaults.read([get_resourcesdir()/'defaults.ini', get_appdir()/'defaults.ini']) return defaults
# Main APIs init_description = """ Should be run after a fresh installation. Creates the local directories where the user's app data and defaults will be stored """ init_parser = argparse.ArgumentParser(description=init_description) init_parser.add_argument("--unpack", metavar='PACKAGE',dest='package',default=None, help="package containing the configuration of a " "previous pyssianutils setup. Obtained through " "'pyssianutils pack myfile.pack'") init_parser.add_argument("--force",action='store_true',default=False, help="overwrite any previous pyssianutils " "application data if it already existed") if platform.system() == 'Linux': init_parser.add_argument('--xdg-default-appdir', dest='use_home',default=True,action='store_false', help="If enabled it will store pyssianutils data " "at $HOME/.local/share/pyssianutils instead of " "at $HOME/.pyssianutils") else: init_parser.set_defaults(use_home=False) def init_main( package:None|Path|str=None, force:bool=False, use_home:bool=False, ): appdir = get_appdir(use_home) if appdir.exists() and not force: raise RuntimeError('A previously existing app data directory was found') if appdir.exists(): warnings.warn('overwriting previously existing pyssianutils app data') shutil.rmtree(appdir) appdir.mkdir(exist_ok=True) if package is not None: unpack_appdir(package,appdir) else: resources = get_resourcesdir() shutil.copy(resources/'defaults.ini',appdir) shutil.copytree(resources/'templates',appdir/'templates') clean_description = ( "Removes the app data files and local directory where the pyssianutils data was" " stored. This is recommended before uninstalling the software" ) clean_parser = argparse.ArgumentParser(description=clean_description) def clean_main(): appdir = get_appdir() try: shutil.rmtree(appdir) except FileNotFoundError: warnings.warn(f'pyssianutils installation at {appdir} was not found so nothing to clean') pack_description = ( "Packages the user app data into a tar file that can be used by pyssianutils init " "to simplify exporting the user defaults and templates to a new machine." ) pack_parser = argparse.ArgumentParser(description=pack_description) pack_parser.add_argument('name',nargs='?', default=PKGDEFAULTNAME, help="name of the file that will contain the user's configuration") pack_parser.add_argument('--outputdir','-O', default=None, help="Directory where the file will be created. " "Defaults to the CWD") def pack_main(name:str, outputdir:Path|None): if outputdir is None: outputdir = Path.cwd() appdir = get_appdir() pack_appdir(appdir,target=outputdir/f'{name}.tar') defaults_description = ( "Allows the display of default values and their modification from the command line." ) defaults_parser = argparse.ArgumentParser(description=defaults_description) defaults_subparsers = defaults_parser.add_subparsers(help='sub-command help',dest='subcommand') show_subparser = defaults_subparsers.add_parser('show', help="Prints in the console the " "avaliable/selected default parameters " "and/or sections in which those " "parameters are grouped") show_subparser.add_argument('parameter', nargs='?', default=None, help="name of the parameter whose value should " "be displayed. 'all' will show all parameters.") show_subparser.add_argument('--section', default=None, help="Allows to filter the parameters for a " "single section. If no section, no parameter " "and no other flag is requested a list of the " "sections available will be displayed.") show_subparser.add_argument('--all', dest='show_all', default=False,action='store_true', help='show all parameters') show_subparser.add_argument('--path', dest='show_path', default=False,action='store_true', help="shows the path to the 'defaults.ini' file") set_subparser = defaults_subparsers.add_parser('set', help="sets/changes the value of a default") set_subparser.add_argument('parameter', help="name of the parameter whose value should set") set_subparser.add_argument('value', help="value to set in the parameter") set_subparser.add_argument('--section', default=None, help="Section of the parameter. If none provided " "it will attempt to guess it. From guessing it if " "there is only one parameter with the provided name " "it will set the new value. Otherwise it will refuse " "and will show the sections containing that parameter " "name") set_subparser.add_argument('--all', dest='update_all', default=False,action='store_true', help="If enabled it forces the update on all " "parameters with the same name") reset_subparser = defaults_subparsers.add_parser('reset', help="resets the value of a default") reset_subparser.add_argument('parameter', nargs='?',default=None, help="name of the parameter whose value should reset. " "If none is provided it will reset all user defaults") reset_subparser.add_argument('--section', default=None, help="Section of the parameter. If none provided " "it will attempt to guess it. From guessing it if " "there is only one parameter with the provided name " "it will set the new value. Otherwise it will refuse " "and will show the sections containing that parameter " "name") reset_subparser.add_argument('--all', dest='update_all', default=False,action='store_true', help="If enabled it forces the update on all " "parameters with the same name") def defaults_main(subcommand:str, **kwargs): if subcommand == 'show': _defaults_show(**kwargs) if subcommand == 'set': _defaults_set(**kwargs) if subcommand == 'reset': _defaults_reset(**kwargs) def _defaults_show(parameter:str|None, section:str|None, show_all:bool=False, show_path:bool=False): if show_path: app_defaults = get_resourcesdir()/'defaults.ini' defaults_path = get_appdir()/'defaults.ini' print(f'packaged defaults ( do not modify): {app_defaults}') if defaults_path.exists(): print(f' user defaults (free to modify): {defaults_path}') else: print(f' user defaults (free to modify): Not Found, please run "pyssianutils init"') return defaults = load_app_defaults() if parameter is None and section is None: for s in defaults.sections(): _show_section(s) if show_all: _show_section_keywords(defaults,s) elif section is not None: _check_section(defaults,section) if parameter is None: _show_section_keywords(defaults,section,indentation=0) elif parameter in defaults.options(section): _show_keyword(parameter,defaults[section][parameter],indentation=0) else: raise ValueError(f'parameter "{parameter}" is not in section "{section}"') else: for s in defaults.sections(): if parameter in defaults.options(s): _show_section(s) _show_keyword(parameter,defaults[s][parameter]) def _check_section(defaults,section): if section not in defaults.sections(): msg = f"subsection={section} not in defaults. Please run 'pyssianutils default show'" raise ValueError(msg) def _show_section(section,indentation=0): spacer = ' '*4 print(f'{indentation*spacer}[{section}]') def _show_keyword(keyword,value,indentation=1): spacer = ' '*4 print(f'{indentation*spacer}{keyword} = {value}') def _show_section_keywords(defaults,section,indentation=1): for k in defaults.options(section): _show_keyword(k,defaults[section][k],indentation=indentation) def _defaults_set(parameter:str, value:str, section:str|None=None, update_all:bool=False): appdir = get_appdir() defaults_filepath = appdir/'defaults.ini' defaults = load_app_defaults() matches = [] if section is None: for s in defaults.sections(): if parameter in defaults.options(s): matches.append((s,parameter)) else: if parameter in defaults.options(section): matches.append((section,parameter)) if len(matches) < 0: raise ValueError(f'parameter={parameter} does not exist') elif len(matches) > 1 and not update_all: print(f'parameter={parameter} is present in more than one section:') print('\n'.join([f' {s}' for s,_ in matches])) warnings.warn('The program cowardly refused to update the defaults values') elif len(matches) > 1: for s,p in matches: print(f' setting [{s}][{p}] = {value}') defaults[s][p] = value else: s,p = matches[0] print(f' setting [{s}][{p}] = {value}') defaults[s][p] = value print('storing new defaults') with open(defaults_filepath,'w') as F: defaults.write(F) def _defaults_reset(parameter:str|None=None, section:str|None=None, update_all:bool=False): appdir = get_appdir() defaults_filepath = appdir/'defaults.ini' defaults = load_app_defaults() defaults_base = load_app_defaults(only_resources=True) matches = [] if section is None and parameter is None: for s in defaults.sections(): for p in defaults.options(s): matches.append((s,p)) elif parameter is None: for p in defaults.options(section): matches.append((section,p)) elif section is None: for s in defaults.sections(): if parameter in defaults.options(s): matches.append((s,parameter)) else: if parameter in defaults.options(section): matches.append((section,parameter)) if len(matches) == 0: raise ValueError(f'parameter={parameter} does not exist') elif len(matches) > 1 and parameter is not None and not update_all: print(f'parameter={parameter} is present in more than one section:') print('\n'.join([f' {s}' for s,_ in matches])) warnings.warn('The program cowardly refused to update the defaults values') elif len(matches) > 1: for s,p in matches: value = defaults_base[s][p] print(f' resetting [{s}][{p}] = {value}') defaults[s][p] = value else: s,p = matches[0] value = defaults_base[s][p] print(f' resetting [{s}][{p}] = {value}') defaults[s][p] = value print('storing new defaults') with open(defaults_filepath,'w') as F: defaults.write(F)