Merge branch 'main' of git.zavage.net:Zavage-Software/app_skellington

This commit is contained in:
Mathew Guest 2024-11-20 04:19:36 -07:00
commit 10e873d2d6
15 changed files with 371 additions and 655 deletions

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

@ -1,25 +1,20 @@
App Skellington
MIT No Attribution
Copyright (c) 2024 Mathew Guest
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to permit
persons to whom the Software is furnished to do so.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
`NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
USE OR OTHER DEALINGS IN THE SOFTWARE.

160
README.md

@ -1,5 +1,10 @@
app_skellington
===============
---
gitea: none
include_toc: true
---
**app_skellington**
Application framework for Python, features include:
- Pain-free multi-level command menu: Expose public class methods as commands available to user.
- Simple to define services and automatic dependency injection based on name (with custom invocation as an option). \*WIP
@ -10,11 +15,31 @@ 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
Application Configuration
-------------------------
# Python Package Index (PyPi) - Installation Instruction
This is the project page for PyPi: The Python Package Index for community third-party libraries
https://pypi.org/project/app-skellington/
```
pip install app_skellington # install
pip uninstall app_skellington # uninstall
pip download app_skellington # download .whl wheel files for redistributable install
pip install -U app_skellington # upgrade
pip list # list install packages in environment
pip index version app_skellington # enumerate available distributions in pypi for package
```
# Description
This is a library. There is nothing to run by itself. It would be helpful to have a sample application that uses
it or something, but I don't have that ready at the moment.
# Application Configuration
Site configurations are supported through ConfigObj. There is a config.spec
in the src directory which is a validation file; it contains the accepted
parameter names, types, and limits for configurable options in the
@ -32,21 +57,19 @@ parameters are added into the config file.
Linux:
/home/\<user\>/.config/\<app_name\>/config.ini
/home/\<user\>/.cache/\<app_name\>/log/\<app_name\>.log
* /home/\<user\>/.config/\<app_name\>/config.ini
* /home/\<user\>/.cache/\<app_name\>/log/\<app_name\>.log
Windows:
C:\\Users\\\<user>\\\<app_name\>\\Local\\\<app_name\>\\config.ini
C:\\Users\\\<user>\\\<app_name\>\\Local\\\<app_name\>\\Logs\\\<app_name\>.log
* C:\\Users\\\<user>\\\<app_name\>\\Local\\\<app_name\>\\config.ini
* C:\\Users\\\<user>\\\<app_name\>\\Local\\\<app_name\>\\Logs\\\<app_name\>.log
Application configuration can be overridden ad-hoc through the --config <filename>
argument.
Debug - Turn on Logging
-----------------------
# Debug - Turn on Logging
Set 'APPSKELLINGTON_ENABLE_LOGGING' environment variable to any value which turns
on AppSkellington-level logging. For example,
@ -57,23 +80,32 @@ or
export APPSKELLINGTON_DEBUG=1
<executable>
Tests
-----
Tests are a WIP. Recommendation is to run 'pytest' in the 'tests' directory.
# Tests
Tests are a WIP and not fully built out yet. Recommendation is to run 'pytest' in the 'tests' directory.
# Development
I recommend pyenv to install a reliable, controlled python of preferred version locally.
```
curl https://pyenv.run | bash
# Add to .bashrc or similar for different shells:
tee -a "$HOME"/.profile <<'EOF'
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"
EOF
```
* reference https://github.com/pyenv/pyenv
* Use pyenv to install desired python version, and/or create any virtual environments you desire
Development
-----------
Clone the repo:
```commandline
git clone https://git-mirror.zavage.net/zavage-software/app_skellington.git
```
Verify your desired python environment.
Poetry install to install dependencies and the program in your desired python environment
```commandline
poetry install
# verify no errors
git clone https://git-repos.zavage.net/zavage-software/app_skellington.git
```
Install pre-commit hooks:
@ -81,16 +113,68 @@ Install pre-commit hooks:
pre-commit install
```
Begin development.
Build:
```
python -m build
```
License
-------
I'm releasing this software under one of the most permissive
licenses, the MIT software license. This applies to this source repository
and all files within it. Alternatively, you are permitted you
to use any of this under the GPL to the fullest legal extent allowed.
Install:
Notes
-----
See official website: https://zavage-software.com
Please report bugs, improvements, or feedback!
```
pip install .
```
Formatting and Linters:
```
black app_skellington
isort app_skellington
flake8 app_skellington
```
Publish:
```
# Push latest commit, or on commit ready to publish:
git push
# Create a tag with the desired version number and push:
git tag -a v0.2.0 -m "0.2.0 provides modern pyproject.toml build with setuptools, versioning, and publishing"
git push origin v0.2.0
# Build the wheel:
python -m build
# Publish to pypi:
twine check dist/*
twine upload dist/*
```
* Reference https://packaging.python.org/en/latest/overview/
# Version
setuptools_scm will infer the version based on the latest tag in your Git history.
Ensure you are tagging your commits with meaningful version numbers like v1.0.0, v1.1.0, etc.
You can view the current version number with the command:
python -m setuptools_scm
# License
See [license](LICENSE.txt)
MIT no attribution required - https://opensource.org/license/mit-0
* Allows commercial use.
* Allows modifications and closed-source derivatives.
* Fully interoperable with nearly all other open-source licenses, including GPL (when combined properly).
# See Also
* Project page: https://zavage-software.com/portfolio/app_skellington
* Please report bugs, improvements, or feedback!
* Contact: mat@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/

