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"] 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
@ -29,9 +29,3 @@ repos:
hooks: hooks:
- id: black - id: black
language_version: python3.8 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 App Skellington
MIT No Attribution
Copyright (c) 2024 Mathew Guest Copyright (c) 2024 Mathew Guest
Permission is hereby granted, free of charge, to any person Permission is hereby granted, free of charge, to any person obtaining a
obtaining a copy of this software and associated documentation copy of this software and associated documentation files (the
files (the "Software"), to deal in the Software without "Software"), to deal in the Software without restriction, including
restriction, including without limitation the rights to use, without limitation the rights to use, copy, modify, merge, publish,
copy, modify, merge, publish, distribute, sublicense, and/or sell distribute, sublicense, and/or sell copies of the Software, and to permit
copies of the Software, and to permit persons to whom the persons to whom the Software is furnished to do so.
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.
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: Application framework for Python, features include:
- Pain-free multi-level command menu: Expose public class methods as commands available to user. - 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 - 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: 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
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 Site configurations are supported through ConfigObj. There is a config.spec
in the src directory which is a validation file; it contains the accepted in the src directory which is a validation file; it contains the accepted
parameter names, types, and limits for configurable options in the parameter names, types, and limits for configurable options in the
@ -32,21 +57,19 @@ parameters are added into the config file.
Linux: Linux:
/home/\<user\>/.config/\<app_name\>/config.ini * /home/\<user\>/.config/\<app_name\>/config.ini
* /home/\<user\>/.cache/\<app_name\>/log/\<app_name\>.log
/home/\<user\>/.cache/\<app_name\>/log/\<app_name\>.log
Windows: Windows:
C:\\Users\\\<user>\\\<app_name\>\\Local\\\<app_name\>\\config.ini * 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\>\\Logs\\\<app_name\>.log
Application configuration can be overridden ad-hoc through the --config <filename> Application configuration can be overridden ad-hoc through the --config <filename>
argument. argument.
Debug - Turn on Logging # Debug - Turn on Logging
-----------------------
Set 'APPSKELLINGTON_ENABLE_LOGGING' environment variable to any value which turns Set 'APPSKELLINGTON_ENABLE_LOGGING' environment variable to any value which turns
on AppSkellington-level logging. For example, on AppSkellington-level logging. For example,
@ -57,23 +80,32 @@ or
export APPSKELLINGTON_DEBUG=1 export APPSKELLINGTON_DEBUG=1
<executable> <executable>
Tests # Tests
-----
Tests are a WIP. Recommendation is to run 'pytest' in the 'tests' directory. 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: Clone the repo:
```commandline ```commandline
git clone https://git-mirror.zavage.net/zavage-software/app_skellington.git git clone https://git-repos.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
``` ```
Install pre-commit hooks: Install pre-commit hooks:
@ -81,16 +113,68 @@ Install pre-commit hooks:
pre-commit install pre-commit install
``` ```
Begin development. Build:
```
python -m build
```
License Install:
-------
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.
Notes ```
----- pip install .
See official website: https://zavage-software.com ```
Please report bugs, improvements, or feedback!
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 # flake8: noqa
import sys
from .app_container import * from .app_container import (
from .cfg import * DEFAULT_APP_AUTHOR,
from .cli import * DEFAULT_APP_NAME,
from .log import * 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 = 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:

@ -4,8 +4,6 @@ import inspect
import os import os
import sys import sys
from . import _util
def eprint(*args, **kwargs): def eprint(*args, **kwargs):
""" """
@ -30,9 +28,9 @@ def does_file_exist(filepath):
instant in execution. instant in execution.
""" """
try: try:
fp = open(filepath, "r") open(filepath, "r")
return True return True
except FileNotFoundError as ex: except FileNotFoundError:
return False return False
@ -88,7 +86,7 @@ def get_asset(module, filepath):
root = os.path.realpath(root) root = os.path.realpath(root)
root = os.path.dirname(os.path.abspath(root)) root = os.path.dirname(os.path.abspath(root))
except Exception as ex: except Exception:
raise raise
path = os.path.join(root, filepath) 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 functools
import inspect import inspect
import logging
import os import os
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 +39,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
@ -89,7 +83,7 @@ class ApplicationContainer:
""" """
try: try:
del self._dependencies[service_name] del self._dependencies[service_name]
except KeyError as ex: except KeyError:
pass pass
def __getitem__(self, service_name): def __getitem__(self, service_name):
@ -100,9 +94,11 @@ 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:
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
@ -160,7 +156,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)
@ -179,12 +177,9 @@ class ApplicationContainer:
return False return False
try: try:
self.cli.run_command() self.cli.run_command()
except NoCommandSpecified as ex: except NoCommandSpecified:
print("Failure: No command specified.") print("Failure: No command specified.")
def interactive_shell(self):
pass
def invoke_from_cli(self): def invoke_from_cli(self):
self.invoke_command() self.invoke_command()

