Source code for upsies.uis.tui.commands.set

"""
Change or show configuration file options
"""

import collections
import functools
import os
import textwrap

from .... import constants, defaults, errors, imagehosts, jobs, utils
from .base import CommandBase


[docs] class set(CommandBase): """ Change or show configuration file options Without any arguments, all options are listed with their current values. OPTION consists of three segments which are delimited with a period ("."). The first segment in OPTION is the INI file name without the extension. The second segment is the section name in that file. The third segment is the option name. List values are given as one argument per list item. If non-list values are given as multiple arguments, they are concatenated with single spaces. In the INI file, list items are delimited by one line break and one or more spaces (e.g. "\\n "). """ names = ('set',) cli_arguments = { 'OPTION': { 'nargs': '?', 'help': 'Option to change or show', }, 'VALUE': { 'nargs': '*', 'default': '', # FIXME: https://bugs.python.org/issue41854 'help': 'New value for OPTION', 'group': 'value', }, ('--reset', '-r'): { 'action': 'store_true', 'help': 'Reset OPTION to default value', 'group': 'value', }, ('--dump', '-d'): { # Accept zero or more sections defined in defaults.Config, e.g. "clients" or "trackers". 'type': utils.argtypes.one_of(defaults.Config.section_names), 'nargs': '*', 'metavar': 'FILE', 'help': ( f'Write current configuration to ' f'{utils.fs.tildify_path(os.path.join(constants.CONFIG_DIRECTORYPATH, "FILE"))}.ini\n' 'with commented-out default values\n' f'FILE may be ' + ", ".join(sorted(f'"{x}"' for x in defaults.Config.section_names)) + ', or it may be omitted to write all configuration files.\n' 'WARNING: You will lose any comments and custom structure in your .ini files.' ), 'group': 'value', }, ('--options',): { 'action': 'store_true', 'help': 'Show description of all configuration options', }, ('--fetch-ptpimg-apikey',): { 'nargs': 2, 'metavar': ('EMAIL', 'PASSWORD'), 'help': ( 'Fetch ptpimg API key and save it in ' f'{utils.fs.tildify_path(constants.IMGHOSTS_FILEPATH)}\n' '(EMAIL and PASSWORD are not saved)' ), }, } @functools.cached_property def jobs(self): if self.args.options: return (self.documentation_job,) elif self.args.fetch_ptpimg_apikey is not None: return (self.fetch_ptpimg_apikey_job,) else: return ( jobs.set.SetJob( config=self.config, option=self.args.OPTION, # VALUE is always a list. SetJob should handle this gracefully. value=self.args.VALUE, reset=self.args.reset, dump=self.dump_sections, ), ) @property def dump_sections(self): if self.args.dump: return self.args.dump elif self.args.dump is None: return () else: return tuple(self.config) @functools.cached_property def fetch_ptpimg_apikey_job(self): return jobs.custom.CustomJob( name='fetch-ptpimg-apikey', label='API key', worker=self.fetch_ptpimg_apikey, catch=(errors.RequestError, errors.ConfigError), ignore_cache=True, ) async def fetch_ptpimg_apikey(self, job): if len(self.args.fetch_ptpimg_apikey) <= 0: job.error('Missing EMAIL and PASSWORD') elif len(self.args.fetch_ptpimg_apikey) <= 1: job.error('Missing PASSWORD') elif len(self.args.fetch_ptpimg_apikey) > 2: unknown_args = ' '.join(self.args.fetch_ptpimg_apikey[2:]) job.error(f'Unrecognized arguments: {unknown_args}') else: email = self.args.fetch_ptpimg_apikey[0] password = self.args.fetch_ptpimg_apikey[1] ptpimg = imagehosts.imagehost('ptpimg') apikey = await ptpimg.get_apikey(email, password) job.add_output(apikey) self.config['imghosts']['ptpimg']['apikey'] = apikey self.config.write('imghosts') @functools.cached_property def documentation_job(self): return jobs.custom.CustomJob( name='documentation-job', label='Documentation', hidden=True, worker=self.display_documentation, ignore_cache=True, ) async def display_documentation(self, job): lines = [] for section_name, subsections in self.config.items(): for subsection_name, options in subsections.defaults.items(): for option_name in options: lines.extend( self._document_option(section_name, subsection_name, option_name) ) lines.append('') # Remove trailing empty line. del lines[-1] job.add_output('\n'.join(lines)) def _document_option(self, section, subsection, option): default = self.config[section][subsection].defaults[option] if isinstance(default, (int, float)): option_type_name = 'number' elif isinstance(default, str): option_type_name = 'string' elif isinstance(default, collections.abc.Sequence): option_type_name = 'list' elif hasattr(default, 'options'): option_type_name = 'choice' else: option_type_name = '' # Display option path and type. if option_type_name: lines = [f'{section}.{subsection}.{option} ({option_type_name})'] else: lines = [f'{section}.{subsection}.{option}'] if option_type_name == 'choice': lines.append(' Options: ' + ', '.join(default.options)) # Handle list value. if utils.is_sequence(default): if default: lines.append(f' Default: {default[0]}') lines.extend( f' {item}' for item in default[1:] ) else: lines.append(' Default: <empty>') else: lines.append(f' Default: {default}') # Add description indented. description = self.config[section][subsection].get_description(option) if description: description_lines = [] for paragraph in description.strip().split('\n'): description_lines.extend( textwrap.wrap( text=paragraph, width=72, ) ) lines.extend(' ' + line for line in description_lines) return lines