mirror of
https://git.zavage.net/Zavage-Software/app_skellington.git
synced 2025-04-19 15:19:20 -06:00
Compare commits
34 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
3e8ad8fbb3 | ||
|
d15517623d | ||
10e873d2d6 | |||
b743beb53a | |||
b2e082b7c6 | |||
f0fb18da71 | |||
afd2b5eb42 | |||
470514ba4b | |||
55a9796806 | |||
7647713498 | |||
f0a4bdbced | |||
adbae0074c | |||
fb0ef3d8f6 | |||
881a2db9dc | |||
ac4b765099 | |||
d76932bcb9 | |||
25ded9e2b3 | |||
82543ce157 | |||
67a8e6e945 | |||
321f50b542 | |||
edb7cd346a | |||
dab6154f33 | |||
5c6a486913 | |||
2b56b06f45 | |||
cb083cd38e | |||
9a1999b8ab | |||
586e7fa54d | |||
0547b69b28 | |||
|
fd5af59c59 | ||
1f8013bd49 | |||
6f5a6f5c91 | |||
6805d18fad | |||
e7cfc394eb | |||
78695ed62e |
11
.flake8
Normal file
11
.flake8
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
[flake8]
|
||||||
|
max-line-length=120
|
||||||
|
ignore =
|
||||||
|
E121,
|
||||||
|
E123,
|
||||||
|
E126,
|
||||||
|
E226,
|
||||||
|
E24,
|
||||||
|
E704,
|
||||||
|
W605
|
||||||
|
exclude = ./tests
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,4 +2,5 @@ build
|
|||||||
dist
|
dist
|
||||||
*.egg-info
|
*.egg-info
|
||||||
__pycache__
|
__pycache__
|
||||||
|
.idea
|
||||||
|
|
||||||
|
31
.pre-commit-config.yaml
Normal file
31
.pre-commit-config.yaml
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
repos:
|
||||||
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
|
rev: v4.5.0
|
||||||
|
hooks:
|
||||||
|
- id: check-yaml
|
||||||
|
- id: check-added-large-files
|
||||||
|
args: ['--maxkb=10240']
|
||||||
|
- id: check-merge-conflict
|
||||||
|
- id: end-of-file-fixer
|
||||||
|
- id: trailing-whitespace
|
||||||
|
|
||||||
|
# isort -- sorts imports
|
||||||
|
- repo: https://github.com/timothycrosley/isort
|
||||||
|
rev: 5.13.2
|
||||||
|
hooks:
|
||||||
|
- id: isort
|
||||||
|
args: ["--profile", "black", "--filter-files"]
|
||||||
|
|
||||||
|
# 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
|
||||||
|
- repo: https://github.com/psf/black-pre-commit-mirror
|
||||||
|
rev: 24.2.0
|
||||||
|
hooks:
|
||||||
|
- id: black
|
||||||
|
language_version: python3.8
|
2
.python-version
Normal file
2
.python-version
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
3.8.20
|
||||||
|
3.8.8
|
22
CHANGELOG.md
Normal file
22
CHANGELOG.md
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## [0.2.2] (2020-07-19)
|
||||||
|
|
||||||
|
Second release is focused on cleanup, documentation.
|
||||||
|
|
||||||
|
* flake8, black, isort - all warnings and errors fixed.
|
||||||
|
* Documentation improved.
|
||||||
|
* Build and deploy process documented for developers.
|
||||||
|
* Poetry removed, replaced with modern pyproject.toml and setuptools.build_meta
|
||||||
|
for automatic version management.
|
||||||
|
|
||||||
|
## [0.1.0] (2020-07-19)
|
||||||
|
|
||||||
|
First release to PyPi.
|
||||||
|
|
||||||
|
* Basic functionality. Probably not the best code.
|
||||||
|
* Sub-menus supported, multi-level through CLI.
|
||||||
|
* Config through ConfigObj ini. Define spec file and input config.ini
|
||||||
|
* Colored Logging
|
||||||
|
* Services can be defined and provided to classes
|
||||||
|
|
35
LICENSE.txt
35
LICENSE.txt
@ -1,25 +1,20 @@
|
|||||||
App Skellington
|
App Skellington
|
||||||
|
|
||||||
Copyright (c) 2020 Mathew Guest
|
MIT No Attribution
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person
|
Copyright (c) 2024 Mathew Guest
|
||||||
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
|
Permission is hereby granted, free of charge, to any person obtaining a
|
||||||
included in all copies or substantial portions of the Software.
|
copy of this software and associated documentation files (the
|
||||||
|
"Software"), to deal in the Software without restriction, including
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
without limitation the rights to use, copy, modify, merge, publish,
|
||||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
distribute, sublicense, and/or sell copies of the Software, and to permit
|
||||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
persons to whom the Software is furnished to do so.
|
||||||
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.
|
||||||
|
156
README.md
156
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,18 +80,101 @@ 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.
|
|
||||||
|
|
||||||
License
|
Tests are a WIP and not fully built out yet. Recommendation is to run 'pytest' in the 'tests' directory.
|
||||||
-------
|
|
||||||
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.
|
|
||||||
|
|
||||||
Notes
|
# Development
|
||||||
-----
|
|
||||||
See official website: https://zavage-software.com
|
|
||||||
Please report bugs, improvements, or feedback!
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
Clone the repo:
|
||||||
|
```commandline
|
||||||
|
git clone https://git-repos.zavage.net/zavage-software/app_skellington.git
|
||||||
|
```
|
||||||
|
|
||||||
|
Install pre-commit hooks:
|
||||||
|
```commandline
|
||||||
|
pre-commit install
|
||||||
|
```
|
||||||
|
|
||||||
|
Build:
|
||||||
|
```
|
||||||
|
python -m build
|
||||||
|
```
|
||||||
|
|
||||||
|
Install:
|
||||||
|
|
||||||
|
```
|
||||||
|
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,8 +1,19 @@
|
|||||||
import logging
|
# flake8: noqa
|
||||||
import sys
|
|
||||||
|
|
||||||
from .app_container import *
|
from .cfg import Config, EnvironmentVariables
|
||||||
from .cfg import *
|
|
||||||
from .cli import *
|
|
||||||
from .log import *
|
|
||||||
|
|
||||||
|
from .app_container import (
|
||||||
|
DEFAULT_APP_AUTHOR,
|
||||||
|
DEFAULT_APP_NAME,
|
||||||
|
ApplicationContainer,
|
||||||
|
ApplicationContext,
|
||||||
|
)
|
||||||
|
from ._util import ServiceNotFound, NoCommandSpecified
|
||||||
|
from .cli import (
|
||||||
|
EXPLICIT_FAIL_ON_UNKNOWN_ARGS,
|
||||||
|
CommandEntry,
|
||||||
|
CommandTree,
|
||||||
|
HelpGenerator,
|
||||||
|
SubMenu,
|
||||||
|
)
|
||||||
|
from .log import LoggingLayer
|
||||||
|
@ -3,24 +3,28 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
# Check and gracefully fail if the user needs to install a 3rd-party dep.
|
# Check and gracefully fail if the user needs to install a 3rd-party dep.
|
||||||
libnames = ['appdirs', 'configobj', 'colorlog']
|
libnames = ["appdirs", "configobj", "colorlog"]
|
||||||
|
|
||||||
|
|
||||||
def check_env_has_dependencies(libnames):
|
def check_env_has_dependencies(libnames):
|
||||||
rc = True
|
rc = True
|
||||||
for libname in libnames:
|
for libname in libnames:
|
||||||
try:
|
try:
|
||||||
__import__(libname)
|
__import__(libname)
|
||||||
except ModuleNotFoundError as ex:
|
except ModuleNotFoundError as ex:
|
||||||
print('Missing third-party library: ', ex, file=sys.stderr)
|
print("Missing third-party library: ", ex, file=sys.stderr)
|
||||||
rc = False
|
rc = False
|
||||||
return rc
|
return rc
|
||||||
|
|
||||||
|
|
||||||
if not check_env_has_dependencies(libnames):
|
if not check_env_has_dependencies(libnames):
|
||||||
print('Unable to load program without installed dependencies', file=sys.stderr)
|
print("Unable to load program without installed dependencies", file=sys.stderr)
|
||||||
raise ImportError('python environment needs third-party dependencies installed')
|
raise ImportError("python environment needs third-party dependencies installed")
|
||||||
|
|
||||||
# Logger for before the application and logging config is loaded
|
# Logger for before the application and logging config is loaded
|
||||||
# - used to log before logging is configured
|
# - used to log before logging is configured
|
||||||
_log_fmt = '%(levelname)-7s:%(message)s'
|
_log_fmt = "%(levelname)-7s:%(message)s"
|
||||||
_logger_name = 'app_skellington'
|
_logger_name = "skell"
|
||||||
_bootstrap_logger = logging.getLogger(_logger_name)
|
_bootstrap_logger = logging.getLogger(_logger_name)
|
||||||
|
|
||||||
# NOTE(MG) Logger monkey-patch:
|
# NOTE(MG) Logger monkey-patch:
|
||||||
@ -29,13 +33,15 @@ _bootstrap_logger = logging.getLogger(_logger_name)
|
|||||||
# configuration is reloaded. This catches APPSKELLINGTON_DEBUG
|
# configuration is reloaded. This catches APPSKELLINGTON_DEBUG
|
||||||
# environment variable the first time, as app_skellington module
|
# environment variable the first time, as app_skellington module
|
||||||
# is imported. See cfg.py
|
# is imported. See cfg.py
|
||||||
if os.environ.get('APPSKELLINGTON_DEBUG', None):
|
if os.environ.get("APPSKELLINGTON_DEBUG", None):
|
||||||
_bootstrap_logger.setLevel(logging.DEBUG) # Don't filter any log levels
|
_bootstrap_logger.setLevel(logging.DEBUG) # Don't filter any log levels
|
||||||
fmt = logging.Formatter(_log_fmt)
|
fmt = logging.Formatter(_log_fmt)
|
||||||
handler = logging.StreamHandler()
|
handler = logging.StreamHandler()
|
||||||
handler.setFormatter(fmt)
|
handler.setFormatter(fmt)
|
||||||
_bootstrap_logger.addHandler(handler)
|
_bootstrap_logger.addHandler(handler)
|
||||||
_bootstrap_logger.debug('log - APPSKELLINGTON_DEBUG set in environment: Enabling verbose logging.')
|
_bootstrap_logger.debug(
|
||||||
|
"log - APPSKELLINGTON_DEBUG set in environment: Enabling verbose logging."
|
||||||
|
)
|
||||||
|
|
||||||
# Logging is by default off, excepting CRITICAL
|
# Logging is by default off, excepting CRITICAL
|
||||||
else:
|
else:
|
||||||
@ -45,4 +51,3 @@ _bootstrap_logger.propagate = False
|
|||||||
# NOTE(MG) Pretty sure the logger has the default handler too at this point.
|
# NOTE(MG) Pretty sure the logger has the default handler too at this point.
|
||||||
# It's been related to some issues with the logger double-printing messages.
|
# It's been related to some issues with the logger double-printing messages.
|
||||||
_bootstrap_logger.addHandler(logging.NullHandler())
|
_bootstrap_logger.addHandler(logging.NullHandler())
|
||||||
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from . import _util
|
|
||||||
|
|
||||||
def eprint(*args, **kwargs):
|
def eprint(*args, **kwargs):
|
||||||
"""
|
"""
|
||||||
@ -11,6 +11,7 @@ def eprint(*args, **kwargs):
|
|||||||
"""
|
"""
|
||||||
print(*args, file=sys.stderr, **kwargs)
|
print(*args, file=sys.stderr, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def filename_to_abspath(filename):
|
def filename_to_abspath(filename):
|
||||||
"""
|
"""
|
||||||
Converts a filename to it's absolute path. If it's already an
|
Converts a filename to it's absolute path. If it's already an
|
||||||
@ -18,6 +19,7 @@ def filename_to_abspath(filename):
|
|||||||
"""
|
"""
|
||||||
return os.path.abspath(filename)
|
return os.path.abspath(filename)
|
||||||
|
|
||||||
|
|
||||||
def does_file_exist(filepath):
|
def does_file_exist(filepath):
|
||||||
"""
|
"""
|
||||||
Because the file can be deleted or created immediately after execution of
|
Because the file can be deleted or created immediately after execution of
|
||||||
@ -26,31 +28,31 @@ 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
|
||||||
|
|
||||||
|
|
||||||
def ensure_dir_exists(dirpath):
|
def ensure_dir_exists(dirpath):
|
||||||
if dirpath is None:
|
if dirpath is None:
|
||||||
return
|
return
|
||||||
if dirpath == '':
|
if dirpath == "":
|
||||||
return
|
return
|
||||||
os.makedirs(dirpath, exist_ok=True)
|
os.makedirs(dirpath, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
def get_root_asset(filepath):
|
def get_root_asset(filepath):
|
||||||
"""
|
"""
|
||||||
Attempts to locate a resource or asset shipped with the application.
|
Attempts to locate a resource or asset shipped with the application.
|
||||||
Searches starting at the root module (__main__) which should be the
|
Searches starting at the root module (__main__) which should be the
|
||||||
python file initially invoked.
|
python file initially invoked.
|
||||||
"""
|
"""
|
||||||
module_root =\
|
module_root = os.path.abspath(os.path.dirname(sys.modules["__main__"].__file__))
|
||||||
os.path.abspath(
|
|
||||||
os.path.dirname(
|
|
||||||
sys.modules['__main__'].__file__))
|
|
||||||
path = os.path.join(module_root, filepath)
|
path = os.path.join(module_root, filepath)
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
||||||
def get_asset(module, filepath):
|
def get_asset(module, filepath):
|
||||||
"""
|
"""
|
||||||
Attempts to locate a resource or asset shipped with the application.
|
Attempts to locate a resource or asset shipped with the application.
|
||||||
@ -75,7 +77,7 @@ def get_asset(module, filepath):
|
|||||||
elif isinstance(module, module):
|
elif isinstance(module, module):
|
||||||
module_file = module.__file__
|
module_file = module.__file__
|
||||||
else:
|
else:
|
||||||
raise Exception('Invalid Usage')
|
raise Exception("Invalid Usage")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
root = module_file
|
root = module_file
|
||||||
@ -84,12 +86,13 @@ 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)
|
||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
||||||
def register_class_as_commands(app, submenu, cls_object):
|
def register_class_as_commands(app, submenu, cls_object):
|
||||||
"""
|
"""
|
||||||
Registers commands for each class method. e.g.: pass in the CLI
|
Registers commands for each class method. e.g.: pass in the CLI
|
||||||
@ -105,7 +108,7 @@ def register_class_as_commands(app, submenu, cls_object):
|
|||||||
for m in members:
|
for m in members:
|
||||||
name = m[0]
|
name = m[0]
|
||||||
ref = m[1]
|
ref = m[1]
|
||||||
if inspect.isfunction(ref) and not name.startswith('_'):
|
if inspect.isfunction(ref) and not name.startswith("_"):
|
||||||
cls_method = ref
|
cls_method = ref
|
||||||
constructor = app._inject_service_dependencies(cls_constructor)
|
constructor = app._inject_service_dependencies(cls_constructor)
|
||||||
sig = inspect.signature(cls_method)
|
sig = inspect.signature(cls_method)
|
||||||
@ -114,9 +117,22 @@ def register_class_as_commands(app, submenu, cls_object):
|
|||||||
docstring = inspect.getdoc(cls_method)
|
docstring = inspect.getdoc(cls_method)
|
||||||
submenu.register_command(func, name, sig, docstring)
|
submenu.register_command(func, name, sig, docstring)
|
||||||
|
|
||||||
|
|
||||||
def create_func(constructor, cls_method):
|
def create_func(constructor, cls_method):
|
||||||
def func(*args, **kwargs):
|
def func(*args, **kwargs):
|
||||||
cmd_class_instance = constructor()
|
cmd_class_instance = constructor()
|
||||||
return cls_method(cmd_class_instance, *args, **kwargs)
|
return cls_method(cmd_class_instance, *args, **kwargs)
|
||||||
|
|
||||||
return func
|
return func
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceNotFound(Exception):
|
||||||
|
"""
|
||||||
|
Application framework error: unable to find and inject dependency.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NoCommandSpecified(Exception):
|
||||||
|
pass
|
||||||
|
20
app_skellington/_version.py
Normal file
20
app_skellington/_version.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# file generated by setuptools-scm
|
||||||
|
# don't change, don't track in version control
|
||||||
|
|
||||||
|
__all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
|
||||||
|
|
||||||
|
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.3.0.dev7+gd155176.d20250223"
|
||||||
|
__version_tuple__ = version_tuple = (0, 3, 0, "dev7", "gd155176.d20250223")
|
@ -1,32 +1,35 @@
|
|||||||
import appdirs
|
|
||||||
import collections
|
|
||||||
import functools
|
import functools
|
||||||
import inspect
|
import inspect
|
||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
|
import appdirs
|
||||||
|
|
||||||
|
from ._util import ServiceNotFound, NoCommandSpecified, get_root_asset
|
||||||
|
from . import log
|
||||||
|
from .cfg import Config
|
||||||
|
from .cli import CommandTree
|
||||||
|
|
||||||
# Application scaffolding:
|
# Application scaffolding:
|
||||||
from ._bootstrap import _bootstrap_logger
|
from ._bootstrap import _bootstrap_logger
|
||||||
from . import log
|
|
||||||
from . import _util
|
|
||||||
from . import cli
|
|
||||||
from . import cfg
|
|
||||||
|
|
||||||
# These two variables affect the directory paths for
|
# These two variables affect the directory paths for
|
||||||
# config files and logging.
|
# config files and logging.
|
||||||
DEFAULT_APP_NAME = ''
|
DEFAULT_APP_NAME = ""
|
||||||
DEFAULT_APP_AUTHOR = ''
|
DEFAULT_APP_AUTHOR = ""
|
||||||
|
|
||||||
|
|
||||||
class ApplicationContext:
|
class ApplicationContext:
|
||||||
"""
|
"""
|
||||||
Container for application-wide state; i.e. app configuration and loggers.
|
Container for application-wide state; i.e. app configuration and loggers.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, config, log):
|
def __init__(self, config, log):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.log = log
|
self.log = log
|
||||||
self.parsed_argv = None
|
self.parsed_argv = None
|
||||||
self.parsed_argv_unknown = None
|
self.parsed_argv_unknown = None
|
||||||
|
|
||||||
|
|
||||||
class ApplicationContainer:
|
class ApplicationContainer:
|
||||||
"""
|
"""
|
||||||
Generalized application functionality. Used for linking components and modules of the application
|
Generalized application functionality. Used for linking components and modules of the application
|
||||||
@ -38,35 +41,43 @@ class ApplicationContainer:
|
|||||||
Override appname and appauthor arguments to direct config and log
|
Override appname and appauthor arguments to direct config and log
|
||||||
directories.
|
directories.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self, configspec_filepath=None, configini_filepath=None, *args, **kwargs
|
||||||
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
|
||||||
|
|
||||||
# Instantiate application context which contains
|
# Instantiate application context which contains
|
||||||
# global state, configuration, loggers, and runtime args.
|
# global state, configuration, loggers, and runtime args.
|
||||||
self._dependencies = {}
|
self._dependencies = {}
|
||||||
|
|
||||||
config = cfg.Config(configspec_filepath, configini_filepath)
|
config = Config(configspec_filepath, configini_filepath)
|
||||||
|
|
||||||
logger = log.LoggingLayer(self.appname, self.appauthor)
|
logger = log.LoggingLayer(self.appname, self.appauthor)
|
||||||
|
# Try and load logging configuration if provided
|
||||||
|
log_config = config.get("logging")
|
||||||
|
if log_config is not None:
|
||||||
|
logger.configure_logging(log_config)
|
||||||
|
else:
|
||||||
logger.configure_logging()
|
logger.configure_logging()
|
||||||
|
|
||||||
self.ctx = ApplicationContext(config, logger)
|
self.ctx = ApplicationContext(config, logger)
|
||||||
self['ctx'] = lambda: self.ctx
|
|
||||||
|
|
||||||
self.cli = cli.CommandTree() # Command-line interface
|
# Reference to root_app avail. in context
|
||||||
|
self.ctx.root_app = self
|
||||||
|
|
||||||
|
# Reference to context service avail. in root_app
|
||||||
|
self["ctx"] = lambda: self.ctx
|
||||||
|
|
||||||
|
self.cli = CommandTree() # Command-line interface
|
||||||
|
|
||||||
# Run methods if subclass implemented them:
|
# Run methods if subclass implemented them:
|
||||||
if callable(getattr(self, '_cli_options', None)):
|
if callable(getattr(self, "_cli_options", None)):
|
||||||
self._cli_options()
|
self._cli_options()
|
||||||
if callable(getattr(self, '_services', None)):
|
if callable(getattr(self, "_services", None)):
|
||||||
self._services()
|
self._services()
|
||||||
if callable(getattr(self, '_command_menu', None)):
|
if callable(getattr(self, "_command_menu", None)):
|
||||||
self._command_menu()
|
self._command_menu()
|
||||||
|
|
||||||
def __delitem__(self, service_name):
|
def __delitem__(self, service_name):
|
||||||
@ -75,7 +86,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):
|
||||||
@ -86,10 +97,12 @@ 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
|
||||||
|
|
||||||
@ -118,9 +131,7 @@ class ApplicationContainer:
|
|||||||
dependencies.append(self[dep_name])
|
dependencies.append(self[dep_name])
|
||||||
return model_constructor(*dependencies)
|
return model_constructor(*dependencies)
|
||||||
|
|
||||||
def _get_config_filepath(
|
def _get_config_filepath(self, app_name, app_author, config_filename="config.ini"):
|
||||||
self, app_name, app_author, config_filename='config.ini'
|
|
||||||
):
|
|
||||||
"""
|
"""
|
||||||
Attempt to find config.ini in the user's config directory.
|
Attempt to find config.ini in the user's config directory.
|
||||||
|
|
||||||
@ -129,14 +140,14 @@ class ApplicationContainer:
|
|||||||
"""
|
"""
|
||||||
dirname = appdirs.user_config_dir(app_name, app_author)
|
dirname = appdirs.user_config_dir(app_name, app_author)
|
||||||
filepath = os.path.join(dirname, config_filename)
|
filepath = os.path.join(dirname, config_filename)
|
||||||
_bootstrap_logger.info('default config filepath calculated to be: %s', filepath)
|
_bootstrap_logger.info("default config filepath calculated to be: %s", filepath)
|
||||||
return filepath
|
return filepath
|
||||||
|
|
||||||
def _get_configspec_filepath(self, configspec_filename='config.spec'):
|
def _get_configspec_filepath(self, configspec_filename="config.spec"):
|
||||||
"""
|
"""
|
||||||
Attempt to find config.spec inside the installed package directory.
|
Attempt to find config.spec inside the installed package directory.
|
||||||
"""
|
"""
|
||||||
return _util.get_root_asset(configspec_filename)
|
return get_root_asset(configspec_filename)
|
||||||
|
|
||||||
def _inject_service_dependencies(self, constructor):
|
def _inject_service_dependencies(self, constructor):
|
||||||
"""
|
"""
|
||||||
@ -148,7 +159,9 @@ class ApplicationContainer:
|
|||||||
"""
|
"""
|
||||||
sig = inspect.signature(constructor.__init__)
|
sig = inspect.signature(constructor.__init__)
|
||||||
params = sig.parameters
|
params = sig.parameters
|
||||||
params = [params[paramname].name for paramname in params] # Convert Param() type => str
|
params = [
|
||||||
|
params[paramname].name for paramname in params
|
||||||
|
] # Convert Param() type => str
|
||||||
cls_dependencies = params[1:] # Skip 'self' parameter on class methods.
|
cls_dependencies = params[1:] # Skip 'self' parameter on class methods.
|
||||||
|
|
||||||
return functools.partial(self._construct_model, constructor, *cls_dependencies)
|
return functools.partial(self._construct_model, constructor, *cls_dependencies)
|
||||||
@ -167,11 +180,8 @@ 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()
|
||||||
@ -180,12 +190,4 @@ class ApplicationContainer:
|
|||||||
pass
|
pass
|
||||||
# Applications need a default usage
|
# Applications need a default usage
|
||||||
|
|
||||||
class ServiceNotFound(Exception):
|
|
||||||
"""
|
|
||||||
Application framework error: unable to find and inject dependency.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
class NoCommandSpecified(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
@ -4,31 +4,30 @@
|
|||||||
# 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.
|
||||||
|
|
||||||
from . import _util
|
|
||||||
|
import configobj
|
||||||
|
import validate
|
||||||
|
|
||||||
from ._bootstrap import _bootstrap_logger
|
from ._bootstrap import _bootstrap_logger
|
||||||
|
|
||||||
import appdirs
|
|
||||||
import argparse
|
|
||||||
import configobj
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import validate
|
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
"""
|
"""
|
||||||
Structure to store application runtime configuration. Also contains
|
Structure to store application runtime configuration. Also contains
|
||||||
functionality to load configuration from local site file.
|
functionality to load configuration from local site file.
|
||||||
|
|
||||||
|
Provide config.spec - specification file which defines allowed parameters and types.
|
||||||
|
|
||||||
|
Provide config.ini - configuration instance which contains values for any
|
||||||
|
configuration arguments.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
DEFAULT_CAPABILITIES = {
|
DEFAULT_CAPABILITIES = {
|
||||||
'allow_options_beyond_spec': True,
|
"allow_options_beyond_spec": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self, configspec_filepath=None, configini_filepath=None, capabilities=None
|
||||||
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
|
||||||
@ -78,9 +77,7 @@ class Config:
|
|||||||
@configspec_filepath.setter
|
@configspec_filepath.setter
|
||||||
def configspec_filepath(self, filepath):
|
def configspec_filepath(self, filepath):
|
||||||
if filepath is None:
|
if filepath is None:
|
||||||
_bootstrap_logger.debug(
|
_bootstrap_logger.debug("cfg - Clearing configspec")
|
||||||
'cfg - Clearing configspec'
|
|
||||||
)
|
|
||||||
self._configspec_filepath = None
|
self._configspec_filepath = None
|
||||||
self._configspec_data = None
|
self._configspec_data = None
|
||||||
self._has_changed_internally = True
|
self._has_changed_internally = True
|
||||||
@ -94,17 +91,15 @@ class Config:
|
|||||||
self._configspec_data = data
|
self._configspec_data = data
|
||||||
self._has_changed_internally = True
|
self._has_changed_internally = True
|
||||||
_bootstrap_logger.debug(
|
_bootstrap_logger.debug(
|
||||||
'cfg - Set configspec and read contents: %s',
|
"cfg - Set configspec and read contents: %s", filepath
|
||||||
filepath
|
|
||||||
)
|
)
|
||||||
self.load_config()
|
self.load_config()
|
||||||
return
|
return
|
||||||
except OSError as ex:
|
except OSError:
|
||||||
_bootstrap_logger.critical(
|
_bootstrap_logger.critical(
|
||||||
'cfg - Failed to find config.spec: file not found (%s)',
|
"cfg - Failed to find config.spec: file not found (%s)", filepath
|
||||||
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()
|
||||||
|
|
||||||
@ -112,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):
|
||||||
@ -122,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):
|
||||||
@ -136,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):
|
||||||
@ -146,9 +141,20 @@ class Config:
|
|||||||
"""
|
"""
|
||||||
self._config_obj[key] = value
|
self._config_obj[key] = value
|
||||||
|
|
||||||
def load_config(
|
def get(self, key, default=None):
|
||||||
self, configspec_filepath=None, configini_filepath=None
|
"""
|
||||||
):
|
Attempt to retrieve configuration item, otherwise return default
|
||||||
|
provided value.
|
||||||
|
|
||||||
|
Similar to Dictionary.get()
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
v = self.__getitem__(key)
|
||||||
|
return v
|
||||||
|
except KeyError:
|
||||||
|
return default
|
||||||
|
|
||||||
|
def load_config(self, configspec_filepath=None, configini_filepath=None):
|
||||||
# Set new arguments if were passed in:
|
# Set new arguments if were passed in:
|
||||||
if configspec_filepath is not None:
|
if configspec_filepath is not None:
|
||||||
self.configspec_filepath = configspec_filepath
|
self.configspec_filepath = configspec_filepath
|
||||||
@ -161,7 +167,7 @@ class Config:
|
|||||||
rc = self._validate_config_against_spec()
|
rc = self._validate_config_against_spec()
|
||||||
if not rc:
|
if not rc:
|
||||||
if self._capability_enforce_strict_spec_validation:
|
if self._capability_enforce_strict_spec_validation:
|
||||||
raise RuntimeError('Failed to validate config.ini against spec.')
|
raise RuntimeError("Failed to validate config.ini against spec.")
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -178,83 +184,103 @@ class Config:
|
|||||||
# options
|
# options
|
||||||
configspec=config_spec,
|
configspec=config_spec,
|
||||||
# encoding
|
# encoding
|
||||||
interpolation='template'
|
interpolation="template",
|
||||||
# raise_errors
|
# raise_errors
|
||||||
)
|
)
|
||||||
_bootstrap_logger.debug(
|
_bootstrap_logger.debug(
|
||||||
'cfg - Parsed configuration. config.spec = %s, config.ini = %s',
|
"cfg - Parsed configuration. config.spec = %s, config.ini = %s",
|
||||||
config_spec, config_ini
|
config_spec,
|
||||||
|
config_ini,
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except configobj.ParseError as ex:
|
except configobj.ParseError:
|
||||||
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
|
||||||
config_ini = self.configini_filepath
|
config_ini = self.configini_filepath
|
||||||
|
|
||||||
# Hack the configobj module to alter the interpolation for validate.py:
|
# Hack the configobj module to alter the interpolation for validate.py:
|
||||||
configobj.DEFAULT_INTERPOLATION = 'template'
|
configobj.DEFAULT_INTERPOLATION = "template"
|
||||||
|
|
||||||
# Validate config.ini against config.spec
|
# Validate config.ini against config.spec
|
||||||
try:
|
try:
|
||||||
_bootstrap_logger.info('cfg - Validating config file against spec')
|
_bootstrap_logger.info("cfg - Validating config file against spec")
|
||||||
val = validate.Validator()
|
val = validate.Validator()
|
||||||
assert isinstance(self._config_obj, configobj.ConfigObj), 'expecting configobj.ConfigObj, received %s' % type(self._config_obj)
|
assert isinstance(
|
||||||
|
self._config_obj, configobj.ConfigObj
|
||||||
|
), "expecting configobj.ConfigObj, received %s" % type(self._config_obj)
|
||||||
# NOTE(MG) copy arg below instructs configobj to use defaults from spec file
|
# NOTE(MG) copy arg below instructs configobj to use defaults from spec file
|
||||||
test_results = self._config_obj.validate(
|
test_results = self._config_obj.validate(
|
||||||
val, copy=True, preserve_errors=True
|
val, copy=True, preserve_errors=True
|
||||||
)
|
)
|
||||||
if test_results is True:
|
if test_results is True:
|
||||||
_bootstrap_logger.info(
|
_bootstrap_logger.info(
|
||||||
'cfg- Successfully validated configuration against spec. input = %s, validation spec = %s',
|
"cfg- Successfully validated configuration against spec. input = %s, validation spec = %s",
|
||||||
config_ini, config_spec
|
config_ini,
|
||||||
|
config_spec,
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
elif test_results is False:
|
elif test_results is False:
|
||||||
_bootstrap_logger.debug(
|
_bootstrap_logger.debug(
|
||||||
'cfg - Potentially discovered invalid config.spec'
|
"cfg - Potentially discovered invalid config.spec"
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self._validate_parse_errors(test_results)
|
self._validate_parse_errors(test_results)
|
||||||
return False
|
return False
|
||||||
except ValueError as ex:
|
except ValueError:
|
||||||
_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('cfg - Config failed validation: [%s].%s appears invalid. msg = %s', '.'.join(section_list), key, rslt)
|
_bootstrap_logger.critical(
|
||||||
|
"cfg - Config failed validation: [%s].%s appears invalid. msg = %s",
|
||||||
|
".".join(section_list),
|
||||||
|
key,
|
||||||
|
rslt,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
_bootstrap_logger.critical("cfg - Config failed validation: missing section, name = '%s'. msg = %s", '.'.join(section_list), rslt)
|
_bootstrap_logger.critical(
|
||||||
|
"cfg - Config failed validation: missing section, name = '%s'. msg = %s",
|
||||||
|
".".join(section_list),
|
||||||
|
rslt,
|
||||||
|
)
|
||||||
|
|
||||||
def print_config(self):
|
def print_config(self):
|
||||||
"""
|
"""
|
||||||
Print configuration to stdout.
|
Print configuration to stdout.
|
||||||
"""
|
"""
|
||||||
print('config:')
|
print("config:")
|
||||||
|
|
||||||
self._config_obj.walk(print)
|
self._config_obj.walk(print)
|
||||||
for section in self._config_obj.sections:
|
for section in self._config_obj.sections:
|
||||||
print(section)
|
print(section)
|
||||||
for key in self._config_obj[section]:
|
for key in self._config_obj[section]:
|
||||||
print(' ', self._config_obj[section][key])
|
print(" ", self._config_obj[section][key])
|
||||||
|
|
||||||
|
|
||||||
class EnvironmentVariables:
|
class EnvironmentVariables:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@ -1,18 +1,17 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import inspect
|
import inspect
|
||||||
import logging
|
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import app_skellington
|
from ._util import NoCommandSpecified
|
||||||
from ._bootstrap import _bootstrap_logger
|
from ._bootstrap import _bootstrap_logger
|
||||||
from . import app_container
|
|
||||||
|
|
||||||
# If explicit fail is enabled, any command with at least one unknown
|
# If explicit fail is enabled, any command with at least one unknown
|
||||||
# argument will be rejected entirely. If not enabled, unknown arguments
|
# argument will be rejected entirely. If not enabled, unknown arguments
|
||||||
# will be ignored.
|
# will be ignored.
|
||||||
EXPLICIT_FAIL_ON_UNKNOWN_ARGS = True
|
EXPLICIT_FAIL_ON_UNKNOWN_ARGS = True
|
||||||
|
|
||||||
|
|
||||||
class CommandTree:
|
class CommandTree:
|
||||||
"""
|
"""
|
||||||
Command-line interface to hold a menu of commands. You can register
|
Command-line interface to hold a menu of commands. You can register
|
||||||
@ -42,15 +41,16 @@ 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.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.root_parser = argparse.ArgumentParser()
|
self.root_parser = argparse.ArgumentParser()
|
||||||
self.submenu_param = None # submenu_param is the variable name
|
self.submenu_param = None # submenu_param is the variable name
|
||||||
@ -66,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):
|
||||||
@ -80,41 +82,25 @@ 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 below strategizes whether to pass in 'required'
|
func_args = {"dest": param_name, "metavar": param_name, "required": is_required}
|
||||||
# paremter to ArgumentParser.add_subparsers()
|
|
||||||
# which was added in in Python3.7.
|
|
||||||
# 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 <= 6
|
|
||||||
):
|
|
||||||
if is_required:
|
|
||||||
_bootstrap_logger.warn('Unable to enforce required submenu: Requires >= Python 3.7')
|
|
||||||
del func_args['required']
|
|
||||||
|
|
||||||
# Creates an argument as a slot in the underlying argparse.
|
# Creates an argument as a slot in the underlying argparse.
|
||||||
subparsers = self.root_parser.add_subparsers(
|
subparsers = self.root_parser.add_subparsers(**func_args)
|
||||||
**func_args
|
|
||||||
)
|
|
||||||
|
|
||||||
submenu = SubMenu(self, subparsers, param_name)
|
submenu = SubMenu(self, subparsers, param_name)
|
||||||
submenu.submenu_path = ''
|
submenu.submenu_path = ""
|
||||||
submenu.var_name = param_name
|
submenu.var_name = param_name
|
||||||
|
|
||||||
_bootstrap_logger.info('Initialized root-level submenu: Parameter = \'%s\'', param_name)
|
_bootstrap_logger.info(
|
||||||
|
"Initialized root-level submenu: Parameter = '%s'", param_name
|
||||||
|
)
|
||||||
self.entries[param_name] = submenu
|
self.entries[param_name] = submenu
|
||||||
self.submenu_param = param_name
|
self.submenu_param = param_name
|
||||||
|
|
||||||
return submenu
|
return submenu
|
||||||
|
|
||||||
def register_command(
|
def register_command(
|
||||||
self, func, cmd_name=None, func_signature=None,
|
self, func, cmd_name=None, func_signature=None, docstring=None
|
||||||
docstring=None
|
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
When no submenu functionality is desired, this links a single
|
When no submenu functionality is desired, this links a single
|
||||||
@ -128,7 +114,7 @@ class CommandTree:
|
|||||||
pass
|
pass
|
||||||
# print('func is method')
|
# print('func is method')
|
||||||
else:
|
else:
|
||||||
raise Exception('bad value passed in for function')
|
raise Exception("bad value passed in for function")
|
||||||
|
|
||||||
if not cmd_name:
|
if not cmd_name:
|
||||||
# safe try/except
|
# safe try/except
|
||||||
@ -145,35 +131,30 @@ 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":
|
||||||
continue
|
continue
|
||||||
param = params[key]
|
param = params[key]
|
||||||
|
|
||||||
if '=' in str(param):
|
if "=" in str(param):
|
||||||
if param.default is None:
|
if param.default is None:
|
||||||
helptext = 'default provided'
|
helptext = "default provided"
|
||||||
else:
|
else:
|
||||||
helptext = "default = '{}'".format(param.default)
|
helptext = "default = '{}'".format(param.default)
|
||||||
self.root_parser.add_argument(
|
self.root_parser.add_argument(
|
||||||
key,
|
key, help=helptext, nargs="?", default=param.default
|
||||||
help=helptext,
|
|
||||||
nargs='?',
|
|
||||||
default=param.default
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
helptext = 'required'
|
helptext = "required"
|
||||||
self.root_parser.add_argument(
|
self.root_parser.add_argument(key, help=helptext)
|
||||||
key,
|
|
||||||
help=helptext)
|
|
||||||
|
|
||||||
# Build the CommandEntry structure
|
# Build the CommandEntry structure
|
||||||
cmd = CommandEntry()
|
cmd = CommandEntry()
|
||||||
@ -184,8 +165,8 @@ class CommandTree:
|
|||||||
cmd.callback = func
|
cmd.callback = func
|
||||||
|
|
||||||
registered_name = cmd_name
|
registered_name = cmd_name
|
||||||
_bootstrap_logger.info('registered command: %s', registered_name)
|
_bootstrap_logger.info("registered command: %s", registered_name)
|
||||||
# end copy-paste then editted from SubMenu.register_command
|
# end copy-paste then 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
|
||||||
@ -209,33 +190,39 @@ class CommandTree:
|
|||||||
# 'failed to parse arguments: explicitly failing to be safe')
|
# 'failed to parse arguments: explicitly failing to be safe')
|
||||||
# return False, False
|
# return False, False
|
||||||
|
|
||||||
if hasattr(pargs, 'usage'):
|
if hasattr(pargs, "usage"):
|
||||||
pass
|
pass
|
||||||
# print('found usage in app_skellington')
|
# print('found usage in app_skellington')
|
||||||
|
|
||||||
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")
|
||||||
return
|
return
|
||||||
|
|
||||||
if args is False and unk is False:
|
if args is False and unk is False:
|
||||||
_bootstrap_logger.error('cli - Failed parsing args.')
|
_bootstrap_logger.error("cli - Failed parsing args.")
|
||||||
return False
|
return False
|
||||||
_bootstrap_logger.info('cli - Received args from shell: %s', args)
|
_bootstrap_logger.info("cli - Received args from shell: %s", args)
|
||||||
|
|
||||||
args = vars(args)
|
args = vars(args)
|
||||||
|
|
||||||
cmd = self._lookup_command(args)
|
cmd = self._lookup_command(args)
|
||||||
if cmd is None:
|
if cmd is None:
|
||||||
_bootstrap_logger.critical('cli - Failed to find command.')
|
_bootstrap_logger.critical("cli - Failed to find command.")
|
||||||
return False
|
raise NoCommandSpecified
|
||||||
|
|
||||||
return self._invoke_command(cmd, args)
|
return self._invoke_command(cmd, args)
|
||||||
|
|
||||||
@ -246,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 (
|
||||||
assert self._entries is None, 'corrupt data structure in CommandMenu'
|
self._cmd_tree_is_single_command is True
|
||||||
assert isinstance(self._single_command, CommandEntry), 'corrupt data structure in CommandMenu'
|
), "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"
|
||||||
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
|
||||||
@ -263,14 +256,18 @@ class CommandTree:
|
|||||||
|
|
||||||
while True:
|
while True:
|
||||||
if argparse_param not in keys:
|
if argparse_param not in keys:
|
||||||
print('root menu parameter not found in args:', argparse_param)
|
print("root menu parameter not found in args:", argparse_param)
|
||||||
input('<broken>')
|
input("<broken>")
|
||||||
|
|
||||||
val = args.get(argparse_param)
|
val = args.get(argparse_param)
|
||||||
_bootstrap_logger.debug('cli - argparse command is \'{}\' = {}'.format(argparse_param, val))
|
_bootstrap_logger.debug(
|
||||||
|
"cli - argparse command is '{}' = {}".format(argparse_param, val)
|
||||||
|
)
|
||||||
|
|
||||||
lookup = submenu.entries.get(val)
|
lookup = submenu.entries.get(val)
|
||||||
_bootstrap_logger.debug('cli - lookup, entries[{}] = {}'.format(val, lookup))
|
_bootstrap_logger.debug(
|
||||||
|
"cli - lookup, entries[{}] = {}".format(val, lookup)
|
||||||
|
)
|
||||||
|
|
||||||
# pop value
|
# pop value
|
||||||
del args[argparse_param]
|
del args[argparse_param]
|
||||||
@ -283,7 +280,7 @@ class CommandTree:
|
|||||||
# return self._invoke_command(lookup, args)
|
# return self._invoke_command(lookup, args)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise app_container.NoCommandSpecified('No command specified.')
|
raise NoCommandSpecified("No command specified.")
|
||||||
|
|
||||||
def _invoke_command(self, cmd, args):
|
def _invoke_command(self, cmd, args):
|
||||||
command_to_be_invoked = cmd.callback
|
command_to_be_invoked = cmd.callback
|
||||||
@ -296,13 +293,14 @@ class CommandTree:
|
|||||||
if param.name in args:
|
if param.name in args:
|
||||||
func_args.append(args[param.name])
|
func_args.append(args[param.name])
|
||||||
|
|
||||||
_bootstrap_logger.info('cli - function: %s', func)
|
_bootstrap_logger.info("cli - function: %s", func)
|
||||||
_bootstrap_logger.info('cli - function args: %s', func_args)
|
_bootstrap_logger.info("cli - function args: %s", func_args)
|
||||||
return command_to_be_invoked(*func_args)
|
return command_to_be_invoked(*func_args)
|
||||||
|
|
||||||
def _get_subparser(self):
|
def _get_subparser(self):
|
||||||
return self.root_parser._subparsers._actions[1]
|
return self.root_parser._subparsers._actions[1]
|
||||||
|
|
||||||
|
|
||||||
class SubMenu:
|
class SubMenu:
|
||||||
def __init__(self, parent, subparsers_obj, name):
|
def __init__(self, parent, subparsers_obj, name):
|
||||||
self.parent = parent # Reference to root CommandTree
|
self.parent = parent # Reference to root CommandTree
|
||||||
@ -313,8 +311,7 @@ class SubMenu:
|
|||||||
self.entries = {}
|
self.entries = {}
|
||||||
|
|
||||||
def register_command(
|
def register_command(
|
||||||
self, func, cmd_name=None, func_signature=None,
|
self, func, cmd_name=None, func_signature=None, docstring=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
|
||||||
@ -345,7 +342,7 @@ class SubMenu:
|
|||||||
elif inspect.ismethod(func):
|
elif inspect.ismethod(func):
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
raise Exception('bad value passed in for function')
|
raise Exception("bad value passed in for function")
|
||||||
|
|
||||||
if not cmd_name:
|
if not cmd_name:
|
||||||
# TODO(MG) Safer sanitation
|
# TODO(MG) Safer sanitation
|
||||||
@ -374,33 +371,27 @@ class SubMenu:
|
|||||||
# created when the SubMenu/argparse.add_subparsers()
|
# created when the SubMenu/argparse.add_subparsers()
|
||||||
# was created.
|
# was created.
|
||||||
help=help_text,
|
help=help_text,
|
||||||
description=description_text
|
description=description_text,
|
||||||
)
|
)
|
||||||
|
|
||||||
# For each paramter in the function create an argparse argument in
|
# For each paramter in the function create an argparse argument in
|
||||||
# the child ArgumentParser created for this menu entry:
|
# the child ArgumentParser created for this menu entry:
|
||||||
for key in params:
|
for key in params:
|
||||||
if key == 'self':
|
if key == "self":
|
||||||
continue
|
continue
|
||||||
param = params[key]
|
param = params[key]
|
||||||
|
|
||||||
if '=' in str(param):
|
if "=" in str(param):
|
||||||
if param.default is None:
|
if param.default is None:
|
||||||
helptext = 'default provided'
|
helptext = "default provided"
|
||||||
else:
|
else:
|
||||||
helptext = "default = '{}'".format(param.default)
|
helptext = "default = '{}'".format(param.default)
|
||||||
child_node.add_argument(
|
child_node.add_argument(
|
||||||
key,
|
key, help=helptext, nargs="?", default=param.default
|
||||||
help=helptext,
|
|
||||||
nargs='?',
|
|
||||||
default=param.default
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
helptext = 'required'
|
helptext = "required"
|
||||||
child_node.add_argument(
|
child_node.add_argument(key, help=helptext)
|
||||||
key,
|
|
||||||
help=helptext
|
|
||||||
)
|
|
||||||
|
|
||||||
# Build the CommandEntry structure
|
# Build the CommandEntry structure
|
||||||
cmd = CommandEntry()
|
cmd = CommandEntry()
|
||||||
@ -410,15 +401,11 @@ class SubMenu:
|
|||||||
# cmd.func_ref = None
|
# cmd.func_ref = None
|
||||||
cmd.callback = func
|
cmd.callback = func
|
||||||
|
|
||||||
registered_name = '{}.{}'.format(
|
registered_name = "{}.{}".format(self.submenu_path, cmd_name)
|
||||||
self.submenu_path,
|
_bootstrap_logger.info("cli - registered command: %s", registered_name)
|
||||||
cmd_name)
|
|
||||||
_bootstrap_logger.info('cli - registered command: %s', registered_name)
|
|
||||||
self.entries[cmd_name] = cmd
|
self.entries[cmd_name] = cmd
|
||||||
|
|
||||||
def create_submenu(
|
def create_submenu(self, var_name, cmd_entry_name=None, is_required=False):
|
||||||
self, var_name, cmd_entry_name=None, is_required=False
|
|
||||||
):
|
|
||||||
"""
|
"""
|
||||||
Creates a child-submenu.
|
Creates a child-submenu.
|
||||||
|
|
||||||
@ -443,52 +430,39 @@ class SubMenu:
|
|||||||
# Create an entry in self's submenu:
|
# Create an entry in self's submenu:
|
||||||
# type = ArgumentParser
|
# type = ArgumentParser
|
||||||
entry_node = self.subparsers_obj.add_parser(
|
entry_node = self.subparsers_obj.add_parser(
|
||||||
cmd_entry_name,
|
cmd_entry_name, help="sub-submenu help", description="sub-sub description"
|
||||||
help='sub-submenu help',
|
)
|
||||||
description='sub-sub description')
|
|
||||||
|
|
||||||
# NOTE(MG) Fix below strategizes whether to pass in 'required'
|
# NOTE(MG) Fix for Python>=3.7,
|
||||||
# paremter to ArgumentParser.add_subparsers()
|
# argparse.ArgumentParser added 'required' argument.
|
||||||
# which was added in in Python3.7.
|
|
||||||
# Must also be written into CommandTree.init_submenu
|
# Must also be written into CommandTree.init_submenu
|
||||||
func_args = {
|
func_args = {"dest": var_name, "metavar": var_name, "required": is_required}
|
||||||
'dest': var_name,
|
if sys.version_info.major == 3 and sys.version_info.minor < 7:
|
||||||
'metavar': var_name,
|
|
||||||
'required': is_required
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
sys.version_info.major == 3
|
|
||||||
and sys.version_info.minor <= 6
|
|
||||||
):
|
|
||||||
if is_required:
|
if is_required:
|
||||||
_bootstrap_logger.warn('Unable to enforce required submenu: Requires >= Python 3.7')
|
_bootstrap_logger.warn(
|
||||||
del func_args['required']
|
"Unable to enforce required submenu: Requires >= Python 3.7"
|
||||||
|
)
|
||||||
|
del func_args["required"]
|
||||||
|
# END fix for Python<3.7
|
||||||
|
|
||||||
# Turn entry into a submenu of it's own:
|
# Turn entry into a submenu of it's own:
|
||||||
# type = _SubParsersAction
|
# type = _SubParsersAction
|
||||||
subp_node = entry_node.add_subparsers(
|
subp_node = entry_node.add_subparsers(**func_args)
|
||||||
**func_args
|
|
||||||
)
|
|
||||||
|
|
||||||
submenu = SubMenu(
|
submenu = SubMenu(self.parent, subp_node, cmd_entry_name)
|
||||||
self.parent,
|
|
||||||
subp_node,
|
|
||||||
cmd_entry_name
|
|
||||||
)
|
|
||||||
|
|
||||||
submenu.var_name = var_name
|
submenu.var_name = var_name
|
||||||
|
|
||||||
submenu.submenu_path = '{}.{}'.format(self.submenu_path, cmd_entry_name)
|
submenu.submenu_path = "{}.{}".format(self.submenu_path, cmd_entry_name)
|
||||||
submenu_name = submenu.submenu_path
|
submenu_name = submenu.submenu_path
|
||||||
|
|
||||||
_bootstrap_logger.info('cli - registered submenu: %s', submenu_name)
|
_bootstrap_logger.info("cli - registered submenu: %s", submenu_name)
|
||||||
self.entries[cmd_entry_name] = submenu
|
self.entries[cmd_entry_name] = submenu
|
||||||
return submenu
|
return submenu
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return 'SubMenu({})<{}>'.format(
|
return "SubMenu({})<{}>".format(self.name, ",".join(["cmds"]))
|
||||||
self.name,
|
|
||||||
','.join(['cmds'])
|
|
||||||
)
|
|
||||||
|
|
||||||
class CommandEntry:
|
class CommandEntry:
|
||||||
"""
|
"""
|
||||||
@ -504,6 +478,7 @@ class CommandEntry:
|
|||||||
arguments into argparse options (creating the documentation also). Similary,
|
arguments into argparse options (creating the documentation also). Similary,
|
||||||
it can convert from argparse options into a function call.
|
it can convert from argparse options into a function call.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.argparse_node = None
|
self.argparse_node = None
|
||||||
|
|
||||||
@ -515,7 +490,8 @@ class CommandEntry:
|
|||||||
self.callback = None
|
self.callback = None
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return 'CommandEntry<{}>'.format(self.cmd_name)
|
return "CommandEntry<{}>".format(self.cmd_name)
|
||||||
|
|
||||||
|
|
||||||
class HelpGenerator:
|
class HelpGenerator:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@ -527,12 +503,12 @@ 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)
|
||||||
if match:
|
if match:
|
||||||
return match.group(1) + '.'
|
return match.group(1) + "."
|
||||||
return doctext
|
return doctext
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -541,11 +517,10 @@ 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)
|
||||||
if match:
|
if match:
|
||||||
return match.group(1) + '.'
|
return match.group(1) + "."
|
||||||
return doctext
|
return doctext
|
||||||
|
|
||||||
|
@ -1,49 +1,47 @@
|
|||||||
from ._bootstrap import _bootstrap_logger
|
|
||||||
from . import _util
|
|
||||||
|
|
||||||
import appdirs
|
|
||||||
import colorlog
|
|
||||||
import logging
|
import logging
|
||||||
import logging.config
|
import logging.config
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
import appdirs
|
||||||
|
|
||||||
|
from . import _util
|
||||||
|
from ._bootstrap import _bootstrap_logger, _logger_name
|
||||||
|
|
||||||
DEFAULT_LOG_SETTINGS = {
|
DEFAULT_LOG_SETTINGS = {
|
||||||
'formatters': {
|
"formatters": {
|
||||||
'colored': {
|
"colored": {
|
||||||
'class': 'colorlog.ColoredFormatter',
|
"class": "colorlog.ColoredFormatter",
|
||||||
# 'format': '%(log_color)s%(levelname)-8s%(reset)s:%(log_color)s%(name)-5s%(reset)s:%(white)s%(message)s'
|
# 'format': '%(log_color)s%(levelname)-8s%(reset)s:%(log_color)s%(name)-5s%(reset)s:%(white)s%(message)s'
|
||||||
'format': '%(white)s%(name)7s%(reset)s|%(log_color)s%(message)s',
|
"format": "%(white)s%(name)7s%(reset)s|%(log_color)s%(message)s",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"handlers": {
|
||||||
'handlers': {
|
"stderr": {
|
||||||
'stderr': {
|
"class": "logging.StreamHandler",
|
||||||
'class': 'logging.StreamHandler',
|
"level": "debug",
|
||||||
'level': 'debug',
|
"formatter": "colored",
|
||||||
'formatter': 'colored'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
"loggers": {
|
||||||
'loggers': {
|
"root": {
|
||||||
'root': {
|
"handlers": [
|
||||||
'handlers': ['stderr',],
|
"stderr",
|
||||||
'level': 'debug'
|
],
|
||||||
|
"level": "debug",
|
||||||
},
|
},
|
||||||
'app_skellington': {
|
"app_skellington": {
|
||||||
# 'handlers': ['stderr',],
|
# 'handlers': ['stderr',],
|
||||||
'level': 'critical',
|
"level": "critical",
|
||||||
'propagate': 'false'
|
"propagate": "false",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class LoggingLayer:
|
class LoggingLayer:
|
||||||
def __init__(
|
def __init__(self, appname=None, appauthor=None):
|
||||||
self, appname=None, appauthor=None
|
self.appname = appname or ""
|
||||||
):
|
self.appauthor = appauthor or ""
|
||||||
self.appname = appname or ''
|
|
||||||
self.appauthor = appauthor or ''
|
|
||||||
self.loggers = {}
|
self.loggers = {}
|
||||||
|
|
||||||
def __getitem__(self, k):
|
def __getitem__(self, k):
|
||||||
@ -72,18 +70,19 @@ class LoggingLayer:
|
|||||||
noise for typical operation.
|
noise for typical operation.
|
||||||
"""
|
"""
|
||||||
if config_dict is None:
|
if config_dict is None:
|
||||||
_bootstrap_logger.debug('log - No application logging configuration provided. Using default')
|
_bootstrap_logger.debug(
|
||||||
|
"log - No application logging configuration provided. Using default"
|
||||||
|
)
|
||||||
config_dict = DEFAULT_LOG_SETTINGS
|
config_dict = DEFAULT_LOG_SETTINGS
|
||||||
|
|
||||||
self.transform_config(config_dict)
|
self.transform_config(config_dict)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# TODO(MG) Pretty print
|
_bootstrap_logger.debug("log - Log configuration: %s", config_dict)
|
||||||
_bootstrap_logger.debug('log - Log configuration: %s', config_dict)
|
|
||||||
logging.config.dictConfig(config_dict)
|
logging.config.dictConfig(config_dict)
|
||||||
_bootstrap_logger.debug('log - Configured all logging')
|
_bootstrap_logger.debug("log - Configured all logging")
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print('unable to configure logging:', ex, type(ex))
|
print("unable to configure logging:", ex, type(ex))
|
||||||
|
|
||||||
def transform_config(self, config_dict):
|
def transform_config(self, config_dict):
|
||||||
"""
|
"""
|
||||||
@ -91,45 +90,52 @@ class LoggingLayer:
|
|||||||
parameters and the final config dictionary passed into the logging module.
|
parameters and the final config dictionary passed into the logging module.
|
||||||
"""
|
"""
|
||||||
# Version should be hard-coded 1, per Python docs
|
# Version should be hard-coded 1, per Python docs
|
||||||
if 'version' in config_dict:
|
if "version" in config_dict:
|
||||||
if config_dict['version'] != 1:
|
if config_dict["version"] != 1:
|
||||||
_bootstrap_logger.warn("logging['version'] must be '1' per Python docs")
|
_bootstrap_logger.warn("logging['version'] must be '1' per Python docs")
|
||||||
config_dict['version'] = 1
|
config_dict["version"] = 1
|
||||||
|
|
||||||
self._add_own_logconfig(config_dict)
|
self._add_own_logconfig(config_dict)
|
||||||
|
|
||||||
# Replace logger level strings with value integers from module
|
# Replace logger level strings with value integers from module
|
||||||
for handler in config_dict['handlers']:
|
for handler in config_dict["handlers"]:
|
||||||
d = config_dict['handlers'][handler]
|
d = config_dict["handlers"][handler]
|
||||||
self._convert_str_to_loglevel(d, 'level')
|
self._convert_str_to_loglevel(d, "level")
|
||||||
|
|
||||||
# Replace logger level strings with value integers from module
|
# Replace logger level strings with value integers from module
|
||||||
for logger in config_dict['loggers']:
|
for logger in config_dict["loggers"]:
|
||||||
d = config_dict['loggers'][logger]
|
d = config_dict["loggers"][logger]
|
||||||
self._convert_str_to_loglevel(d, 'level')
|
self._convert_str_to_loglevel(d, "level")
|
||||||
|
|
||||||
# Replace 'root' logger with '', logging module convention for root handler
|
# Implementation note:
|
||||||
# Note: '' is disallowed in ConfigObj (hence the reason for this replacement)
|
# app_skellington expects root logger configuration to be under 'root'
|
||||||
|
# instead of '' (python spec) because '' is not a valid name in ConfigObj.
|
||||||
try:
|
try:
|
||||||
config_dict['loggers'][''] = config_dict['loggers']['root']
|
if config_dict["loggers"].get("root") is not None:
|
||||||
del config_dict['loggers']['root']
|
config_dict["loggers"][""] = config_dict["loggers"]["root"]
|
||||||
except Exception as ex:
|
del config_dict["loggers"]["root"]
|
||||||
_bootstrap.logger.warn('internal failure patching root logger')
|
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
|
# Evaluate the full filepath of the file handler
|
||||||
if 'file' not in config_dict['handlers']:
|
if "file" not in config_dict["handlers"]:
|
||||||
return
|
return
|
||||||
|
|
||||||
if os.path.abspath(config_dict['handlers']['file']['filename']) ==\
|
if (
|
||||||
config_dict['handlers']['file']['filename']:
|
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(
|
||||||
config_dict['handlers']['file']['filename'] = log_filepath
|
dirname, config_dict["handlers"]["file"]["filename"]
|
||||||
|
)
|
||||||
|
config_dict["handlers"]["file"]["filename"] = log_filepath
|
||||||
|
|
||||||
def _add_own_logconfig(self, config_dict):
|
def _add_own_logconfig(self, config_dict):
|
||||||
# NOTE(MG) Monkey-patch logger
|
# NOTE(MG) Monkey-patch logger
|
||||||
@ -139,13 +145,14 @@ class LoggingLayer:
|
|||||||
# variable the second time, when it's being reloaded as a
|
# variable the second time, when it's being reloaded as a
|
||||||
# logging configuration is read from config file.
|
# logging configuration is read from config file.
|
||||||
# See _bootstrap.py
|
# See _bootstrap.py
|
||||||
if os.environ.get('APPSKELLINGTON_DEBUG', None):
|
if os.environ.get("APPSKELLINGTON_DEBUG", None):
|
||||||
if 'app_skellington' not in config_dict['loggers']:
|
if _logger_name not in config_dict["loggers"]:
|
||||||
config_dict['loggers']['app_skellington'] = {
|
config_dict["loggers"][_logger_name] = {
|
||||||
'level': 'debug', 'propagate': 'false'
|
"level": "debug",
|
||||||
|
"propagate": "false",
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
config_dict['loggers']['app_skellington']['level'] = 'debug'
|
config_dict["loggers"][_logger_name]["level"] = "debug"
|
||||||
|
|
||||||
def _convert_str_to_loglevel(self, dict_, key):
|
def _convert_str_to_loglevel(self, dict_, key):
|
||||||
"""
|
"""
|
||||||
@ -161,18 +168,17 @@ 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
|
||||||
elif s == 'error':
|
elif s == "error":
|
||||||
dict_[key] = logging.ERROR
|
dict_[key] = logging.ERROR
|
||||||
elif s == 'warning':
|
elif s == "warning":
|
||||||
dict_[key] = logging.WARNING
|
dict_[key] = logging.WARNING
|
||||||
elif s == 'info':
|
elif s == "info":
|
||||||
dict_[key] = logging.INFO
|
dict_[key] = logging.INFO
|
||||||
elif s == 'debug':
|
elif s == "debug":
|
||||||
dict_[key] = logging.DEBUG
|
dict_[key] = logging.DEBUG
|
||||||
elif s == 'all':
|
elif s == "all":
|
||||||
dict_[key] = logging.NOTSET
|
dict_[key] = logging.NOTSET
|
||||||
|
|
||||||
|
45
pyproject.toml
Normal file
45
pyproject.toml
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=45", "setuptools_scm[toml]>=6.2"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[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"
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
[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"
|
75
setup.py
75
setup.py
@ -1,75 +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
|
|
||||||
|
|
||||||
|
|
||||||
from setuptools import setup
|
|
||||||
import os
|
|
||||||
|
|
||||||
__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',
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
@ -1,45 +1,49 @@
|
|||||||
from app_skellington.cfg import Config
|
|
||||||
from app_skellington import _util
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from app_skellington import _util
|
||||||
|
from app_skellington.cfg import Config
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def sample_configspec_filepath():
|
def sample_configspec_filepath():
|
||||||
return _util.get_asset(__name__, 'sample_config.spec')
|
return _util.get_asset(__name__, "sample_config.spec")
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def sample_configini_filepath():
|
def sample_configini_filepath():
|
||||||
return _util.get_asset(__name__, 'sample_config.ini')
|
return _util.get_asset(__name__, "sample_config.ini")
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def sample_full_configspec_filepath():
|
def sample_full_configspec_filepath():
|
||||||
return _util.get_asset(__name__, 'sample_config_full.spec')
|
return _util.get_asset(__name__, "sample_config_full.spec")
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def sample_full_configini_filepath():
|
def sample_full_configini_filepath():
|
||||||
return _util.get_asset(__name__, 'sample_config_full.ini')
|
return _util.get_asset(__name__, "sample_config_full.ini")
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def sample_invalid_configspec_filepath():
|
def sample_invalid_configspec_filepath():
|
||||||
return _util.get_asset(__name__, 'sample_config_invalid.spec')
|
return _util.get_asset(__name__, "sample_config_invalid.spec")
|
||||||
|
|
||||||
|
|
||||||
class TestConfig_e2e:
|
class TestConfig_e2e:
|
||||||
def test_allows_reading_ini_and_no_spec(
|
def test_allows_reading_ini_and_no_spec(self, sample_configini_filepath):
|
||||||
self, sample_configini_filepath
|
cfg = Config(configini_filepath=sample_configini_filepath)
|
||||||
):
|
assert (
|
||||||
cfg = Config(
|
cfg["root_option"] == "root_option_val"
|
||||||
configini_filepath=sample_configini_filepath
|
), "expecting default from config.spec (didnt get)"
|
||||||
)
|
assert (
|
||||||
assert cfg['root_option'] == 'root_option_val', 'expecting default from config.spec (didnt get)'
|
cfg["app"]["sub_option"] == "sub_option_val"
|
||||||
assert cfg['app']['sub_option'] == 'sub_option_val', 'expecting default for sub option'
|
), "expecting default for sub option"
|
||||||
|
|
||||||
def test_allows_reading_spec_and_no_ini(
|
def test_allows_reading_spec_and_no_ini(self, sample_configspec_filepath):
|
||||||
self, sample_configspec_filepath
|
cfg = Config(configspec_filepath=sample_configspec_filepath)
|
||||||
):
|
assert (
|
||||||
cfg = Config(
|
cfg["root_option"] == "def_string"
|
||||||
configspec_filepath=sample_configspec_filepath
|
), "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(
|
||||||
@ -50,48 +54,39 @@ class TestConfig_e2e:
|
|||||||
# configspec_filepath=sample_invalid_configspec_filepath
|
# configspec_filepath=sample_invalid_configspec_filepath
|
||||||
# )
|
# )
|
||||||
|
|
||||||
def test_allows_options_beyond_spec(
|
def test_allows_options_beyond_spec(self, sample_configspec_filepath):
|
||||||
self, sample_configspec_filepath
|
cfg = Config(configspec_filepath=sample_configspec_filepath)
|
||||||
):
|
cfg["foo"] = "test my value"
|
||||||
cfg = Config(
|
assert cfg["foo"] == "test my value"
|
||||||
configspec_filepath=sample_configspec_filepath
|
|
||||||
)
|
|
||||||
cfg['foo'] = 'test my value'
|
|
||||||
assert cfg['foo'] == 'test my value'
|
|
||||||
|
|
||||||
cfg['app']['bar'] = 'another value'
|
cfg["app"]["bar"] = "another value"
|
||||||
assert cfg['app']['bar'] == 'another value'
|
assert cfg["app"]["bar"] == "another value"
|
||||||
|
|
||||||
# def test_can_read_config_file_mutiple_times(self):
|
# def test_can_read_config_file_mutiple_times(self):
|
||||||
# pass
|
# pass
|
||||||
|
|
||||||
def test_can_override_config_file_manually(
|
def test_can_override_config_file_manually(self, sample_configini_filepath):
|
||||||
self, sample_configini_filepath
|
cfg = Config(configini_filepath=sample_configini_filepath)
|
||||||
):
|
cfg["root_option"] = "newval"
|
||||||
cfg = Config(
|
assert cfg["root_option"] == "newval"
|
||||||
configini_filepath=sample_configini_filepath
|
|
||||||
)
|
|
||||||
cfg['root_option'] = 'newval'
|
|
||||||
assert cfg['root_option'] == 'newval'
|
|
||||||
|
|
||||||
cfg['app']['sub_option'] = 'another_new_val'
|
cfg["app"]["sub_option"] = "another_new_val"
|
||||||
assert cfg['app']['sub_option'] == 'another_new_val', 'expecting default for sub option'
|
assert (
|
||||||
|
cfg["app"]["sub_option"] == "another_new_val"
|
||||||
|
), "expecting default for sub option"
|
||||||
|
|
||||||
def test_can_set_option_without_config(self):
|
def test_can_set_option_without_config(self):
|
||||||
cfg = Config()
|
cfg = Config()
|
||||||
cfg['foo'] = 'test my value'
|
cfg["foo"] = "test my value"
|
||||||
assert cfg['foo'] == 'test my value'
|
assert cfg["foo"] == "test my value"
|
||||||
|
|
||||||
cfg['app'] = {}
|
cfg["app"] = {}
|
||||||
cfg['app']['bar'] = 'another value'
|
cfg["app"]["bar"] = "another value"
|
||||||
assert cfg['app']['bar'] == 'another value'
|
assert cfg["app"]["bar"] == "another value"
|
||||||
|
|
||||||
def test_uses_spec_as_defaults(
|
|
||||||
self, sample_configspec_filepath
|
|
||||||
):
|
|
||||||
cfg = Config(
|
|
||||||
configspec_filepath=sample_configspec_filepath
|
|
||||||
)
|
|
||||||
assert cfg['root_option'] == 'def_string', 'expecting default from config.spec (didnt get)'
|
|
||||||
assert cfg['app']['sub_option'] == 'def_sub', 'expecting default for sub option'
|
|
||||||
|
|
||||||
|
def test_uses_spec_as_defaults(self, sample_configspec_filepath):
|
||||||
|
cfg = Config(configspec_filepath=sample_configspec_filepath)
|
||||||
|
assert (
|
||||||
|
cfg["root_option"] == "def_string"
|
||||||
|
), "expecting default from config.spec (didnt get)"
|
||||||
|
assert cfg["app"]["sub_option"] == "def_sub", "expecting default for sub option"
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
from app_skellington.cli import CommandTree
|
from app_skellington.cli import CommandTree
|
||||||
|
|
||||||
|
|
||||||
class TestCli_e2e:
|
class TestCli_e2e:
|
||||||
def test_null_constructor_works(self):
|
def test_null_constructor_works(self):
|
||||||
x = CommandTree()
|
x = CommandTree()
|
||||||
assert True == True
|
assert True == True
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user