refactor: ran black and isort

This commit is contained in:
Mathew Guest 2024-08-02 05:00:42 -06:00
parent 586e7fa54d
commit 9a1999b8ab
11 changed files with 350 additions and 430 deletions

@ -17,10 +17,10 @@ repos:
args: ["--profile", "black", "--filter-files"] args: ["--profile", "black", "--filter-files"]
# Flake8 # Flake8
- repo: https://github.com/pycqa/flake8 #- repo: https://github.com/pycqa/flake8
rev: '7.0.0' # rev: '7.0.0'
hooks: # hooks:
- id: flake8 # - id: flake8
# Black # Black
# Using this mirror lets us use mypyc-compiled black, which is about 2x faster # Using this mirror lets us use mypyc-compiled black, which is about 2x faster

@ -5,4 +5,3 @@ from .app_container import *
from .cfg import * from .cfg import *
from .cli import * from .cli import *
from .log import * from .log import *

@ -3,24 +3,28 @@ import os
import sys import sys
# Check and gracefully fail if the user needs to install a 3rd-party dep. # Check and gracefully fail if the user needs to install a 3rd-party dep.
libnames = ['appdirs', 'configobj', 'colorlog'] libnames = ["appdirs", "configobj", "colorlog"]
def check_env_has_dependencies(libnames): def check_env_has_dependencies(libnames):
rc = True rc = True
for libname in libnames: for libname in libnames:
try: try:
__import__(libname) __import__(libname)
except ModuleNotFoundError as ex: except ModuleNotFoundError as ex:
print('Missing third-party library: ', ex, file=sys.stderr) print("Missing third-party library: ", ex, file=sys.stderr)
rc = False rc = False
return rc return rc
if not check_env_has_dependencies(libnames): if not check_env_has_dependencies(libnames):
print('Unable to load program without installed dependencies', file=sys.stderr) print("Unable to load program without installed dependencies", file=sys.stderr)
raise ImportError('python environment needs third-party dependencies installed') raise ImportError("python environment needs third-party dependencies installed")
# Logger for before the application and logging config is loaded # Logger for before the application and logging config is loaded
# - used to log before logging is configured # - used to log before logging is configured
_log_fmt = '%(levelname)-7s:%(message)s' _log_fmt = "%(levelname)-7s:%(message)s"
_logger_name = 'skell' _logger_name = "skell"
_bootstrap_logger = logging.getLogger(_logger_name) _bootstrap_logger = logging.getLogger(_logger_name)
# NOTE(MG) Logger monkey-patch: # NOTE(MG) Logger monkey-patch:
@ -29,13 +33,13 @@ _bootstrap_logger = logging.getLogger(_logger_name)
# configuration is reloaded. This catches APPSKELLINGTON_DEBUG # configuration is reloaded. This catches APPSKELLINGTON_DEBUG
# environment variable the first time, as app_skellington module # environment variable the first time, as app_skellington module
# is imported. See cfg.py # is imported. See cfg.py
if os.environ.get('APPSKELLINGTON_DEBUG', None): if os.environ.get("APPSKELLINGTON_DEBUG", None):
_bootstrap_logger.setLevel(logging.DEBUG) # Don't filter any log levels _bootstrap_logger.setLevel(logging.DEBUG) # Don't filter any log levels
fmt = logging.Formatter(_log_fmt) fmt = logging.Formatter(_log_fmt)
handler = logging.StreamHandler() handler = logging.StreamHandler()
handler.setFormatter(fmt) handler.setFormatter(fmt)
_bootstrap_logger.addHandler(handler) _bootstrap_logger.addHandler(handler)
_bootstrap_logger.debug('log - APPSKELLINGTON_DEBUG set in environment: Enabling verbose logging.') _bootstrap_logger.debug("log - APPSKELLINGTON_DEBUG set in environment: Enabling verbose logging.")
# Logging is by default off, excepting CRITICAL # Logging is by default off, excepting CRITICAL
else: else:
@ -45,4 +49,3 @@ _bootstrap_logger.propagate = False
# NOTE(MG) Pretty sure the logger has the default handler too at this point. # NOTE(MG) Pretty sure the logger has the default handler too at this point.
# It's been related to some issues with the logger double-printing messages. # It's been related to some issues with the logger double-printing messages.
_bootstrap_logger.addHandler(logging.NullHandler()) _bootstrap_logger.addHandler(logging.NullHandler())

@ -1,16 +1,19 @@
from __future__ import print_function from __future__ import print_function
import inspect import inspect
import os import os
import sys import sys
from . import _util from . import _util
def eprint(*args, **kwargs): def eprint(*args, **kwargs):
""" """
Print to STDERR stream. Print to STDERR stream.
""" """
print(*args, file=sys.stderr, **kwargs) print(*args, file=sys.stderr, **kwargs)
def filename_to_abspath(filename): def filename_to_abspath(filename):
""" """
Converts a filename to it's absolute path. If it's already an Converts a filename to it's absolute path. If it's already an
@ -18,6 +21,7 @@ def filename_to_abspath(filename):
""" """
return os.path.abspath(filename) return os.path.abspath(filename)
def does_file_exist(filepath): def does_file_exist(filepath):
""" """
Because the file can be deleted or created immediately after execution of Because the file can be deleted or created immediately after execution of
@ -26,31 +30,31 @@ def does_file_exist(filepath):
instant in execution. instant in execution.
""" """
try: try:
fp = open(filepath, 'r') fp = open(filepath, "r")
return True return True
except FileNotFoundError as ex: except FileNotFoundError as ex:
return False return False
def ensure_dir_exists(dirpath): def ensure_dir_exists(dirpath):
if dirpath is None: if dirpath is None:
return return
if dirpath == '': if dirpath == "":
return return
os.makedirs(dirpath, exist_ok=True) os.makedirs(dirpath, exist_ok=True)
def get_root_asset(filepath): def get_root_asset(filepath):
""" """
Attempts to locate a resource or asset shipped with the application. Attempts to locate a resource or asset shipped with the application.
Searches starting at the root module (__main__) which should be the Searches starting at the root module (__main__) which should be the
python file initially invoked. python file initially invoked.
""" """
module_root =\ module_root = os.path.abspath(os.path.dirname(sys.modules["__main__"].__file__))
os.path.abspath(
os.path.dirname(
sys.modules['__main__'].__file__))
path = os.path.join(module_root, filepath) path = os.path.join(module_root, filepath)
return path return path
def get_asset(module, filepath): def get_asset(module, filepath):
""" """
Attempts to locate a resource or asset shipped with the application. Attempts to locate a resource or asset shipped with the application.
@ -75,7 +79,7 @@ def get_asset(module, filepath):
elif isinstance(module, module): elif isinstance(module, module):
module_file = module.__file__ module_file = module.__file__
else: else:
raise Exception('Invalid Usage') raise Exception("Invalid Usage")
try: try:
root = module_file root = module_file
@ -90,6 +94,7 @@ def get_asset(module, filepath):
path = os.path.join(root, filepath) path = os.path.join(root, filepath)
return path return path
def register_class_as_commands(app, submenu, cls_object): def register_class_as_commands(app, submenu, cls_object):
""" """
Registers commands for each class method. e.g.: pass in the CLI Registers commands for each class method. e.g.: pass in the CLI
@ -105,7 +110,7 @@ def register_class_as_commands(app, submenu, cls_object):
for m in members: for m in members:
name = m[0] name = m[0]
ref = m[1] ref = m[1]
if inspect.isfunction(ref) and not name.startswith('_'): if inspect.isfunction(ref) and not name.startswith("_"):
cls_method = ref cls_method = ref
constructor = app._inject_service_dependencies(cls_constructor) constructor = app._inject_service_dependencies(cls_constructor)
sig = inspect.signature(cls_method) sig = inspect.signature(cls_method)
@ -114,9 +119,10 @@ def register_class_as_commands(app, submenu, cls_object):
docstring = inspect.getdoc(cls_method) docstring = inspect.getdoc(cls_method)
submenu.register_command(func, name, sig, docstring) submenu.register_command(func, name, sig, docstring)
def create_func(constructor, cls_method): def create_func(constructor, cls_method):
def func(*args, **kwargs): def func(*args, **kwargs):
cmd_class_instance = constructor() cmd_class_instance = constructor()
return cls_method(cmd_class_instance, *args, **kwargs) return cls_method(cmd_class_instance, *args, **kwargs)
return func
return func