@ -4,15 +4,10 @@
# 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 configobj
import validate import validate
from . import _util
from ._bootstrap import _bootstrap_logger from ._bootstrap import _bootstrap_logger
@ -31,7 +26,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 +90,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:
_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()
@ -106,7 +107,7 @@ class Config:
try: try:
has_item = key in self._config_obj has_item = key in self._config_obj
return has_item return has_item
except KeyError as ex: except KeyError:
pass pass
def __delitem__(self, key): def __delitem__(self, key):
@ -116,7 +117,7 @@ class Config:
""" """
try: try:
del self[key] del self[key]
except KeyError as ex: except KeyError:
pass pass
def __getitem__(self, key): def __getitem__(self, key):
@ -130,7 +131,7 @@ class Config:
else: else:
# return self._config_obj[key].dict() # return self._config_obj[key].dict()
return self._config_obj[key] return self._config_obj[key]
except KeyError as ex: except KeyError:
raise raise
def __setitem__(self, key, value): def __setitem__(self, key, value):
@ -150,7 +151,7 @@ class Config:
try: try:
v = self.__getitem__(key) v = self.__getitem__(key)
return v return v
except KeyError as ex: except KeyError:
return default return default
def load_config(self, configspec_filepath=None, configini_filepath=None): def load_config(self, configspec_filepath=None, configini_filepath=None):
@ -187,20 +188,23 @@ 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:
msg = "cfg - Failed to load config: error in config.spec configuration: {}".format(config_filepath) msg = f"cfg - Failed to load config: error in config.spec configuration: {self.configspec_filepath}"
_bootstrap_logger.error(msg) _bootstrap_logger.error(msg)
return False return False
except OSError as ex: except OSError:
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:
print(ex) _bootstrap_logger.error(ex)
return False
def _validate_config_against_spec(self): def _validate_config_against_spec(self):
config_spec = self.configspec_filepath config_spec = self.configspec_filepath
@ -217,7 +221,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 +233,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:
_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",

@ -1,12 +1,9 @@
import argparse import argparse
import inspect import inspect
import logging
import re import re
import sys import sys
import app_skellington from . import NoCommandSpecified, app_container
from . import app_container
from ._bootstrap import _bootstrap_logger 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
@ -44,12 +41,12 @@ class CommandTree:
./scriptname --option="value" [submenu] [command] ./scriptname --option="value" [submenu] [command]
is different than is different from
./scriptname [submenu] [command] --option="value" ./scriptname [submenu] [command] --option="value"
in that option is being applied to the application in the first example and 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 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.
""" """
@ -69,13 +66,15 @@ class CommandTree:
self._single_command = None self._single_command = None
def print_tree(self): def print_tree(self):
raise NotImplemented raise NotImplementedError
def add_argument(self, *args, **kwargs): def add_argument(self, *args, **kwargs):
""" """
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,15 +82,7 @@ class CommandTree:
Creates a root-level submenu with no entries. SubMenu node is Creates a root-level submenu with no entries. SubMenu node is
returned which can have submenus and commands attached to it. 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} 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. # Creates an argument as a slot in the underlying argparse.
subparsers = self.root_parser.add_subparsers(**func_args) subparsers = self.root_parser.add_subparsers(**func_args)
@ -100,13 +91,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.
@ -136,13 +131,13 @@ class CommandTree:
# help is displayed next to the command in the submenu enumeration or # help is displayed next to the command in the submenu enumeration or
# list of commands: # 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 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 # end copy-paste from SubMenu.register_command
# begin copy-paste then editted from SubMenu.register_command # begin copy-paste then edited from SubMenu.register_command
# For each paramter in the function create an argparse argument in # For each parameter 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":
@ -154,7 +149,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)
@ -169,7 +166,7 @@ class CommandTree:
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 edited from SubMenu.register_command
self._cmd_tree_is_single_command = True self._cmd_tree_is_single_command = True
self._single_command = cmd self._single_command = cmd
@ -200,10 +197,16 @@ class CommandTree:
return pargs, unk, True return pargs, unk, True
# Note: SystemExit is raised when '-h' argument is supplied. # Note: SystemExit is raised when '-h' argument is supplied.
except SystemExit as ex: except SystemExit:
return None, None, False return None, None, False
def run_command(self, args=None): def run_command(self, args=None):
"""
Args:
args (list[str]): Direct from STDIN
Raises:
NoCommandSpecified for invalid command.
"""
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")
@ -219,7 +222,7 @@ class CommandTree:
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 raise NoCommandSpecified
return self._invoke_command(cmd, args) return self._invoke_command(cmd, args)
@ -230,16 +233,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 is 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 +260,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 +310,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 +386,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 +439,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
@ -484,7 +503,7 @@ class HelpGenerator:
The 'help' text is displayed next to the command when enumerating The 'help' text is displayed next to the command when enumerating
the submenu commands. the submenu commands.
""" """
if doctext == None: if doctext is 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)
@ -498,7 +517,7 @@ class HelpGenerator:
The 'description' paragraph is provided when the user requests help The 'description' paragraph is provided when the user requests help
on a specific command. on a specific command.
""" """
if doctext == None: if doctext is 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)

@ -3,7 +3,6 @@ import logging.config
import os import os
import appdirs import appdirs
import colorlog
from . import _util from . import _util
from ._bootstrap import _bootstrap_logger, _logger_name 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", "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 +70,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)
@ -107,20 +114,27 @@ class LoggingLayer:
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:
_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 +147,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"
@ -151,7 +168,7 @@ class LoggingLayer:
""" """
try: try:
s = dict_[key] s = dict_[key]
except KeyError as ex: except KeyError:
raise raise
if s == "critical": if s == "critical":
dict_[key] = logging.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] [build-system]
requires = ["poetry-core>=1.0.0"] requires = ["setuptools>=45", "setuptools_scm[toml]>=6.2"]
build-backend = "poetry.core.masonry.api" build-backend = "setuptools.build_meta"
[tool.black] [project]
line-length = 120 name = "app_skellington"
target-version = ['py38'] 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 [project.urls]
combine_as_imports = true homepage = "https://zavage-software.com/portfolio/app_skellington"
include_trailing_comma = true repository = "https://git-repos.zavage.net/Zavage-Software/app_skellington"
force_grid_wrap = 3 documentation = "https://git-repos.zavage.net/Zavage-Software/app_skellington"
ensure_newline_before_comments = true
[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: 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"