@ -1,7 +1,19 @@
import logging
import sys
# flake8: noqa
from .app_container import *
from .cfg import *
from .cli import *
from .log import *
from .app_container import (
DEFAULT_APP_AUTHOR,
DEFAULT_APP_NAME,
ApplicationContainer,
ApplicationContext,
NoCommandSpecified,
ServiceNotFound,
)
from .cfg import Config, EnvironmentVariables
from .cli import (
EXPLICIT_FAIL_ON_UNKNOWN_ARGS,
CommandEntry,
CommandTree,
HelpGenerator,
SubMenu,
)
from .log import LoggingLayer

@ -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:

@ -4,8 +4,6 @@ import inspect
import os
import sys
from . import _util
def eprint(*args, **kwargs):
"""
@ -30,9 +28,9 @@ def does_file_exist(filepath):
instant in execution.
"""
try:
fp = open(filepath, "r")
open(filepath, "r")
return True
except FileNotFoundError as ex:
except FileNotFoundError:
return False
@ -88,7 +86,7 @@ def get_asset(module, filepath):
root = os.path.realpath(root)
root = os.path.dirname(os.path.abspath(root))
except Exception as ex:
except Exception:
raise
path = os.path.join(root, filepath)

@ -0,0 +1,17 @@
# file generated by setuptools_scm
# don't change, don't track in version control
TYPE_CHECKING = False
if TYPE_CHECKING:
from typing import Tuple, Union
VERSION_TUPLE = Tuple[Union[int, str], ...]
else:
VERSION_TUPLE = object
version: str
__version__: str
__version_tuple__: VERSION_TUPLE
version_tuple: VERSION_TUPLE
__version__ = version = "0.2.2"
__version_tuple__ = version_tuple = (0, 2, 2)

@ -1,18 +1,10 @@
import collections
import functools
import inspect
import logging
import os
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 +39,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
@ -89,7 +83,7 @@ class ApplicationContainer:
"""
try:
del self._dependencies[service_name]
except KeyError as ex:
except KeyError:
pass
def __getitem__(self, service_name):
@ -100,9 +94,11 @@ 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:
except KeyError:
msg = "failed to inject service: {}".format(service_name)
_bootstrap_logger.critical(msg)
raise ServiceNotFound
@ -160,7 +156,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)
@ -179,12 +177,9 @@ class ApplicationContainer:
return False
try:
self.cli.run_command()
except NoCommandSpecified as ex:
except NoCommandSpecified:
print("Failure: No command specified.")
def interactive_shell(self):
pass
def invoke_from_cli(self):
self.invoke_command()

@ -4,15 +4,10 @@
# ConfigObj module and it's recommended to use config.spec files to define
# your available configuration of the relevant application.
import argparse
import os
import sys
import appdirs
import configobj
import validate
from . import _util
from ._bootstrap import _bootstrap_logger
@ -31,7 +26,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 +90,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)
except OSError:
_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()
@ -106,7 +107,7 @@ class Config:
try:
has_item = key in self._config_obj
return has_item
except KeyError as ex:
except KeyError:
pass
def __delitem__(self, key):
@ -116,7 +117,7 @@ class Config:
"""
try:
del self[key]
except KeyError as ex:
except KeyError:
pass
def __getitem__(self, key):
@ -130,7 +131,7 @@ class Config:
else:
# return self._config_obj[key].dict()
return self._config_obj[key]
except KeyError as ex:
except KeyError:
raise
def __setitem__(self, key, value):
@ -150,7 +151,7 @@ class Config:
try:
v = self.__getitem__(key)
return v
except KeyError as ex:
except KeyError:
return default
def load_config(self, configspec_filepath=None, configini_filepath=None):
@ -187,20 +188,23 @@ 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)
except configobj.ParseError:
msg = f"cfg - Failed to load config: error in config.spec configuration: {self.configspec_filepath}"
_bootstrap_logger.error(msg)
return False
except OSError as ex:
except OSError:
msg = "cfg - Failed to load config: config.spec file not found."
_bootstrap_logger.error(msg)
return False
except Exception as ex:
print(ex)
_bootstrap_logger.error(ex)
return False
def _validate_config_against_spec(self):
config_spec = self.configspec_filepath
@ -217,7 +221,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 +233,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. ")
except ValueError:
_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",