@ -1,34 +1,40 @@
import appdirs
import collections import collections
import functools import functools
import inspect import inspect
import logging
import os import os
import sys import sys
import appdirs
from . import (
_util,
cfg,
cli,
log,
)
# Application scaffolding: # Application scaffolding:
from ._bootstrap import _bootstrap_logger from ._bootstrap import _bootstrap_logger
from . import log
from . import _util
from . import cli
from . import cfg
import logging
# These two variables affect the directory paths for # These two variables affect the directory paths for
# config files and logging. # config files and logging.
DEFAULT_APP_NAME = '' DEFAULT_APP_NAME = ""
DEFAULT_APP_AUTHOR = '' DEFAULT_APP_AUTHOR = ""
class ApplicationContext: class ApplicationContext:
""" """
Container for application-wide state; i.e. app configuration and loggers. Container for application-wide state; i.e. app configuration and loggers.
""" """
def __init__(self, config, log): def __init__(self, config, log):
self.config = config self.config = config
self.log = log self.log = log
self.parsed_argv = None self.parsed_argv = None
self.parsed_argv_unknown = None self.parsed_argv_unknown = None
class ApplicationContainer: class ApplicationContainer:
""" """
Generalized application functionality. Used for linking components and modules of the application Generalized application functionality. Used for linking components and modules of the application
@ -40,14 +46,10 @@ class ApplicationContainer:
Override appname and appauthor arguments to direct config and log Override appname and appauthor arguments to direct config and log
directories. directories.
""" """
def __init__(
self, def __init__(self, configspec_filepath=None, configini_filepath=None, *args, **kwargs):
configspec_filepath=None, self.appname = kwargs.get("appname") or DEFAULT_APP_NAME
configini_filepath=None, self.appauthor = kwargs.get("appauthor") or DEFAULT_APP_AUTHOR
*args, **kwargs
):
self.appname = kwargs.get('appname') or DEFAULT_APP_NAME
self.appauthor = kwargs.get('appauthor') or DEFAULT_APP_AUTHOR
# Instantiate application context which contains # Instantiate application context which contains
# global state, configuration, loggers, and runtime args. # global state, configuration, loggers, and runtime args.
@ -57,29 +59,28 @@ class ApplicationContainer:
logger = log.LoggingLayer(self.appname, self.appauthor) logger = log.LoggingLayer(self.appname, self.appauthor)
# Try and load logging configuration if provided # Try and load logging configuration if provided
log_config = config.get('logging') log_config = config.get("logging")
if log_config is not None: if log_config is not None:
logger.configure_logging(log_config) logger.configure_logging(log_config)
else: else:
logger.configure_logging() logger.configure_logging()
self.ctx = ApplicationContext(config, logger) self.ctx = ApplicationContext(config, logger)
# Reference to root_app avail. in context # Reference to root_app avail. in context
self.ctx.root_app = self self.ctx.root_app = self
# Reference to context service avail. in root_app # Reference to context service avail. in root_app
self['ctx'] = lambda: self.ctx self["ctx"] = lambda: self.ctx
self.cli = cli.CommandTree() # Command-line interface self.cli = cli.CommandTree() # Command-line interface
# Run methods if subclass implemented them: # Run methods if subclass implemented them:
if callable(getattr(self, '_cli_options', None)): if callable(getattr(self, "_cli_options", None)):
self._cli_options() self._cli_options()
if callable(getattr(self, '_services', None)): if callable(getattr(self, "_services", None)):
self._services() self._services()
if callable(getattr(self, '_command_menu', None)): if callable(getattr(self, "_command_menu", None)):
self._command_menu() self._command_menu()
def __delitem__(self, service_name): def __delitem__(self, service_name):
@ -102,7 +103,7 @@ class ApplicationContainer:
service_factory = self._dependencies[service_name] # Retrieve factory function service_factory = self._dependencies[service_name] # Retrieve factory function
return service_factory() # Call factory() to return instance of service return service_factory() # Call factory() to return instance of service
except KeyError as ex: except KeyError as ex:
msg = 'failed to inject service: {}'.format(service_name) msg = "failed to inject service: {}".format(service_name)
_bootstrap_logger.critical(msg) _bootstrap_logger.critical(msg)
raise ServiceNotFound raise ServiceNotFound
@ -131,9 +132,7 @@ class ApplicationContainer:
dependencies.append(self[dep_name]) dependencies.append(self[dep_name])
return model_constructor(*dependencies) return model_constructor(*dependencies)
def _get_config_filepath( def _get_config_filepath(self, app_name, app_author, config_filename="config.ini"):
self, app_name, app_author, config_filename='config.ini'
):
""" """
Attempt to find config.ini in the user's config directory. Attempt to find config.ini in the user's config directory.
@ -142,10 +141,10 @@ class ApplicationContainer:
""" """
dirname = appdirs.user_config_dir(app_name, app_author) dirname = appdirs.user_config_dir(app_name, app_author)
filepath = os.path.join(dirname, config_filename) filepath = os.path.join(dirname, config_filename)
_bootstrap_logger.info('default config filepath calculated to be: %s', filepath) _bootstrap_logger.info("default config filepath calculated to be: %s", filepath)
return filepath return filepath
def _get_configspec_filepath(self, configspec_filename='config.spec'): def _get_configspec_filepath(self, configspec_filename="config.spec"):
""" """
Attempt to find config.spec inside the installed package directory. Attempt to find config.spec inside the installed package directory.
""" """
@ -181,7 +180,7 @@ class ApplicationContainer:
try: try:
self.cli.run_command() self.cli.run_command()
except NoCommandSpecified as ex: except NoCommandSpecified as ex:
print('Failure: No command specified.') print("Failure: No command specified.")
def interactive_shell(self): def interactive_shell(self):
pass pass
@ -193,12 +192,14 @@ class ApplicationContainer:
pass pass
# Applications need a default usage # Applications need a default usage
class ServiceNotFound(Exception): class ServiceNotFound(Exception):
""" """
Application framework error: unable to find and inject dependency. Application framework error: unable to find and inject dependency.
""" """
pass pass
class NoCommandSpecified(Exception): class NoCommandSpecified(Exception):
pass pass

