This commit is contained in:
Jörn-Michael Miehe 2020-08-13 10:48:01 +02:00
parent 1bd547587b
commit 0b2b9a0a75
8 changed files with 108 additions and 23 deletions

View file

@ -8,6 +8,8 @@ import kiwi
def set_verbosity(logger, handler, verbosity): def set_verbosity(logger, handler, verbosity):
"""set logging default verbosity level and format"""
if verbosity >= 2: if verbosity >= 2:
log_level = logging.DEBUG log_level = logging.DEBUG
log_format = "[%(asctime)s] %(levelname)s @ %(filename)s:%(funcName)s:%(lineno)d: %(message)s" log_format = "[%(asctime)s] %(levelname)s @ %(filename)s:%(funcName)s:%(lineno)d: %(message)s"
@ -23,10 +25,12 @@ def set_verbosity(logger, handler, verbosity):
def main(): def main():
# add a new handler (needed to set the level)
log_handler = logging.StreamHandler() log_handler = logging.StreamHandler()
logging.getLogger().addHandler(log_handler) logging.getLogger().addHandler(log_handler)
set_verbosity(logging.getLogger(), log_handler, kiwi.verbosity()) set_verbosity(logging.getLogger(), log_handler, kiwi.verbosity())
# run the app
kiwi.run() kiwi.run()

View file

@ -4,11 +4,13 @@ from .runner import Runner
def verbosity(): def verbosity():
# ensure singleton is instantiated: runs subcommand setup routines
_ = Runner() _ = Runner()
return Parser().get_args().verbosity return Parser().get_args().verbosity
def run(): def run():
# pass down
Runner().run() Runner().run()

View file

@ -1,5 +1,7 @@
# system # system
import os import os
# location of "src" directory to use
KIWI_ROOT = os.getenv('KIWI_ROOT', ".") KIWI_ROOT = os.getenv('KIWI_ROOT', ".")
# default name of kiwi-config file
KIWI_CONF_NAME = os.getenv('KIWI_CONF_NAME', "kiwi.yml") KIWI_CONF_NAME = os.getenv('KIWI_CONF_NAME', "kiwi.yml")

View file