@ -1,12 +1,9 @@
import argparse
import inspect
import logging
import re
import sys
import app_skellington
from . import app_container
from . import NoCommandSpecified, app_container
from ._bootstrap import _bootstrap_logger
# If explicit fail is enabled, any command with at least one unknown
@ -44,12 +41,12 @@ class CommandTree:
./scriptname --option="value" [submenu] [command]
is different than
is different from
./scriptname [submenu] [command] --option="value"
in that option is being applied to the application in the first example and
applied to the refresh_datasets command (under the nhsn command group) in
applied to the command (under the submenu command group) in
the second. In the same way the -h, --help options print different docs
depending on where the help option was passed.
"""
@ -69,13 +66,15 @@ class CommandTree:
self._single_command = None
def print_tree(self):
raise NotImplemented
raise NotImplementedError
def add_argument(self, *args, **kwargs):
"""
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):
@ -83,15 +82,7 @@ class CommandTree:
Creates a root-level submenu with no entries. SubMenu node is
returned which can have submenus and commands attached to it.
"""
# NOTE(MG) Fix for Python>=3.7,
# argparse.ArgumentParser added 'required' argument.
# Must also be written into SubMenu.create_submenu.
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")
del func_args["required"]
# END fix for Python<3.7
# Creates an argument as a slot in the underlying argparse.
subparsers = self.root_parser.add_subparsers(**func_args)
@ -100,13 +91,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.
@ -136,13 +131,13 @@ class CommandTree:
# help is displayed next to the command in the submenu enumeration or
# list of commands:
help_text = HelpGenerator.generate_help_from_sig(docstring)
HelpGenerator.generate_help_from_sig(docstring)
# description is displayed when querying help for the specific command:
description_text = HelpGenerator.generate_description_from_sig(docstring)
HelpGenerator.generate_description_from_sig(docstring)
# end copy-paste from SubMenu.register_command
# begin copy-paste then editted from SubMenu.register_command
# For each paramter in the function create an argparse argument in
# begin copy-paste then edited from SubMenu.register_command
# For each parameter in the function create an argparse argument in
# the child ArgumentParser created for this menu entry:
for key in params:
if key == "self":
@ -154,7 +149,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)
@ -169,7 +166,7 @@ class CommandTree:
registered_name = cmd_name
_bootstrap_logger.info("registered command: %s", registered_name)
# end copy-paste then editted from SubMenu.register_command
# end copy-paste then edited from SubMenu.register_command
self._cmd_tree_is_single_command = True
self._single_command = cmd
@ -200,10 +197,16 @@ class CommandTree:
return pargs, unk, True
# Note: SystemExit is raised when '-h' argument is supplied.
except SystemExit as ex:
except SystemExit:
return None, None, False
def run_command(self, args=None):
"""
Args:
args (list[str]): Direct from STDIN
Raises:
NoCommandSpecified for invalid command.
"""
args, unk, success = self.parse(args)
if not success:
_bootstrap_logger.info("cli - SystemExit: Perhaps user invoked --help")
@ -219,7 +222,7 @@ class CommandTree:
cmd = self._lookup_command(args)
if cmd is None:
_bootstrap_logger.critical("cli - Failed to find command.")
return False
raise NoCommandSpecified
return self._invoke_command(cmd, args)
@ -230,16 +233,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 is 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 +260,14 @@ class CommandTree:
input("<broken>")
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 +310,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 +386,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 +439,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
@ -484,7 +503,7 @@ class HelpGenerator:
The 'help' text is displayed next to the command when enumerating
the submenu commands.
"""
if doctext == None:
if doctext is None:
return doctext
regex = "(.*?)[.?!]"
match = re.match(regex, doctext, re.MULTILINE | re.DOTALL)
@ -498,7 +517,7 @@ class HelpGenerator:
The 'description' paragraph is provided when the user requests help
on a specific command.
"""
if doctext == None:
if doctext is None:
return doctext
regex = "(.*?)[.?!]"
match = re.match(regex, doctext, re.MULTILINE | re.DOTALL)

@ -3,7 +3,6 @@ import logging.config
import os
import appdirs
import colorlog
from . import _util
from ._bootstrap import _bootstrap_logger, _logger_name
@ -16,7 +15,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 +70,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)
@ -107,20 +114,27 @@ class LoggingLayer:
if config_dict["loggers"].get("root") is not None:
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")
except Exception:
_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 +147,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"
@ -151,7 +168,7 @@ class LoggingLayer:
"""
try:
s = dict_[key]
except KeyError as ex:
except KeyError:
raise
if s == "critical":
dict_[key] = logging.CRITICAL