@ -4,15 +4,17 @@
# ConfigObj module and it's recommended to use config.spec files to define # ConfigObj module and it's recommended to use config.spec files to define
# your available configuration of the relevant application. # your available configuration of the relevant application.
import argparse
import os
import sys
import appdirs
import configobj
import validate
from . import _util from . import _util
from ._bootstrap import _bootstrap_logger from ._bootstrap import _bootstrap_logger
import appdirs
import argparse
import configobj
import os
import sys
import validate
class Config: class Config:
""" """
@ -26,15 +28,10 @@ class Config:
""" """
DEFAULT_CAPABILITIES = { DEFAULT_CAPABILITIES = {
'allow_options_beyond_spec': True, "allow_options_beyond_spec": True,
} }
def __init__( def __init__(self, configspec_filepath=None, configini_filepath=None, capabilities=None):
self,
configspec_filepath=None,
configini_filepath=None,
capabilities=None
):
self._config_obj = None # must be type configobj.ConfigObj() self._config_obj = None # must be type configobj.ConfigObj()
self._configini_data = None self._configini_data = None
self._configini_filepath = None self._configini_filepath = None
@ -83,9 +80,7 @@ class Config:
@configspec_filepath.setter @configspec_filepath.setter
def configspec_filepath(self, filepath): def configspec_filepath(self, filepath):
if filepath is None: if filepath is None:
_bootstrap_logger.debug( _bootstrap_logger.debug("cfg - Clearing configspec")
'cfg - Clearing configspec'
)
self._configspec_filepath = None self._configspec_filepath = None
self._configspec_data = None self._configspec_data = None
self._has_changed_internally = True self._has_changed_internally = True
@ -98,18 +93,12 @@ class Config:
self._configspec_filepath = filepath self._configspec_filepath = filepath
self._configspec_data = data self._configspec_data = data
self._has_changed_internally = True self._has_changed_internally = True
_bootstrap_logger.debug( _bootstrap_logger.debug("cfg - Set configspec and read contents: %s", filepath)
'cfg - Set configspec and read contents: %s',
filepath
)
self.load_config() self.load_config()
return return
except OSError as ex: except OSError as ex:
_bootstrap_logger.critical( _bootstrap_logger.critical("cfg - Failed to find config.spec: file not found (%s)", filepath)
'cfg - Failed to find config.spec: file not found (%s)', raise OSError("Failed to read provided config.spec file")
filepath
)
raise OSError('Failed to read provided config.spec file')
self.load_config() self.load_config()
@ -164,9 +153,7 @@ class Config:
except KeyError as ex: except KeyError as ex:
return default return default
def load_config( def load_config(self, configspec_filepath=None, configini_filepath=None):
self, configspec_filepath=None, configini_filepath=None
):
# Set new arguments if were passed in: # Set new arguments if were passed in:
if configspec_filepath is not None: if configspec_filepath is not None:
self.configspec_filepath = configspec_filepath self.configspec_filepath = configspec_filepath
@ -179,7 +166,7 @@ class Config:
rc = self._validate_config_against_spec() rc = self._validate_config_against_spec()
if not rc: if not rc:
if self._capability_enforce_strict_spec_validation: if self._capability_enforce_strict_spec_validation:
raise RuntimeError('Failed to validate config.ini against spec.') raise RuntimeError("Failed to validate config.ini against spec.")
return False return False
return True return True
@ -196,21 +183,20 @@ class Config:
# options # options
configspec=config_spec, configspec=config_spec,
# encoding # encoding
interpolation='template' interpolation="template",
# raise_errors # raise_errors
) )
_bootstrap_logger.debug( _bootstrap_logger.debug(
'cfg - Parsed configuration. config.spec = %s, config.ini = %s', "cfg - Parsed configuration. config.spec = %s, config.ini = %s", config_spec, config_ini
config_spec, config_ini
) )
return True return True
except configobj.ParseError as ex: except configobj.ParseError as ex:
msg = 'cfg - Failed to load config: error in config.spec configuration: {}'.format(config_filepath) msg = "cfg - Failed to load config: error in config.spec configuration: {}".format(config_filepath)
_bootstrap_logger.error(msg) _bootstrap_logger.error(msg)
return False return False
except OSError as ex: except OSError as ex:
msg = 'cfg - Failed to load config: config.spec file not found.' msg = "cfg - Failed to load config: config.spec file not found."
_bootstrap_logger.error(msg) _bootstrap_logger.error(msg)
return False return False
except Exception as ex: except Exception as ex:
@ -221,58 +207,66 @@ class Config:
config_ini = self.configini_filepath config_ini = self.configini_filepath
# Hack the configobj module to alter the interpolation for validate.py: # Hack the configobj module to alter the interpolation for validate.py:
configobj.DEFAULT_INTERPOLATION = 'template' configobj.DEFAULT_INTERPOLATION = "template"
# Validate config.ini against config.spec # Validate config.ini against config.spec
try: try:
_bootstrap_logger.info('cfg - Validating config file against spec') _bootstrap_logger.info("cfg - Validating config file against spec")
val = validate.Validator() val = validate.Validator()
assert isinstance(self._config_obj, configobj.ConfigObj), 'expecting configobj.ConfigObj, received %s' % type(self._config_obj) assert isinstance(
self._config_obj, configobj.ConfigObj
), "expecting configobj.ConfigObj, received %s" % type(self._config_obj)
# NOTE(MG) copy arg below instructs configobj to use defaults from spec file # NOTE(MG) copy arg below instructs configobj to use defaults from spec file
test_results = self._config_obj.validate( test_results = self._config_obj.validate(val, copy=True, preserve_errors=True)
val, copy=True, preserve_errors=True
)
if test_results is True: if test_results is True:
_bootstrap_logger.info( _bootstrap_logger.info(
'cfg- Successfully validated configuration against spec. input = %s, validation spec = %s', "cfg- Successfully validated configuration against spec. input = %s, validation spec = %s",
config_ini, config_spec config_ini,
config_spec,
) )
return True return True
elif test_results is False: elif test_results is False:
_bootstrap_logger.debug( _bootstrap_logger.debug("cfg - Potentially discovered invalid config.spec")
'cfg - Potentially discovered invalid config.spec'
)
else: else:
self._validate_parse_errors(test_results) self._validate_parse_errors(test_results)
return False return False
except ValueError as ex: except ValueError as ex:
_bootstrap_logger.error('cfg - Failed while validating config against spec. ') _bootstrap_logger.error("cfg - Failed while validating config against spec. ")
return False return False
def _validate_parse_errors(self, test_results): def _validate_parse_errors(self, test_results):
_bootstrap_logger.critical('cfg - Config file failed validation.') _bootstrap_logger.critical("cfg - Config file failed validation.")
for (section_list, key, rslt) in configobj.flatten_errors(self._config_obj, test_results): for section_list, key, rslt in configobj.flatten_errors(self._config_obj, test_results):
_bootstrap_logger.critical('cfg - Config error info: %s %s %s', section_list, key, rslt) _bootstrap_logger.critical("cfg - Config error info: %s %s %s", section_list, key, rslt)
if key is not None: if key is not None:
_bootstrap_logger.critical('cfg - Config failed validation: [%s].%s appears invalid. msg = %s', '.'.join(section_list), key, rslt) _bootstrap_logger.critical(
"cfg - Config failed validation: [%s].%s appears invalid. msg = %s",
".".join(section_list),
key,
rslt,
)
else: else:
_bootstrap_logger.critical("cfg - Config failed validation: missing section, name = '%s'. msg = %s", '.'.join(section_list), rslt) _bootstrap_logger.critical(
"cfg - Config failed validation: missing section, name = '%s'. msg = %s",
".".join(section_list),
rslt,
)
def print_config(self): def print_config(self):
""" """
Print configuration to stdout. Print configuration to stdout.
""" """
print('config:') print("config:")
self._config_obj.walk(print) self._config_obj.walk(print)
for section in self._config_obj.sections: for section in self._config_obj.sections:
print(section) print(section)
for key in self._config_obj[section]: for key in self._config_obj[section]:
print(' ', self._config_obj[section][key]) print(" ", self._config_obj[section][key])
class EnvironmentVariables: class EnvironmentVariables:
def __init__(self): def __init__(self):
raise NotImplementedError raise NotImplementedError

