style: black and isort

This commit is contained in:
Mathew Guest 2024-11-16 00:57:00 -07:00
parent 82543ce157
commit 25ded9e2b3
7 changed files with 130 additions and 47 deletions

@ -11,7 +11,7 @@ Application framework for Python, features include:
Principles: Principles:
- Lend to creating beautiful, easy to read and understand code in the application. - Lend to creating beautiful, easy to read and understand code in the application.
- Minimize coupling of applications to this framework. - Minimize coupling of applications to this framework.
- Compatable with Linux, Windows, and Mac. Try to be compatible as possible otherwise. - Compatible with Linux, Windows, and Mac. Try to be compatible as possible otherwise.
- Try to be compatible with alternate Python runtimes such as PyPy and older python environments. \*WIP - Try to be compatible with alternate Python runtimes such as PyPy and older python environments. \*WIP
# PyPi Hosted Link # PyPi Hosted Link
@ -103,6 +103,13 @@ Install:
pip install . pip install .
``` ```
Formatting and Linters:
```
black app_skellington
isort app_skellington
flake8 app_skellington
```
# Version # Version
@ -123,7 +130,6 @@ MIT no attribution required - https://opensource.org/license/mit-0
* Project page: https://zavage-software.com/portfolio/app_skellington * Project page: https://zavage-software.com/portfolio/app_skellington
* Please report bugs, improvements, or feedback! * Please report bugs, improvements, or feedback!
* Contact: mathew@zavage.net * Contact: mathew@zavage.net
* Packing and distribution conforms to PEP 621 https://peps.python.org/pep-0621/ * Packing and distribution conforms to PEP 621 https://peps.python.org/pep-0621/
* Reference https://packaging.python.org/en/latest/guides/distributing-packages-using-setuptools/ * Reference https://packaging.python.org/en/latest/guides/distributing-packages-using-setuptools/

@ -39,7 +39,9 @@ if os.environ.get("APPSKELLINGTON_DEBUG", None):
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:

@ -7,12 +7,7 @@ import sys
import appdirs import appdirs
from . import ( from . import _util, cfg, cli, log
_util,
cfg,
cli,
log,
)
# Application scaffolding: # Application scaffolding:
from ._bootstrap import _bootstrap_logger from ._bootstrap import _bootstrap_logger
@ -47,7 +42,9 @@ class ApplicationContainer:
directories. directories.
""" """
def __init__(self, configspec_filepath=None, configini_filepath=None, *args, **kwargs): def __init__(
self, configspec_filepath=None, configini_filepath=None, *args, **kwargs
):
self.appname = kwargs.get("appname") or DEFAULT_APP_NAME self.appname = kwargs.get("appname") or DEFAULT_APP_NAME
self.appauthor = kwargs.get("appauthor") or DEFAULT_APP_AUTHOR self.appauthor = kwargs.get("appauthor") or DEFAULT_APP_AUTHOR
@ -100,7 +97,9 @@ class ApplicationContainer:
app['datalayer'] => returns the made-up "datalayer" service. app['datalayer'] => returns the made-up "datalayer" service.
""" """
try: try:
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)
@ -160,7 +159,9 @@ class ApplicationContainer:
""" """
sig = inspect.signature(constructor.__init__) sig = inspect.signature(constructor.__init__)
params = sig.parameters params = sig.parameters
params = [params[paramname].name for paramname in params] # Convert Param() type => str params = [
params[paramname].name for paramname in params
] # Convert Param() type => str
cls_dependencies = params[1:] # Skip 'self' parameter on class methods. cls_dependencies = params[1:] # Skip 'self' parameter on class methods.
return functools.partial(self._construct_model, constructor, *cls_dependencies) return functools.partial(self._construct_model, constructor, *cls_dependencies)

