diff --git a/README.md b/README.md index e6c55f8..0c84492 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Application framework for Python, features include: Principles: - Lend to creating beautiful, easy to read and understand code in the application. - 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 # PyPi Hosted Link @@ -103,6 +103,13 @@ Install: pip install . ``` +Formatting and Linters: +``` +black app_skellington +isort app_skellington +flake8 app_skellington +``` + # Version @@ -123,7 +130,6 @@ MIT no attribution required - https://opensource.org/license/mit-0 * Project page: https://zavage-software.com/portfolio/app_skellington * Please report bugs, improvements, or feedback! * Contact: mathew@zavage.net - + * 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/ - diff --git a/app_skellington/_bootstrap.py b/app_skellington/_bootstrap.py index 9fcc6f7..2470f46 100644 --- a/app_skellington/_bootstrap.py +++ b/app_skellington/_bootstrap.py @@ -39,7 +39,9 @@ if os.environ.get("APPSKELLINGTON_DEBUG", None): handler = logging.StreamHandler() handler.setFormatter(fmt) _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 else: diff --git a/app_skellington/app_container.py b/app_skellington/app_container.py index e20d3d8..237a7b9 100644 --- a/app_skellington/app_container.py +++ b/app_skellington/app_container.py @@ -7,12 +7,7 @@ import sys import appdirs -from . import ( - _util, - cfg, - cli, - log, -) +from . import _util, cfg, cli, log # Application scaffolding: from ._bootstrap import _bootstrap_logger @@ -47,7 +42,9 @@ class ApplicationContainer: 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.appauthor = kwargs.get("appauthor") or DEFAULT_APP_AUTHOR @@ -100,7 +97,9 @@ class ApplicationContainer: app['datalayer'] => returns the made-up "datalayer" service. """ 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 except KeyError as ex: msg = "failed to inject service: {}".format(service_name) @@ -160,7 +159,9 @@ class ApplicationContainer: """ sig = inspect.signature(constructor.__init__) 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. return functools.partial(self._construct_model, constructor, *cls_dependencies) diff --git a/app_skellington/cfg.py b/app_skellington/cfg.py index ac9baf7..ea112fd 100644 --- a/app_skellington/cfg.py +++ b/app_skellington/cfg.py @@ -31,7 +31,9 @@ class Config: "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._configini_data = None self._configini_filepath = None @@ -93,11 +95,15 @@ class Config: self._configspec_filepath = filepath self._configspec_data = data 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() return 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") self.load_config() @@ -187,12 +193,16 @@ class Config: # raise_errors ) _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 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) return False except OSError as ex: @@ -217,7 +227,9 @@ class Config: 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 - 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: _bootstrap_logger.info( "cfg- Successfully validated configuration against spec. input = %s, validation spec = %s", @@ -227,19 +239,27 @@ class Config: return True elif test_results is False: - _bootstrap_logger.debug("cfg - Potentially discovered invalid config.spec") + _bootstrap_logger.debug( + "cfg - Potentially discovered invalid config.spec" + ) else: self._validate_parse_errors(test_results) return False 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 def _validate_parse_errors(self, test_results): _bootstrap_logger.critical("cfg - Config file failed validation.") - 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) + 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 + ) if key is not None: _bootstrap_logger.critical( "cfg - Config failed validation: [%s].%s appears invalid. msg = %s", diff --git a/app_skellington/cli.py b/app_skellington/cli.py index b6b8680..2c791ca 100644 --- a/app_skellington/cli.py +++ b/app_skellington/cli.py @@ -75,7 +75,9 @@ class CommandTree: """ 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) 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} if sys.version_info.major == 3 and sys.version_info.minor < 7: 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"] # END fix for Python<3.7 @@ -100,13 +104,17 @@ class CommandTree: submenu.submenu_path = "" 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.submenu_param = param_name 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 command into underlying argparse options. @@ -154,7 +162,9 @@ class CommandTree: helptext = "default provided" else: 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: helptext = "required" self.root_parser.add_argument(key, help=helptext) @@ -230,16 +240,22 @@ class CommandTree: # the CommandTree with no SubMenu (submenu will be disabled # in this case): 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 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 # There is at least one submenu we need to go down: else: 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 argparse_param = self.submenu_param # e.g.: submenu_root @@ -251,10 +267,14 @@ class CommandTree: input("") 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) - _bootstrap_logger.debug("cli - lookup, entries[{}] = {}".format(val, lookup)) + _bootstrap_logger.debug( + "cli - lookup, entries[{}] = {}".format(val, lookup) + ) # pop value del args[argparse_param] @@ -297,7 +317,9 @@ class SubMenu: 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 converted into argparse arguments and made available to the user. @@ -371,7 +393,9 @@ class SubMenu: helptext = "default provided" else: 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: helptext = "required" child_node.add_argument(key, help=helptext) @@ -422,7 +446,9 @@ class SubMenu: func_args = {"dest": var_name, "metavar": var_name, "required": is_required} if sys.version_info.major == 3 and sys.version_info.minor < 7: 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"] # END fix for Python<3.7 diff --git a/app_skellington/log.py b/app_skellington/log.py index a134285..761ffef 100644 --- a/app_skellington/log.py +++ b/app_skellington/log.py @@ -16,7 +16,13 @@ DEFAULT_LOG_SETTINGS = { "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": { "root": { "handlers": [ @@ -65,7 +71,9 @@ class LoggingLayer: noise for typical operation. """ 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 self.transform_config(config_dict) @@ -108,19 +116,26 @@ class LoggingLayer: config_dict["loggers"][""] = config_dict["loggers"]["root"] del config_dict["loggers"]["root"] 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 if "file" not in config_dict["handlers"]: 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 pass else: dirname = appdirs.user_log_dir(self.appname, self.appauthor) _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 def _add_own_logconfig(self, config_dict): @@ -133,7 +148,10 @@ class LoggingLayer: # See _bootstrap.py if os.environ.get("APPSKELLINGTON_DEBUG", None): 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: config_dict["loggers"][_logger_name]["level"] = "debug" diff --git a/tests/cfg/test_cfg.py b/tests/cfg/test_cfg.py index 459dfe8..219033f 100644 --- a/tests/cfg/test_cfg.py +++ b/tests/cfg/test_cfg.py @@ -32,12 +32,18 @@ def sample_invalid_configspec_filepath(): class TestConfig_e2e: def test_allows_reading_ini_and_no_spec(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)" - assert cfg["app"]["sub_option"] == "sub_option_val", "expecting default for sub option" + 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(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["root_option"] == "def_string" + ), "expecting default from config.spec (didnt get)" # NOTE(MG) Changed the functionality to not do it this way. # def test_constructor_fails_with_invalid_spec( @@ -65,7 +71,9 @@ class TestConfig_e2e: assert cfg["root_option"] == "newval" 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): cfg = Config() @@ -78,5 +86,7 @@ class TestConfig_e2e: 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["root_option"] == "def_string" + ), "expecting default from config.spec (didnt get)" assert cfg["app"]["sub_option"] == "def_sub", "expecting default for sub option"