@ -5,14 +5,16 @@ import re
import sys import sys
import app_skellington import app_skellington
from ._bootstrap import _bootstrap_logger
from . import app_container from . import app_container
from ._bootstrap import _bootstrap_logger
# If explicit fail is enabled, any command with at least one unknown # If explicit fail is enabled, any command with at least one unknown
# argument will be rejected entirely. If not enabled, unknown arguments # argument will be rejected entirely. If not enabled, unknown arguments
# will be ignored. # will be ignored.
EXPLICIT_FAIL_ON_UNKNOWN_ARGS = True EXPLICIT_FAIL_ON_UNKNOWN_ARGS = True
class CommandTree: class CommandTree:
""" """
Command-line interface to hold a menu of commands. You can register Command-line interface to hold a menu of commands. You can register
@ -51,6 +53,7 @@ class CommandTree:
the second. In the same way the -h, --help options print different docs the second. In the same way the -h, --help options print different docs
depending on where the help option was passed. depending on where the help option was passed.
""" """
def __init__(self): def __init__(self):
self.root_parser = argparse.ArgumentParser() self.root_parser = argparse.ArgumentParser()
self.submenu_param = None # submenu_param is the variable name self.submenu_param = None # submenu_param is the variable name
@ -72,7 +75,7 @@ class CommandTree:
""" """
Adds an argument to the root parser. Adds an argument to the root parser.
""" """
_bootstrap_logger.info('adding argument to root parser: %s and %s', args, kwargs) _bootstrap_logger.info("adding argument to root parser: %s and %s", args, kwargs)
self.root_parser.add_argument(*args, **kwargs) self.root_parser.add_argument(*args, **kwargs)
def init_submenu(self, param_name, is_required=False): def init_submenu(self, param_name, is_required=False):
@ -83,39 +86,27 @@ class CommandTree:
# NOTE(MG) Fix for Python>=3.7, # NOTE(MG) Fix for Python>=3.7,
# argparse.ArgumentParser added 'required' argument. # argparse.ArgumentParser added 'required' argument.
# Must also be written into SubMenu.create_submenu. # Must also be written into SubMenu.create_submenu.
func_args = { func_args = {"dest": param_name, "metavar": param_name, "required": is_required}
'dest': param_name, if sys.version_info.major == 3 and sys.version_info.minor < 7:
'metavar': param_name,
'required': is_required
}
if (
sys.version_info.major == 3
and sys.version_info.minor < 7
):
if is_required: if is_required:
_bootstrap_logger.warn('Unable to enforce required submenu: Requires >= Python 3.7') _bootstrap_logger.warn("Unable to enforce required submenu: Requires >= Python 3.7")
del func_args['required'] del func_args["required"]
# END fix for Python<3.7 # END fix for Python<3.7
# Creates an argument as a slot in the underlying argparse. # Creates an argument as a slot in the underlying argparse.
subparsers = self.root_parser.add_subparsers( subparsers = self.root_parser.add_subparsers(**func_args)
**func_args
)
submenu = SubMenu(self, subparsers, param_name) submenu = SubMenu(self, subparsers, param_name)
submenu.submenu_path = '' submenu.submenu_path = ""
submenu.var_name = param_name submenu.var_name = param_name
_bootstrap_logger.info('Initialized root-level submenu: Parameter = \'%s\'', param_name) _bootstrap_logger.info("Initialized root-level submenu: Parameter = '%s'", param_name)
self.entries[param_name] = submenu self.entries[param_name] = submenu
self.submenu_param = param_name self.submenu_param = param_name
return submenu return submenu
def register_command( def register_command(self, func, cmd_name=None, func_signature=None, docstring=None):
self, func, cmd_name=None, func_signature=None,
docstring=None
):
""" """
When no submenu functionality is desired, this links a single When no submenu functionality is desired, this links a single
command into underlying argparse options. command into underlying argparse options.
@ -128,7 +119,7 @@ class CommandTree:
pass pass
# print('func is method') # print('func is method')
else: else:
raise Exception('bad value passed in for function') raise Exception("bad value passed in for function")
if not cmd_name: if not cmd_name:
# safe try/except # safe try/except
@ -154,26 +145,19 @@ class CommandTree:
# For each paramter in the function create an argparse argument in # For each paramter in the function create an argparse argument in
# the child ArgumentParser created for this menu entry: # the child ArgumentParser created for this menu entry:
for key in params: for key in params:
if key == 'self': if key == "self":
continue continue
param = params[key] param = params[key]
if '=' in str(param): if "=" in str(param):
if param.default is None: if param.default is None:
helptext = 'default provided' helptext = "default provided"
else: else:
helptext = "default = '{}'".format(param.default) helptext = "default = '{}'".format(param.default)
self.root_parser.add_argument( self.root_parser.add_argument(key, help=helptext, nargs="?", default=param.default)
key,
help=helptext,
nargs='?',
default=param.default
)
else: else:
helptext = 'required' helptext = "required"
self.root_parser.add_argument( self.root_parser.add_argument(key, help=helptext)
key,
help=helptext)
# Build the CommandEntry structure # Build the CommandEntry structure
cmd = CommandEntry() cmd = CommandEntry()
@ -184,7 +168,7 @@ class CommandTree:
cmd.callback = func cmd.callback = func
registered_name = cmd_name registered_name = cmd_name
_bootstrap_logger.info('registered command: %s', registered_name) _bootstrap_logger.info("registered command: %s", registered_name)
# end copy-paste then editted from SubMenu.register_command # end copy-paste then editted from SubMenu.register_command
self._cmd_tree_is_single_command = True self._cmd_tree_is_single_command = True
@ -209,7 +193,7 @@ class CommandTree:
# 'failed to parse arguments: explicitly failing to be safe') # 'failed to parse arguments: explicitly failing to be safe')
# return False, False # return False, False
if hasattr(pargs, 'usage'): if hasattr(pargs, "usage"):
pass pass
# print('found usage in app_skellington') # print('found usage in app_skellington')
@ -222,19 +206,19 @@ class CommandTree:
def run_command(self, args=None): def run_command(self, args=None):
args, unk, success = self.parse(args) args, unk, success = self.parse(args)
if not success: if not success:
_bootstrap_logger.info('cli - SystemExit: Perhaps user invoked --help') _bootstrap_logger.info("cli - SystemExit: Perhaps user invoked --help")
return return
if args is False and unk is False: if args is False and unk is False:
_bootstrap_logger.error('cli - Failed parsing args.') _bootstrap_logger.error("cli - Failed parsing args.")
return False return False
_bootstrap_logger.info('cli - Received args from shell: %s', args) _bootstrap_logger.info("cli - Received args from shell: %s", args)
args = vars(args) args = vars(args)
cmd = self._lookup_command(args) cmd = self._lookup_command(args)
if cmd is None: if cmd is None:
_bootstrap_logger.critical('cli - Failed to find command.') _bootstrap_logger.critical("cli - Failed to find command.")
return False return False
return self._invoke_command(cmd, args) return self._invoke_command(cmd, args)
@ -246,16 +230,16 @@ class CommandTree:
# the CommandTree with no SubMenu (submenu will be disabled # the CommandTree with no SubMenu (submenu will be disabled
# in this case): # in this case):
if self._cmd_tree_is_single_command: if self._cmd_tree_is_single_command:
assert self._cmd_tree_is_single_command is True, 'corrupt data structure in CommandMenu' assert self._cmd_tree_is_single_command is True, "corrupt data structure in CommandMenu"
assert self._entries is None, 'corrupt data structure in CommandMenu' assert self._entries is None, "corrupt data structure in CommandMenu"
assert isinstance(self._single_command, CommandEntry), 'corrupt data structure in CommandMenu' assert isinstance(self._single_command, CommandEntry), "corrupt data structure in CommandMenu"
return self._single_command return self._single_command
# There is at least one submenu we need to go down: # There is at least one submenu we need to go down:
else: else:
assert self._single_command is None, 'corrupt data structure in CommandMenu' assert self._single_command is None, "corrupt data structure in CommandMenu"
assert self._cmd_tree_is_single_command == False, 'corrupt data structure in CommandMenu' assert self._cmd_tree_is_single_command == False, "corrupt data structure in CommandMenu"
# Key or variable name used by argparse to store the submenu options # Key or variable name used by argparse to store the submenu options
argparse_param = self.submenu_param # e.g.: submenu_root argparse_param = self.submenu_param # e.g.: submenu_root
@ -263,14 +247,14 @@ class CommandTree:
while True: while True:
if argparse_param not in keys: if argparse_param not in keys:
print('root menu parameter not found in args:', argparse_param) print("root menu parameter not found in args:", argparse_param)
input('<broken>') input("<broken>")
val = args.get(argparse_param) val = args.get(argparse_param)
_bootstrap_logger.debug('cli - argparse command is \'{}\' = {}'.format(argparse_param, val)) _bootstrap_logger.debug("cli - argparse command is '{}' = {}".format(argparse_param, val))
lookup = submenu.entries.get(val) lookup = submenu.entries.get(val)
_bootstrap_logger.debug('cli - lookup, entries[{}] = {}'.format(val, lookup)) _bootstrap_logger.debug("cli - lookup, entries[{}] = {}".format(val, lookup))
# pop value # pop value
del args[argparse_param] del args[argparse_param]
@ -283,7 +267,7 @@ class CommandTree:
# return self._invoke_command(lookup, args) # return self._invoke_command(lookup, args)
else: else:
raise app_container.NoCommandSpecified('No command specified.') raise app_container.NoCommandSpecified("No command specified.")
def _invoke_command(self, cmd, args): def _invoke_command(self, cmd, args):
command_to_be_invoked = cmd.callback command_to_be_invoked = cmd.callback
@ -296,13 +280,14 @@ class CommandTree:
if param.name in args: if param.name in args:
func_args.append(args[param.name]) func_args.append(args[param.name])
_bootstrap_logger.info('cli - function: %s', func) _bootstrap_logger.info("cli - function: %s", func)
_bootstrap_logger.info('cli - function args: %s', func_args) _bootstrap_logger.info("cli - function args: %s", func_args)
return command_to_be_invoked(*func_args) return command_to_be_invoked(*func_args)
def _get_subparser(self): def _get_subparser(self):
return self.root_parser._subparsers._actions[1] return self.root_parser._subparsers._actions[1]
class SubMenu: class SubMenu:
def __init__(self, parent, subparsers_obj, name): def __init__(self, parent, subparsers_obj, name):
self.parent = parent # Reference to root CommandTree self.parent = parent # Reference to root CommandTree
@ -312,10 +297,7 @@ class SubMenu:
self.entries = {} self.entries = {}
def register_command( def register_command(self, func, cmd_name=None, func_signature=None, docstring=None):
self, func, cmd_name=None, func_signature=None,
docstring=None
):
""" """
Registers a command as an entry in this submenu. Provided function is Registers a command as an entry in this submenu. Provided function is
converted into argparse arguments and made available to the user. converted into argparse arguments and made available to the user.
@ -345,7 +327,7 @@ class SubMenu:
elif inspect.ismethod(func): elif inspect.ismethod(func):
pass pass
else: else:
raise Exception('bad value passed in for function') raise Exception("bad value passed in for function")
if not cmd_name: if not cmd_name:
# TODO(MG) Safer sanitation # TODO(MG) Safer sanitation
@ -374,33 +356,25 @@ class SubMenu:
# created when the SubMenu/argparse.add_subparsers() # created when the SubMenu/argparse.add_subparsers()
# was created. # was created.
help=help_text, help=help_text,
description=description_text description=description_text,
) )
# For each paramter in the function create an argparse argument in # For each paramter in the function create an argparse argument in
# the child ArgumentParser created for this menu entry: # the child ArgumentParser created for this menu entry:
for key in params: for key in params:
if key == 'self': if key == "self":
continue continue
param = params[key] param = params[key]
if '=' in str(param): if "=" in str(param):
if param.default is None: if param.default is None:
helptext = 'default provided' helptext = "default provided"
else: else:
helptext = "default = '{}'".format(param.default) helptext = "default = '{}'".format(param.default)
child_node.add_argument( child_node.add_argument(key, help=helptext, nargs="?", default=param.default)
key,
help=helptext,
nargs='?',
default=param.default
)
else: else:
helptext = 'required' helptext = "required"
child_node.add_argument( child_node.add_argument(key, help=helptext)
key,
help=helptext
)
# Build the CommandEntry structure # Build the CommandEntry structure
cmd = CommandEntry() cmd = CommandEntry()
@ -410,15 +384,11 @@ class SubMenu:
# cmd.func_ref = None # cmd.func_ref = None
cmd.callback = func cmd.callback = func
registered_name = '{}.{}'.format( registered_name = "{}.{}".format(self.submenu_path, cmd_name)
self.submenu_path, _bootstrap_logger.info("cli - registered command: %s", registered_name)
cmd_name)
_bootstrap_logger.info('cli - registered command: %s', registered_name)
self.entries[cmd_name] = cmd self.entries[cmd_name] = cmd
def create_submenu( def create_submenu(self, var_name, cmd_entry_name=None, is_required=False):
self, var_name, cmd_entry_name=None, is_required=False
):
""" """
Creates a child-submenu. Creates a child-submenu.
@ -443,54 +413,37 @@ class SubMenu:
# Create an entry in self's submenu: # Create an entry in self's submenu:
# type = ArgumentParser # type = ArgumentParser
entry_node = self.subparsers_obj.add_parser( entry_node = self.subparsers_obj.add_parser(
cmd_entry_name, cmd_entry_name, help="sub-submenu help", description="sub-sub description"
help='sub-submenu help', )
description='sub-sub description')
# NOTE(MG) Fix for Python>=3.7, # NOTE(MG) Fix for Python>=3.7,
# argparse.ArgumentParser added 'required' argument. # argparse.ArgumentParser added 'required' argument.
# Must also be written into CommandTree.init_submenu # Must also be written into CommandTree.init_submenu
func_args = { func_args = {"dest": var_name, "metavar": var_name, "required": is_required}
'dest': var_name, if sys.version_info.major == 3 and sys.version_info.minor < 7:
'metavar': var_name,
'required': is_required
}
if (
sys.version_info.major == 3
and sys.version_info.minor < 7
):
if is_required: if is_required:
_bootstrap_logger.warn('Unable to enforce required submenu: Requires >= Python 3.7') _bootstrap_logger.warn("Unable to enforce required submenu: Requires >= Python 3.7")
del func_args['required'] del func_args["required"]
# END fix for Python<3.7 # END fix for Python<3.7
# Turn entry into a submenu of it's own: # Turn entry into a submenu of it's own:
# type = _SubParsersAction # type = _SubParsersAction
subp_node = entry_node.add_subparsers( subp_node = entry_node.add_subparsers(**func_args)
**func_args
)
submenu = SubMenu( submenu = SubMenu(self.parent, subp_node, cmd_entry_name)
self.parent,
subp_node,
cmd_entry_name
)
submenu.var_name = var_name submenu.var_name = var_name
submenu.submenu_path = '{}.{}'.format(self.submenu_path, cmd_entry_name) submenu.submenu_path = "{}.{}".format(self.submenu_path, cmd_entry_name)
submenu_name = submenu.submenu_path submenu_name = submenu.submenu_path
_bootstrap_logger.info('cli - registered submenu: %s', submenu_name) _bootstrap_logger.info("cli - registered submenu: %s", submenu_name)
self.entries[cmd_entry_name] = submenu self.entries[cmd_entry_name] = submenu
return submenu return submenu
def __repr__(self): def __repr__(self):
return 'SubMenu({})<{}>'.format( return "SubMenu({})<{}>".format(self.name, ",".join(["cmds"]))
self.name,
','.join(['cmds'])
)
class CommandEntry: class CommandEntry:
""" """
@ -506,6 +459,7 @@ class CommandEntry:
arguments into argparse options (creating the documentation also). Similary, arguments into argparse options (creating the documentation also). Similary,
it can convert from argparse options into a function call. it can convert from argparse options into a function call.
""" """
def __init__(self): def __init__(self):
self.argparse_node = None self.argparse_node = None
@ -517,7 +471,8 @@ class CommandEntry:
self.callback = None self.callback = None
def __repr__(self): def __repr__(self):
return 'CommandEntry<{}>'.format(self.cmd_name) return "CommandEntry<{}>".format(self.cmd_name)
class HelpGenerator: class HelpGenerator:
def __init__(self): def __init__(self):
@ -531,10 +486,10 @@ class HelpGenerator:
""" """
if doctext == None: if doctext == None:
return doctext return doctext
regex = '(.*?)[.?!]' regex = "(.*?)[.?!]"
match = re.match(regex, doctext, re.MULTILINE | re.DOTALL) match = re.match(regex, doctext, re.MULTILINE | re.DOTALL)
if match: if match:
return match.group(1) + '.' return match.group(1) + "."
return doctext return doctext
@staticmethod @staticmethod
@ -545,9 +500,8 @@ class HelpGenerator:
""" """
if doctext == None: if doctext == None:
return doctext return doctext
regex = '(.*?)[.?!]' regex = "(.*?)[.?!]"
match = re.match(regex, doctext, re.MULTILINE | re.DOTALL) match = re.match(regex, doctext, re.MULTILINE | re.DOTALL)
if match: if match:
return match.group(1) + '.' return match.group(1) + "."
return doctext return doctext