@ -11,15 +11,25 @@ from ._constants import KIWI_ROOT, KIWI_CONF_NAME
########### ###########
# CONSTANTS # CONSTANTS
# text files inside kiwi-config "src" directory
HEADER_KIWI_CONF_NAME = f"{KIWI_ROOT}/kiwi_header.yml" HEADER_KIWI_CONF_NAME = f"{KIWI_ROOT}/kiwi_header.yml"
DEFAULT_KIWI_CONF_NAME = f"{KIWI_ROOT}/kiwi_default.yml" DEFAULT_KIWI_CONF_NAME = f"{KIWI_ROOT}/kiwi_default.yml"
VERSION_TAG_NAME = f"{KIWI_ROOT}/version-tag" VERSION_TAG_NAME = f"{KIWI_ROOT}/version-tag"
class Config: class Config:
"""represents a kiwi.yml"""
__yml_content = {} __yml_content = {}
def __key_resolve(self, key): def __key_resolve(self, key):
"""
Resolve nested dictionaries
If __yml_content is {'a': {'b': {'c': "val"}}} and key is 'a:b:c',
this returns a single dict {'c': "val"} and the direct key 'c'
"""
# "a:b:c" => path = ['a', 'b'], key = 'c' # "a:b:c" => path = ['a', 'b'], key = 'c'
path = key.split(':') path = key.split(':')
path, key = path[:-1], path[-1] path, key = path[:-1], path[-1]
@ -32,14 +42,20 @@ class Config:
return container, key return container, key
def __getitem__(self, key): def __getitem__(self, key):
"""array-like read access to __yml_content"""
container, key = self.__key_resolve(key) container, key = self.__key_resolve(key)
return container[key] return container[key]
def __setitem__(self, key, value): def __setitem__(self, key, value):
"""array-like write access to __yml_content"""
container, key = self.__key_resolve(key) container, key = self.__key_resolve(key)
container[key] = value container[key] = value
def __str__(self): def __str__(self):
"""dump into textual representation"""
# dump yml content # dump yml content
yml_string = yaml.dump(self.__yml_content, default_flow_style=False, sort_keys=False) yml_string = yaml.dump(self.__yml_content, default_flow_style=False, sort_keys=False)
@ -53,44 +69,68 @@ class Config:
return yml_string return yml_string
def _update_from_file(self, filename): def _update_from_file(self, filename):
"""return a copy updated using a kiwi.yml file"""
with open(filename, 'r') as stream: with open(filename, 'r') as stream:
try: try:
self.__yml_content.update(yaml.safe_load(stream)) # create copy
except yaml.YAMLError as exc:
logging.error(exc)
def clone(self):
result = Config() result = Config()
result.__yml_content = copy.deepcopy(self.__yml_content) result.__yml_content = copy.deepcopy(self.__yml_content)
# read file
logging.debug(f"Reading '{filename}' into '{id(result.__yml_content)}'")
result.__yml_content.update(yaml.safe_load(stream))
return result return result
except yaml.YAMLError as exc:
logging.error(exc)
def save(self): def save(self):
"""save current yml representation in current directory's kiwi.yml"""
with open(KIWI_CONF_NAME, 'w') as stream: with open(KIWI_CONF_NAME, 'w') as stream:
stream.write(str(self)) stream.write(str(self))
class DefaultConfig(Config): class DefaultConfig(Config):
"""Singleton: The default kiwi.yml file"""
__instance = None __instance = None
@classmethod @classmethod
def get(cls): def get(cls):
if cls.__instance is None: if cls.__instance is None:
cls.__instance = cls() # create singleton
cls.__instance._update_from_file(DEFAULT_KIWI_CONF_NAME) cls.__instance = cls()._update_from_file(DEFAULT_KIWI_CONF_NAME)
# add version data from separate file (keeps default config cleaner)
with open(VERSION_TAG_NAME, 'r') as stream: with open(VERSION_TAG_NAME, 'r') as stream:
cls.__instance["version"] = stream.read().strip() cls.__instance['version'] = stream.read().strip()
# return singleton
return cls.__instance return cls.__instance
class LoadedConfig(Config): class LoadedConfig(Config):
"""Singleton collection: kiwi.yml files by path"""
__instances = {}
@classmethod @classmethod
def get(cls): def get(cls):
result = DefaultConfig.get().clone() cwd = os.getcwd()
if os.path.isfile(KIWI_CONF_NAME): if cwd not in LoadedConfig.__instances:
result._update_from_file(KIWI_CONF_NAME) # create singleton for new path
result = DefaultConfig.get()
return result # update with current dir's kiwi.yml
try:
result = result._update_from_file(KIWI_CONF_NAME)
except FileNotFoundError:
logging.info(f"No '{KIWI_CONF_NAME}' found at '{cwd}'. Using defaults.")
LoadedConfig.__instances[cwd] = result
# return singleton
return LoadedConfig.__instances[cwd]

View file

@ -3,31 +3,39 @@ import argparse
class Parser: class Parser:
"""Singleton: Main CLI arguments parser"""
class __Parser: class __Parser:
"""Singleton type"""
# argparse objects
__parser = None __parser = None
__subparsers = None __subparsers = None
__args = None __args = None
def __init__(self): def __init__(self):
self.__parser = argparse.ArgumentParser(description='kiwi-config') # create main parsers
self.__parser = argparse.ArgumentParser(
description='kiwi-config'
)
# main arguments
self.__parser.add_argument( self.__parser.add_argument(
'-v', '--verbosity', '-v', '--verbosity',
action='count', default=0 action='count', default=0
) )
# attach subparsers
self.__subparsers = self.__parser.add_subparsers() self.__subparsers = self.__parser.add_subparsers()
self.__subparsers.required = True self.__subparsers.required = True
self.__subparsers.dest = 'command' self.__subparsers.dest = 'command'
def get_parser(self):
return self.__parser
def get_subparsers(self): def get_subparsers(self):
return self.__subparsers return self.__subparsers
def get_args(self): def get_args(self):
if self.__args is None: if self.__args is None:
# parse args if needed
self.__args = self.__parser.parse_args() self.__args = self.__parser.parse_args()
return self.__args return self.__args
@ -36,7 +44,9 @@ class Parser:
def __init__(self): def __init__(self):
if Parser.__instance is None: if Parser.__instance is None:
# create singleton
Parser.__instance = Parser.__Parser() Parser.__instance = Parser.__Parser()
def __getattr__(self, item): def __getattr__(self, item):
"""Inner singleton direct access"""
return getattr(self.__instance, item) return getattr(self.__instance, item)