@ -31,7 +31,9 @@ class Config:
"allow_options_beyond_spec": True, "allow_options_beyond_spec": True,
} }
def __init__(self, configspec_filepath=None, configini_filepath=None, capabilities=None): def __init__(
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
@ -93,11 +95,15 @@ 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("cfg - Set configspec and read contents: %s", filepath) _bootstrap_logger.debug(
"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("cfg - Failed to find config.spec: file not found (%s)", filepath) _bootstrap_logger.critical(
"cfg - Failed to find config.spec: file not found (%s)", filepath
)
raise OSError("Failed to read provided config.spec file") raise OSError("Failed to read provided config.spec file")
self.load_config() self.load_config()
@ -187,12 +193,16 @@ class Config:
# raise_errors # raise_errors
) )
_bootstrap_logger.debug( _bootstrap_logger.debug(
"cfg - Parsed configuration. config.spec = %s, config.ini = %s", config_spec, config_ini "cfg - Parsed configuration. config.spec = %s, config.ini = %s",
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:
@ -217,7 +227,9 @@ class Config:
self._config_obj, configobj.ConfigObj self._config_obj, configobj.ConfigObj
), "expecting configobj.ConfigObj, received %s" % type(self._config_obj) ), "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(val, copy=True, preserve_errors=True) test_results = self._config_obj.validate(
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",
@ -227,19 +239,27 @@ class Config:
return True return True
elif test_results is False: elif test_results is False:
_bootstrap_logger.debug("cfg - Potentially discovered invalid config.spec") _bootstrap_logger.debug(
"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(
_bootstrap_logger.critical("cfg - Config error info: %s %s %s", section_list, key, rslt) self._config_obj, test_results
):
_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( _bootstrap_logger.critical(
"cfg - Config failed validation: [%s].%s appears invalid. msg = %s", "cfg - Config failed validation: [%s].%s appears invalid. msg = %s",

@ -75,7 +75,9 @@ 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):
@ -89,7 +91,9 @@ class CommandTree:
func_args = {"dest": param_name, "metavar": param_name, "required": is_required} func_args = {"dest": param_name, "metavar": param_name, "required": is_required}
if sys.version_info.major == 3 and sys.version_info.minor < 7: 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
@ -100,13 +104,17 @@ class CommandTree:
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(self, func, cmd_name=None, func_signature=None, docstring=None): def register_command(
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.
@ -154,7 +162,9 @@ class CommandTree:
helptext = "default provided" helptext = "default provided"
else: else:
helptext = "default = '{}'".format(param.default) helptext = "default = '{}'".format(param.default)
self.root_parser.add_argument(key, help=helptext, nargs="?", default=param.default) self.root_parser.add_argument(
key, help=helptext, nargs="?", default=param.default
)
else: else:
helptext = "required" helptext = "required"
self.root_parser.add_argument(key, help=helptext) self.root_parser.add_argument(key, help=helptext)
@ -230,16 +240,22 @@ 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
@ -251,10 +267,14 @@ class CommandTree:
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]
@ -297,7 +317,9 @@ class SubMenu:
self.entries = {} self.entries = {}
def register_command(self, func, cmd_name=None, func_signature=None, docstring=None): def register_command(
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.
@ -371,7 +393,9 @@ class SubMenu:
helptext = "default provided" helptext = "default provided"
else: else:
helptext = "default = '{}'".format(param.default) helptext = "default = '{}'".format(param.default)
child_node.add_argument(key, help=helptext, nargs="?", default=param.default) child_node.add_argument(
key, help=helptext, nargs="?", default=param.default
)
else: else:
helptext = "required" helptext = "required"
child_node.add_argument(key, help=helptext) child_node.add_argument(key, help=helptext)
@ -422,7 +446,9 @@ class SubMenu:
func_args = {"dest": var_name, "metavar": var_name, "required": is_required} func_args = {"dest": var_name, "metavar": var_name, "required": is_required}
if sys.version_info.major == 3 and sys.version_info.minor < 7: 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

@ -16,7 +16,13 @@ DEFAULT_LOG_SETTINGS = {
"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": {
"stderr": {
"class": "logging.StreamHandler",
"level": "debug",
"formatter": "colored",
}
},
"loggers": { "loggers": {
"root": { "root": {
"handlers": [ "handlers": [
@ -65,7 +71,9 @@ 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)
@ -108,19 +116,26 @@ class LoggingLayer:
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"]) == config_dict["handlers"]["file"]["filename"]: if (
os.path.abspath(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):
@ -133,7 +148,10 @@ class LoggingLayer:
# 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] = {"level": "debug", "propagate": "false"} config_dict["loggers"][_logger_name] = {
"level": "debug",
"propagate": "false",
}
else: else:
config_dict["loggers"][_logger_name]["level"] = "debug" config_dict["loggers"][_logger_name]["level"] = "debug"

@ -32,12 +32,18 @@ def sample_invalid_configspec_filepath():
class TestConfig_e2e: class TestConfig_e2e:
def test_allows_reading_ini_and_no_spec(self, sample_configini_filepath): def test_allows_reading_ini_and_no_spec(self, sample_configini_filepath):
cfg = Config(configini_filepath=sample_configini_filepath) cfg = Config(configini_filepath=sample_configini_filepath)
assert cfg["root_option"] == "root_option_val", "expecting default from config.spec (didnt get)" assert (
assert cfg["app"]["sub_option"] == "sub_option_val", "expecting default for sub option" 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(self, sample_configspec_filepath): def test_allows_reading_spec_and_no_ini(self, sample_configspec_filepath):
cfg = Config(configspec_filepath=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["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(
@ -65,7 +71,9 @@ class TestConfig_e2e:
assert 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()
@ -78,5 +86,7 @@ class TestConfig_e2e:
def test_uses_spec_as_defaults(self, sample_configspec_filepath): def test_uses_spec_as_defaults(self, sample_configspec_filepath):
cfg = Config(configspec_filepath=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["root_option"] == "def_string"
), "expecting default from config.spec (didnt get)"
assert cfg["app"]["sub_option"] == "def_sub", "expecting default for sub option" assert cfg["app"]["sub_option"] == "def_sub", "expecting default for sub option"