@ -1,48 +1,42 @@
from ._bootstrap import _bootstrap_logger, _logger_name
from . import _util
import appdirs
import colorlog
import logging import logging
import logging.config import logging.config
import os import os
import appdirs
import colorlog
from . import _util
from ._bootstrap import _bootstrap_logger, _logger_name
DEFAULT_LOG_SETTINGS = { DEFAULT_LOG_SETTINGS = {
'formatters': { "formatters": {
'colored': { "colored": {
'class': 'colorlog.ColoredFormatter', "class": "colorlog.ColoredFormatter",
# 'format': '%(log_color)s%(levelname)-8s%(reset)s:%(log_color)s%(name)-5s%(reset)s:%(white)s%(message)s' # 'format': '%(log_color)s%(levelname)-8s%(reset)s:%(log_color)s%(name)-5s%(reset)s:%(white)s%(message)s'
'format': '%(white)s%(name)7s%(reset)s|%(log_color)s%(message)s', "format": "%(white)s%(name)7s%(reset)s|%(log_color)s%(message)s",
} }
}, },
"handlers": {"stderr": {"class": "logging.StreamHandler", "level": "debug", "formatter": "colored"}},
'handlers': { "loggers": {
'stderr': { "root": {
'class': 'logging.StreamHandler', "handlers": [
'level': 'debug', "stderr",
'formatter': 'colored' ],
} "level": "debug",
}, },
"app_skellington": {
'loggers': {
'root': {
'handlers': ['stderr',],
'level': 'debug'
},
'app_skellington': {
# 'handlers': ['stderr',], # 'handlers': ['stderr',],
'level': 'critical', "level": "critical",
'propagate': 'false' "propagate": "false",
} },
} },
} }
class LoggingLayer: class LoggingLayer:
def __init__( def __init__(self, appname=None, appauthor=None):
self, appname=None, appauthor=None self.appname = appname or ""
): self.appauthor = appauthor or ""
self.appname = appname or ''
self.appauthor = appauthor or ''
self.loggers = {} self.loggers = {}
def __getitem__(self, k): def __getitem__(self, k):
@ -71,17 +65,17 @@ class LoggingLayer:
noise for typical operation. noise for typical operation.
""" """
if config_dict is None: if config_dict is None:
_bootstrap_logger.debug('log - No application logging configuration provided. Using default') _bootstrap_logger.debug("log - No application logging configuration provided. Using default")
config_dict = DEFAULT_LOG_SETTINGS config_dict = DEFAULT_LOG_SETTINGS
self.transform_config(config_dict) self.transform_config(config_dict)
try: try:
_bootstrap_logger.debug('log - Log configuration: %s', config_dict) _bootstrap_logger.debug("log - Log configuration: %s", config_dict)
logging.config.dictConfig(config_dict) logging.config.dictConfig(config_dict)
_bootstrap_logger.debug('log - Configured all logging') _bootstrap_logger.debug("log - Configured all logging")
except Exception as ex: except Exception as ex:
print('unable to configure logging:', ex, type(ex)) print("unable to configure logging:", ex, type(ex))
def transform_config(self, config_dict): def transform_config(self, config_dict):
""" """
@ -89,48 +83,45 @@ class LoggingLayer:
parameters and the final config dictionary passed into the logging module. parameters and the final config dictionary passed into the logging module.
""" """
# Version should be hard-coded 1, per Python docs # Version should be hard-coded 1, per Python docs
if 'version' in config_dict: if "version" in config_dict:
if config_dict['version'] != 1: if config_dict["version"] != 1:
_bootstrap_logger.warn("logging['version'] must be '1' per Python docs") _bootstrap_logger.warn("logging['version'] must be '1' per Python docs")
config_dict['version'] = 1 config_dict["version"] = 1
self._add_own_logconfig(config_dict) self._add_own_logconfig(config_dict)
# Replace logger level strings with value integers from module # Replace logger level strings with value integers from module
for handler in config_dict['handlers']: for handler in config_dict["handlers"]:
d = config_dict['handlers'][handler] d = config_dict["handlers"][handler]
self._convert_str_to_loglevel(d, 'level') self._convert_str_to_loglevel(d, "level")
# Replace logger level strings with value integers from module # Replace logger level strings with value integers from module
for logger in config_dict['loggers']: for logger in config_dict["loggers"]:
d = config_dict['loggers'][logger] d = config_dict["loggers"][logger]
self._convert_str_to_loglevel(d, 'level') self._convert_str_to_loglevel(d, "level")
# Implementation note: # Implementation note:
# app_skellington expects root logger configuration to be under 'root' # app_skellington expects root logger configuration to be under 'root'
# instead of '' (python spec) because '' is not a valid name in ConfigObj. # instead of '' (python spec) because '' is not a valid name in ConfigObj.
try: try:
if config_dict['loggers'].get('root') is not None: if config_dict["loggers"].get("root") is not None:
config_dict['loggers'][''] = config_dict['loggers']['root'] config_dict["loggers"][""] = config_dict["loggers"]["root"]
del config_dict['loggers']['root'] del config_dict["loggers"]["root"]
except Exception as ex: except Exception as ex:
_bootstrap_logger.warn('was not able to find and patch root logger configuration from arguments') _bootstrap_logger.warn("was not able to find and patch root logger configuration from arguments")
# Evaluate the full filepath of the file handler # Evaluate the full filepath of the file handler
if 'file' not in config_dict['handlers']: if "file" not in config_dict["handlers"]:
return return
if os.path.abspath(config_dict['handlers']['file']['filename']) ==\ if os.path.abspath(config_dict["handlers"]["file"]["filename"]) == config_dict["handlers"]["file"]["filename"]:
config_dict['handlers']['file']['filename']:
# Path is already absolute # Path is already absolute
pass pass
else: else:
dirname = appdirs.user_log_dir(self.appname, self.appauthor) dirname = appdirs.user_log_dir(self.appname, self.appauthor)
_util.ensure_dir_exists(dirname) _util.ensure_dir_exists(dirname)
log_filepath = os.path.join(dirname, config_dict['handlers']['file']['filename']) log_filepath = os.path.join(dirname, config_dict["handlers"]["file"]["filename"])
config_dict['handlers']['file']['filename'] = log_filepath config_dict["handlers"]["file"]["filename"] = log_filepath
def _add_own_logconfig(self, config_dict): def _add_own_logconfig(self, config_dict):
# NOTE(MG) Monkey-patch logger # NOTE(MG) Monkey-patch logger
@ -140,13 +131,11 @@ class LoggingLayer:
# variable the second time, when it's being reloaded as a # variable the second time, when it's being reloaded as a
# logging configuration is read from config file. # logging configuration is read from config file.
# See _bootstrap.py # See _bootstrap.py
if os.environ.get('APPSKELLINGTON_DEBUG', None): if os.environ.get("APPSKELLINGTON_DEBUG", None):
if _logger_name not in config_dict['loggers']: if _logger_name not in config_dict["loggers"]:
config_dict['loggers'][_logger_name] = { config_dict["loggers"][_logger_name] = {"level": "debug", "propagate": "false"}
'level': 'debug', 'propagate': 'false'
}
else: else:
config_dict['loggers'][_logger_name]['level'] = 'debug' config_dict["loggers"][_logger_name]["level"] = "debug"
def _convert_str_to_loglevel(self, dict_, key): def _convert_str_to_loglevel(self, dict_, key):
""" """
@ -164,16 +153,15 @@ class LoggingLayer:
s = dict_[key] s = dict_[key]
except KeyError as ex: except KeyError as ex:
raise raise
if s == 'critical': if s == "critical":
dict_[key] = logging.CRITICAL dict_[key] = logging.CRITICAL
elif s == 'error': elif s == "error":
dict_[key] = logging.ERROR dict_[key] = logging.ERROR
elif s == 'warning': elif s == "warning":
dict_[key] = logging.WARNING dict_[key] = logging.WARNING
elif s == 'info': elif s == "info":
dict_[key] = logging.INFO dict_[key] = logging.INFO
elif s == 'debug': elif s == "debug":
dict_[key] = logging.DEBUG dict_[key] = logging.DEBUG
elif s == 'all': elif s == "all":
dict_[key] = logging.NOTSET dict_[key] = logging.NOTSET