View file

@ -9,6 +9,7 @@ from .subcommands import *
########### ###########
# CONSTANTS # CONSTANTS
# all available subcommands
SUBCOMMANDS = [ SUBCOMMANDS = [
InitCommand, InitCommand,
ShowCommand, ShowCommand,
@ -17,30 +18,43 @@ SUBCOMMANDS = [
class Runner: class Runner:
"""Singleton: Subcommands setup and run"""
class __Runner: class __Runner:
"""Singleton type"""
__parser = None
__commands = [] __commands = []
def __init__(self): def __init__(self):
# setup all subcommands
for cmd in SUBCOMMANDS: for cmd in SUBCOMMANDS:
self.__commands.append(cmd()) self.__commands.append(cmd())
def run(self): def run(self):
config = LoadedConfig.get() """run the desired subcommand"""
args = Parser().get_args() args = Parser().get_args()
for cmd in self.__commands: for cmd in self.__commands:
if str(cmd) == args.command: if str(cmd) == args.command:
# command found
logging.debug(f"Running '{cmd}' with args: {args}") logging.debug(f"Running '{cmd}' with args: {args}")
cmd.run(config, args) cmd.run(LoadedConfig.get(), args)
return True return True
# command not found
logging.error(f"kiwi command '{args.command}' unknown")
return False return False
__instance = None __instance = None
def __init__(self): def __init__(self):
if Runner.__instance is None: if Runner.__instance is None:
# create singleton
Runner.__instance = Runner.__Runner() Runner.__instance = Runner.__Runner()
def __getattr__(self, item): def __getattr__(self, item):
"""Inner singleton direct access"""
return getattr(self.__instance, item) return getattr(self.__instance, item)

View file

@ -3,15 +3,23 @@ from ..parser import Parser
class SubCommand: class SubCommand:
"""represents kiwi [anything] command"""
# actual command string
__name = None __name = None
# command parser
_sub_parser = None _sub_parser = None
def __init__(self, name, **kwargs): def __init__(self, name, **kwargs):
self.__name = name self.__name = name
self._sub_parser = Parser().get_subparsers().add_parser(name, **kwargs) self._sub_parser = Parser().get_subparsers().add_parser(
name,
**kwargs
)
def __str__(self): def __str__(self):
return self.__name return self.__name
def run(self, config, args): def run(self, config, args):
"""actually run command with this dir's config and parsed CLI args"""
pass pass

View file

@ -10,6 +10,8 @@ from ._subcommand import SubCommand
def user_input(config, key, prompt): def user_input(config, key, prompt):
"""query user for new config value"""
# prompt user as per argument # prompt user as per argument
try: try:
result = input("{} [{}] ".format(prompt, config[key])).strip() result = input("{} [{}] ".format(prompt, config[key])).strip()
@ -23,12 +25,15 @@ def user_input(config, key, prompt):
class InitCommand(SubCommand): class InitCommand(SubCommand):
"""kiwi init"""
def __init__(self): def __init__(self):
super().__init__( super().__init__(
'init', 'init',
description="Create a new kiwi-config instance" description="Create a new kiwi-config instance"
) )
# -f switch: Initialize with default config
self._sub_parser.add_argument( self._sub_parser.add_argument(
'-f', '--force', '-f', '--force',
action='store_true', action='store_true',
@ -36,7 +41,7 @@ class InitCommand(SubCommand):
) )
def run(self, config, args): def run(self, config, args):
logging.info(f"Initializing kiwi-config instance in '{os.getcwd()}'") logging.info(f"Initializing '{KIWI_CONF_NAME}' in '{os.getcwd()}'")
if args.force and os.path.isfile(KIWI_CONF_NAME): if args.force and os.path.isfile(KIWI_CONF_NAME):
from ..config import DefaultConfig from ..config import DefaultConfig