371
poetry.lock generated

@ -1,371 +0,0 @@
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
[[package]]
name = "black"
version = "24.4.2"
description = "The uncompromising code formatter."
optional = false
python-versions = ">=3.8"
files = [
{file = "black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce"},
{file = "black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021"},
{file = "black-24.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063"},
{file = "black-24.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96"},
{file = "black-24.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474"},
{file = "black-24.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c"},
{file = "black-24.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb"},
{file = "black-24.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1"},
{file = "black-24.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d"},
{file = "black-24.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04"},
{file = "black-24.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc"},
{file = "black-24.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0"},
{file = "black-24.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bf10f7310db693bb62692609b397e8d67257c55f949abde4c67f9cc574492cc7"},
{file = "black-24.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:98e123f1d5cfd42f886624d84464f7756f60ff6eab89ae845210631714f6db94"},
{file = "black-24.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48a85f2cb5e6799a9ef05347b476cce6c182d6c71ee36925a6c194d074336ef8"},
{file = "black-24.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:b1530ae42e9d6d5b670a34db49a94115a64596bc77710b1d05e9801e62ca0a7c"},
{file = "black-24.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1"},
{file = "black-24.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741"},
{file = "black-24.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e"},
{file = "black-24.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7"},
{file = "black-24.4.2-py3-none-any.whl", hash = "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c"},
{file = "black-24.4.2.tar.gz", hash = "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d"},
]
[package.dependencies]
click = ">=8.0.0"
mypy-extensions = ">=0.4.3"
packaging = ">=22.0"
pathspec = ">=0.9.0"
platformdirs = ">=2"
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""}
[package.extras]
colorama = ["colorama (>=0.4.3)"]
d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"]
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
uvloop = ["uvloop (>=0.15.2)"]
[[package]]
name = "cfgv"
version = "3.4.0"
description = "Validate configuration and produce human readable error messages."
optional = false
python-versions = ">=3.8"
files = [
{file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"},
{file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"},
]
[[package]]
name = "click"
version = "8.1.7"
description = "Composable command line interface toolkit"
optional = false
python-versions = ">=3.7"
files = [
{file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
{file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
]
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "distlib"
version = "0.3.8"
description = "Distribution utilities"
optional = false
python-versions = "*"
files = [
{file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"},
{file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"},
]
[[package]]
name = "filelock"
version = "3.15.4"
description = "A platform independent file lock."
optional = false
python-versions = ">=3.8"
files = [
{file = "filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7"},
{file = "filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb"},
]
[package.extras]
docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"]
testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"]
typing = ["typing-extensions (>=4.8)"]
[[package]]
name = "flake8"
version = "5.0.4"
description = "the modular source code checker: pep8 pyflakes and co"
optional = false
python-versions = ">=3.6.1"
files = [
{file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"},
{file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"},
]
[package.dependencies]
mccabe = ">=0.7.0,<0.8.0"
pycodestyle = ">=2.9.0,<2.10.0"
pyflakes = ">=2.5.0,<2.6.0"
[[package]]
name = "identify"
version = "2.6.0"
description = "File identification library for Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "identify-2.6.0-py2.py3-none-any.whl", hash = "sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0"},
{file = "identify-2.6.0.tar.gz", hash = "sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf"},
]
[package.extras]
license = ["ukkonen"]
[[package]]
name = "isort"
version = "5.13.2"
description = "A Python utility / library to sort Python imports."
optional = false
python-versions = ">=3.8.0"
files = [
{file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"},
{file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"},
]
[package.extras]
colors = ["colorama (>=0.4.6)"]
[[package]]
name = "mccabe"
version = "0.7.0"
description = "McCabe checker, plugin for flake8"
optional = false
python-versions = ">=3.6"
files = [
{file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"},
{file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
]
[[package]]
name = "mypy-extensions"
version = "1.0.0"
description = "Type system extensions for programs checked with the mypy type checker."
optional = false
python-versions = ">=3.5"
files = [
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
]
[[package]]
name = "nodeenv"
version = "1.9.1"
description = "Node.js virtual environment builder"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
files = [
{file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"},
{file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"},
]
[[package]]
name = "packaging"
version = "24.1"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.8"
files = [
{file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
{file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
]
[[package]]
name = "pathspec"
version = "0.12.1"
description = "Utility library for gitignore style pattern matching of file paths."
optional = false
python-versions = ">=3.8"
files = [
{file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
{file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
]
[[package]]
name = "platformdirs"
version = "4.2.2"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
optional = false
python-versions = ">=3.8"
files = [
{file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"},
{file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"},
]
[package.extras]
docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"]
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"]
type = ["mypy (>=1.8)"]
[[package]]
name = "pre-commit"
version = "3.5.0"
description = "A framework for managing and maintaining multi-language pre-commit hooks."
optional = false
python-versions = ">=3.8"
files = [
{file = "pre_commit-3.5.0-py2.py3-none-any.whl", hash = "sha256:841dc9aef25daba9a0238cd27984041fa0467b4199fc4852e27950664919f660"},
{file = "pre_commit-3.5.0.tar.gz", hash = "sha256:5804465c675b659b0862f07907f96295d490822a450c4c40e747d0b1c6ebcb32"},
]
[package.dependencies]
cfgv = ">=2.0.0"
identify = ">=1.0.0"
nodeenv = ">=0.11.1"
pyyaml = ">=5.1"
virtualenv = ">=20.10.0"
[[package]]
name = "pycodestyle"
version = "2.9.1"
description = "Python style guide checker"
optional = false
python-versions = ">=3.6"
files = [
{file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"},
{file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"},
]
[[package]]
name = "pyflakes"
version = "2.5.0"
description = "passive checker of Python programs"
optional = false
python-versions = ">=3.6"
files = [
{file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"},
{file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"},
]
[[package]]
name = "pyyaml"
version = "6.0.1"
description = "YAML parser and emitter for Python"
optional = false
python-versions = ">=3.6"
files = [
{file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"},
{file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"},
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
{file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"},
{file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
{file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
{file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
{file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"},
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
{file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"},
{file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
{file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"},
{file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"},
{file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"},
{file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"},
{file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"},
{file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"},
{file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"},
{file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"},
{file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"},
{file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"},
{file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"},
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
{file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"},
{file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
{file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
{file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
{file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"},
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
{file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"},
{file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
{file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
{file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
]
[[package]]
name = "tomli"
version = "2.0.1"
description = "A lil' TOML parser"
optional = false
python-versions = ">=3.7"
files = [
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
]
[[package]]
name = "typing-extensions"
version = "4.12.2"
description = "Backported and Experimental Type Hints for Python 3.8+"
optional = false
python-versions = ">=3.8"
files = [
{file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
]
[[package]]
name = "virtualenv"
version = "20.26.3"
description = "Virtual Python Environment builder"
optional = false
python-versions = ">=3.7"
files = [
{file = "virtualenv-20.26.3-py3-none-any.whl", hash = "sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589"},
{file = "virtualenv-20.26.3.tar.gz", hash = "sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a"},
]
[package.dependencies]
distlib = ">=0.3.7,<1"
filelock = ">=3.12.2,<4"
platformdirs = ">=3.9.1,<5"
[package.extras]
docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"]
[metadata]
lock-version = "2.0"
python-versions = "^3.8"
content-hash = "c20dac4ba2095b38a3009c1520b6cd1e9865cc50dff8559ddd767403898406c6"

@ -1,49 +1,45 @@
[tool.poetry]
name = "app_skellington"
version = "0.1.0"
description = "app_skellington CLI framework"
authors = [
"Mathew Guest <mathew.guest@davita.com>",
]
license = "Creative Commons"
readme = "README.md"
homepage = "https://zavage-software.com"
repository = "https://git-mirror.zavage.net/Zavage-Software/app_skellington"
documentation = "https://git-mirror.zavage.net/Zavage-Software/app_skellington"
keywords = [""]
packages = [{ include = "app_skellington" }]
include = [
"README.md",
]
# [tool.poetry.scripts]
[tool.poetry.dependencies]
python = "^3.8"
# psycopg2-binary = "^2.9"
[tool.poetry.group.dev.dependencies]
black = "*"
pre-commit = "*"
isort = "*"
flake8 = "*"
#pytest = "^7.2"
#Sphinx = "^5.3.0"
#sphinx-rtd-theme = "^1.3.0"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
requires = ["setuptools>=45", "setuptools_scm[toml]>=6.2"]
build-backend = "setuptools.build_meta"
[tool.black]
line-length = 120
target-version = ['py38']
[project]
name = "app_skellington"
dynamic = ["version"]
license = {file = "LICENSE.txt"}
readme = "README.md"
description = "app_skellington CLI framework"
requires-python = ">=3.8"
dependencies = [
"appdirs",
"configobj",
"colorlog"
]
authors = [
{name = "Mathew Guest", email = "mat@zavage.net"}
]
keywords = ["cli", "logging", "application"]
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent"
]
[tool.isort]
multi_line_output = 3
combine_as_imports = true
include_trailing_comma = true
force_grid_wrap = 3
ensure_newline_before_comments = true
[project.urls]
homepage = "https://zavage-software.com/portfolio/app_skellington"
repository = "https://git-repos.zavage.net/Zavage-Software/app_skellington"
documentation = "https://git-repos.zavage.net/Zavage-Software/app_skellington"
[project.optional-dependencies]
dev = [
"black",
"pre-commit",
"isort",
"flake8"
]
[tool.setuptools_scm]
version_file = "app_skellington/_version.py"
version_scheme = "release-branch-semver"
local_scheme = "node-and-date"

@ -1,66 +0,0 @@
#!/usr/bin/env python
#
# Usage:
#
# First, enable the python environment you want to install to, or if installing
# system-wide then ensure you're logged in with sufficient permissions
# (admin or root to install to system directories)
#
# installation:
#
# $ ./setup.py install
#
# de-installation:
#
# $ pip uninstall app_skellington
import os
from setuptools import setup
__project__ = "app_skellington"
__version__ = "0.1.1"
__description__ = "A high-powered command line menu framework."
long_description = __description__
readme_filepath = os.path.join(os.path.abspath(os.path.dirname(__file__)), "README.md")
with open(readme_filepath, encoding="utf-8") as fp:
long_description = fp.read()
setup(
name=__project__,
version=__version__,
description="A high-powered command line menu framework.",
long_description=long_description,
author="Mathew Guest",
author_email="t3h.zavage@gmail.com",
url="https://git-mirror.zavage.net/Mirror/app_skellington",
license="MIT",
python_requires=">=3",
classifiers=[
"Development Status :: 3 - Alpha",
"Environment :: Console",
"Framework :: Pytest",
"Intended Audience :: Developers",
"Intended Audience :: System Administrators",
"License :: OSI Approved :: MIT License",
"Natural Language :: English",
"Operating System :: MacOS",
"Operating System :: Microsoft",
"Operating System :: Microsoft :: Windows",
"Operating System :: OS Independent",
"Operating System :: POSIX",
"Operating System :: POSIX :: Linux",
"Topic :: Software Development :: Libraries",
"Topic :: Utilities",
],
# Third-party dependencies; will be automatically installed
install_requires=(
"appdirs",
"configobj",
"colorlog",
),
# Local packages to be installed (our packages)
packages=("app_skellington",),
)

@ -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"