@ -15,61 +15,52 @@
# $ pip uninstall app_skellington # $ pip uninstall app_skellington
from setuptools import setup
import os import os
__project__ = 'app_skellington' from setuptools import setup
__version__ = '0.1.1'
__description__ = 'A high-powered command line menu framework.' __project__ = "app_skellington"
__version__ = "0.1.1"
__description__ = "A high-powered command line menu framework."
long_description = __description__ long_description = __description__
readme_filepath = os.path.join( readme_filepath = os.path.join(os.path.abspath(os.path.dirname(__file__)), "README.md")
os.path.abspath(os.path.dirname(__file__)), with open(readme_filepath, encoding="utf-8") as fp:
'README.md'
)
with open(readme_filepath, encoding='utf-8') as fp:
long_description = fp.read() long_description = fp.read()
setup( setup(
name = __project__, name=__project__,
version = __version__, version=__version__,
description = 'A high-powered command line menu framework.', description="A high-powered command line menu framework.",
long_description = long_description, long_description=long_description,
author = 'Mathew Guest', author="Mathew Guest",
author_email = 't3h.zavage@gmail.com', author_email="t3h.zavage@gmail.com",
url = 'https://git-mirror.zavage.net/Mirror/app_skellington', url="https://git-mirror.zavage.net/Mirror/app_skellington",
license = 'MIT', license="MIT",
python_requires=">=3",
python_requires = '>=3', classifiers=[
"Development Status :: 3 - Alpha",
classifiers = [ "Environment :: Console",
'Development Status :: 3 - Alpha', "Framework :: Pytest",
'Environment :: Console', "Intended Audience :: Developers",
'Framework :: Pytest', "Intended Audience :: System Administrators",
'Intended Audience :: Developers', "License :: OSI Approved :: MIT License",
'Intended Audience :: System Administrators', "Natural Language :: English",
'License :: OSI Approved :: MIT License', "Operating System :: MacOS",
'Natural Language :: English', "Operating System :: Microsoft",
'Operating System :: MacOS', "Operating System :: Microsoft :: Windows",
'Operating System :: Microsoft', "Operating System :: OS Independent",
'Operating System :: Microsoft :: Windows', "Operating System :: POSIX",
'Operating System :: OS Independent', "Operating System :: POSIX :: Linux",
'Operating System :: POSIX', "Topic :: Software Development :: Libraries",
'Operating System :: POSIX :: Linux', "Topic :: Utilities",
'Topic :: Software Development :: Libraries',
'Topic :: Utilities'
], ],
# Third-party dependencies; will be automatically installed # Third-party dependencies; will be automatically installed
install_requires = ( install_requires=(
'appdirs', "appdirs",
'configobj', "configobj",
'colorlog', "colorlog",
), ),
# Local packages to be installed (our packages) # Local packages to be installed (our packages)
packages = ( packages=("app_skellington",),
'app_skellington',
),
) )

@ -1,45 +1,43 @@
from app_skellington.cfg import Config
from app_skellington import _util
import pytest import pytest
from app_skellington import _util
from app_skellington.cfg import Config
@pytest.fixture @pytest.fixture
def sample_configspec_filepath(): def sample_configspec_filepath():
return _util.get_asset(__name__, 'sample_config.spec') return _util.get_asset(__name__, "sample_config.spec")
@pytest.fixture @pytest.fixture
def sample_configini_filepath(): def sample_configini_filepath():
return _util.get_asset(__name__, 'sample_config.ini') return _util.get_asset(__name__, "sample_config.ini")
@pytest.fixture @pytest.fixture
def sample_full_configspec_filepath(): def sample_full_configspec_filepath():
return _util.get_asset(__name__, 'sample_config_full.spec') return _util.get_asset(__name__, "sample_config_full.spec")
@pytest.fixture @pytest.fixture
def sample_full_configini_filepath(): def sample_full_configini_filepath():
return _util.get_asset(__name__, 'sample_config_full.ini') return _util.get_asset(__name__, "sample_config_full.ini")
@pytest.fixture @pytest.fixture
def sample_invalid_configspec_filepath(): def sample_invalid_configspec_filepath():
return _util.get_asset(__name__, 'sample_config_invalid.spec') return _util.get_asset(__name__, "sample_config_invalid.spec")
class TestConfig_e2e: class TestConfig_e2e:
def test_allows_reading_ini_and_no_spec( def test_allows_reading_ini_and_no_spec(self, sample_configini_filepath):
self, sample_configini_filepath cfg = Config(configini_filepath=sample_configini_filepath)
): assert cfg["root_option"] == "root_option_val", "expecting default from config.spec (didnt get)"
cfg = Config( assert cfg["app"]["sub_option"] == "sub_option_val", "expecting default for sub option"
configini_filepath=sample_configini_filepath
)
assert cfg['root_option'] == 'root_option_val', 'expecting default from config.spec (didnt get)'
assert cfg['app']['sub_option'] == 'sub_option_val', 'expecting default for sub option'
def test_allows_reading_spec_and_no_ini( def test_allows_reading_spec_and_no_ini(self, sample_configspec_filepath):
self, sample_configspec_filepath cfg = Config(configspec_filepath=sample_configspec_filepath)
): assert cfg["root_option"] == "def_string", "expecting default from config.spec (didnt get)"
cfg = Config(
configspec_filepath=sample_configspec_filepath
)
assert cfg['root_option'] == 'def_string', 'expecting default from config.spec (didnt get)'
# NOTE(MG) Changed the functionality to not do it this way. # NOTE(MG) Changed the functionality to not do it this way.
# def test_constructor_fails_with_invalid_spec( # def test_constructor_fails_with_invalid_spec(
@ -50,48 +48,35 @@ class TestConfig_e2e:
# configspec_filepath=sample_invalid_configspec_filepath # configspec_filepath=sample_invalid_configspec_filepath
# ) # )
def test_allows_options_beyond_spec( def test_allows_options_beyond_spec(self, sample_configspec_filepath):
self, sample_configspec_filepath cfg = Config(configspec_filepath=sample_configspec_filepath)
): cfg["foo"] = "test my value"
cfg = Config( assert cfg["foo"] == "test my value"
configspec_filepath=sample_configspec_filepath
)
cfg['foo'] = 'test my value'
assert cfg['foo'] == 'test my value'
cfg['app']['bar'] = 'another value' cfg["app"]["bar"] = "another value"
assert cfg['app']['bar'] == 'another value' assert cfg["app"]["bar"] == "another value"
# def test_can_read_config_file_mutiple_times(self): # def test_can_read_config_file_mutiple_times(self):
# pass # pass
def test_can_override_config_file_manually( def test_can_override_config_file_manually(self, sample_configini_filepath):
self, sample_configini_filepath cfg = Config(configini_filepath=sample_configini_filepath)
): cfg["root_option"] = "newval"
cfg = Config( assert cfg["root_option"] == "newval"
configini_filepath=sample_configini_filepath
)
cfg['root_option'] = 'newval'
assert cfg['root_option'] == 'newval'
cfg['app']['sub_option'] = 'another_new_val' cfg["app"]["sub_option"] = "another_new_val"
assert cfg['app']['sub_option'] == 'another_new_val', 'expecting default for sub option' assert cfg["app"]["sub_option"] == "another_new_val", "expecting default for sub option"
def test_can_set_option_without_config(self): def test_can_set_option_without_config(self):
cfg = Config() cfg = Config()
cfg['foo'] = 'test my value' cfg["foo"] = "test my value"
assert cfg['foo'] == 'test my value' assert cfg["foo"] == "test my value"
cfg['app'] = {} cfg["app"] = {}
cfg['app']['bar'] = 'another value' cfg["app"]["bar"] = "another value"
assert cfg['app']['bar'] == 'another value' assert cfg["app"]["bar"] == "another value"
def test_uses_spec_as_defaults(
self, sample_configspec_filepath
):
cfg = Config(
configspec_filepath=sample_configspec_filepath
)
assert cfg['root_option'] == 'def_string', 'expecting default from config.spec (didnt get)'
assert cfg['app']['sub_option'] == 'def_sub', 'expecting default for sub option'
def test_uses_spec_as_defaults(self, sample_configspec_filepath):
cfg = Config(configspec_filepath=sample_configspec_filepath)
assert cfg["root_option"] == "def_string", "expecting default from config.spec (didnt get)"
assert cfg["app"]["sub_option"] == "def_sub", "expecting default for sub option"

@ -1,8 +1,7 @@
from app_skellington.cli import CommandTree from app_skellington.cli import CommandTree
class TestCli_e2e: class TestCli_e2e:
def test_null_constructor_works(self): def test_null_constructor_works(self):
x = CommandTree() x = CommandTree()
assert True == True assert True == True