Compare commits
No commits in common. "fd5af59c599418cadfa52b7c9f6c5b7737b8cb99" and "ffc39641bec23215b22750e4ac66497df246c5a6" have entirely different histories.
fd5af59c59
...
ffc39641be
39
README.md
39
README.md
@ -1,17 +1,17 @@
|
|||||||
app_skellington
|
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
|
||||||
- INI-style config and and validation (provided through ConfigObj).
|
* INI-style config and and validation (provided through ConfigObj).
|
||||||
- Colored logging (provided through colorlog)
|
* Colored logging (provided through colorlog)
|
||||||
- Works on Linux, Windows, and Mac.
|
* Works on Linux, Windows, and Mac.
|
||||||
|
|
||||||
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.
|
* Compatable 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
|
Application Configuration
|
||||||
-------------------------
|
-------------------------
|
||||||
@ -23,8 +23,8 @@ application which is built on app_skellington. The format is multi-level .ini sy
|
|||||||
Reference the ConfigObj documentation for config.ini and config.spec
|
Reference the ConfigObj documentation for config.ini and config.spec
|
||||||
format. See:
|
format. See:
|
||||||
|
|
||||||
- https://configobj.readthedocs.io/en/latest/configobj.html#the-config-file-format
|
* https://configobj.readthedocs.io/en/latest/configobj.html#the-config-file-format
|
||||||
- https://configobj.readthedocs.io/en/latest/configobj.html#validation
|
* https://configobj.readthedocs.io/en/latest/configobj.html#validation
|
||||||
|
|
||||||
Config files (config.ini) are created if they don't exist. The
|
Config files (config.ini) are created if they don't exist. The
|
||||||
file always contains the full specification of parameters; i.e. even default
|
file always contains the full specification of parameters; i.e. even default
|
||||||
@ -38,9 +38,9 @@ Linux:
|
|||||||
|
|
||||||
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.
|
||||||
@ -50,26 +50,19 @@ 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,
|
||||||
|
|
||||||
APPSKELLINGTON_DEBUG=1 <executable>
|
APP_SKELLINGTON_DEBUG=1 <executable>
|
||||||
|
|
||||||
or
|
or
|
||||||
|
|
||||||
export APPSKELLINGTON_DEBUG=1
|
export APP_SKELLINGTON_DEBUG=1
|
||||||
<executable>
|
<executable>
|
||||||
|
|
||||||
Tests
|
Tests
|
||||||
-----
|
-----
|
||||||
Tests are a WIP. Recommendation is to run 'pytest' in the 'tests' directory.
|
Tests are a WIP. Recommendation is to run 'pytest' in the 'tests' directory.
|
||||||
|
|
||||||
License
|
|
||||||
-------
|
|
||||||
I'm releasing this software under one of the most permissive
|
|
||||||
licenses, the MIT software license. This applies to this source repository
|
|
||||||
and all files within it. Alternatively, you are permitted you
|
|
||||||
to use any of this under the GPL to the fullest legal extent allowed.
|
|
||||||
|
|
||||||
Notes
|
Notes
|
||||||
-----
|
-----
|
||||||
See official website: https://zavage-software.com
|
See official website: https://zavage-software.com
|
||||||
Please report bugs, improvements, or feedback!
|
Please report bugs, improvements, or feedback! <contact>
|
||||||
|
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
APP_CONFIG_FILENAME = 'config.ini' # Relative to user directory on machine
|
||||||
|
APP_CONFIGSPEC_FILENAME = 'config.spec' # Relative to module source directory
|
||||||
|
|
||||||
from .app_container import *
|
from .app_container import *
|
||||||
from .cfg import *
|
from .cfg import *
|
||||||
from .cli import *
|
from .cli import *
|
||||||
|
@ -10,21 +10,21 @@ def check_env_has_dependencies(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-part 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('refusing 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 = 'skell'
|
_logger_name = 'app_skellington'
|
||||||
_bootstrap_logger = logging.getLogger(_logger_name)
|
_bootstrap_logger = logging.getLogger(_logger_name)
|
||||||
|
|
||||||
# NOTE(MG) Logger monkey-patch:
|
|
||||||
# This is done twice: once when app_skellington
|
# NOTE(MG) This is done twice: once when app_skellington
|
||||||
# module is imported via _bootstrap.py and again if logging
|
# module is imported via _bootstrap.py and again if logging
|
||||||
# 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
|
||||||
|
@ -12,12 +12,20 @@ from . import _util
|
|||||||
from . import cli
|
from . import cli
|
||||||
from . import cfg
|
from . import cfg
|
||||||
|
|
||||||
import logging
|
DEFAULT_APP_NAME = 'python-app'
|
||||||
|
DEFAULT_APP_AUTHOR = 'John Doe'
|
||||||
|
|
||||||
# These two variables affect the directory paths for
|
|
||||||
# config files and logging.
|
# OPTIONAL: classes can sub-class from this?
|
||||||
DEFAULT_APP_NAME = ''
|
class Components:
|
||||||
DEFAULT_APP_AUTHOR = ''
|
def inject_dependencies_based_on_names_in_args(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def inject_dependency(self, name):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def register_dependency(self, service, name):
|
||||||
|
pass
|
||||||
|
|
||||||
class ApplicationContext:
|
class ApplicationContext:
|
||||||
"""
|
"""
|
||||||
@ -36,9 +44,6 @@ class ApplicationContainer:
|
|||||||
object instances for services, passes off to the cli to determine what to
|
object instances for services, passes off to the cli to determine what to
|
||||||
do, and then injects any necessary dependencies (e.g. database module)
|
do, and then injects any necessary dependencies (e.g. database module)
|
||||||
and kicks off the functionality requested in the cli.
|
and kicks off the functionality requested in the cli.
|
||||||
|
|
||||||
Override appname and appauthor arguments to direct config and log
|
|
||||||
directories.
|
|
||||||
"""
|
"""
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@ -46,35 +51,27 @@ class ApplicationContainer:
|
|||||||
configini_filepath=None,
|
configini_filepath=None,
|
||||||
*args, **kwargs
|
*args, **kwargs
|
||||||
):
|
):
|
||||||
self.appname = kwargs.get('appname') or DEFAULT_APP_NAME
|
# Instantiate root application context (container for globals)
|
||||||
|
# if configspec_filepath is None:
|
||||||
|
# configspec_filepath = self._get_configspec_filepath()
|
||||||
|
|
||||||
|
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
|
|
||||||
# global state, configuration, loggers, and runtime args.
|
|
||||||
self._dependencies = {}
|
self._dependencies = {}
|
||||||
|
|
||||||
config = cfg.Config(configspec_filepath, configini_filepath)
|
config = cfg.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()
|
|
||||||
|
|
||||||
|
# added here, is this okay to do twice?
|
||||||
|
logger.configure_logging()
|
||||||
|
|
||||||
self.ctx = ApplicationContext(config, logger)
|
self.ctx = ApplicationContext(config, logger)
|
||||||
|
|
||||||
# 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['ctx'] = lambda: self.ctx
|
||||||
|
|
||||||
self.cli = cli.CommandTree() # Command-line interface
|
self.cli = cli.CommandTree() # Command-line interface
|
||||||
|
|
||||||
# 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)):
|
||||||
@ -96,7 +93,7 @@ class ApplicationContainer:
|
|||||||
Returns a factory of a service or dependency. The factory is a function
|
Returns a factory of a service or dependency. The factory is a function
|
||||||
that is called to return an instance of the service object.
|
that is called to return an instance of the service object.
|
||||||
|
|
||||||
app['datalayer'] => returns the made-up "datalayer" service.
|
app_container['netezza'] => returns the netezza service instance
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
service_factory = self._dependencies[service_name] # Retrieve factory function
|
service_factory = self._dependencies[service_name] # Retrieve factory function
|
||||||
@ -104,6 +101,7 @@ class ApplicationContainer:
|
|||||||
except KeyError as ex:
|
except KeyError as ex:
|
||||||
msg = 'failed to inject service: {}'.format(service_name)
|
msg = 'failed to inject service: {}'.format(service_name)
|
||||||
_bootstrap_logger.critical(msg)
|
_bootstrap_logger.critical(msg)
|
||||||
|
_util.eprint(msg)
|
||||||
raise ServiceNotFound
|
raise ServiceNotFound
|
||||||
|
|
||||||
def __setitem__(self, service_name, value):
|
def __setitem__(self, service_name, value):
|
||||||
@ -131,9 +129,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.
|
||||||
|
|
||||||
|
@ -18,11 +18,6 @@ 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 = {
|
||||||
@ -82,9 +77,15 @@ class Config:
|
|||||||
|
|
||||||
@configspec_filepath.setter
|
@configspec_filepath.setter
|
||||||
def configspec_filepath(self, filepath):
|
def configspec_filepath(self, filepath):
|
||||||
|
# Check if exists as file (at least seems to):
|
||||||
|
# if not _util.does_file_exist(filepath):
|
||||||
|
# _bootstrap_logger.error(
|
||||||
|
# 'failed to set config.spec: file not found '
|
||||||
|
# '(%s)', filepath)
|
||||||
|
# raise Exception
|
||||||
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
|
||||||
@ -99,17 +100,17 @@ 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 as ex:
|
||||||
_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()
|
||||||
|
|
||||||
@ -151,19 +152,6 @@ class Config:
|
|||||||
"""
|
"""
|
||||||
self._config_obj[key] = value
|
self._config_obj[key] = value
|
||||||
|
|
||||||
def get(self, key, default=None):
|
|
||||||
"""
|
|
||||||
Attempt to retrieve configuration item, otherwise return default
|
|
||||||
provided value.
|
|
||||||
|
|
||||||
Similar to Dictionary.get()
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
v = self.__getitem__(key)
|
|
||||||
return v
|
|
||||||
except KeyError as ex:
|
|
||||||
return default
|
|
||||||
|
|
||||||
def load_config(
|
def load_config(
|
||||||
self, configspec_filepath=None, configini_filepath=None
|
self, configspec_filepath=None, configini_filepath=None
|
||||||
):
|
):
|
||||||
@ -228,7 +216,7 @@ class Config:
|
|||||||
_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 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
|
||||||
)
|
)
|
||||||
|
@ -80,9 +80,9 @@ class CommandTree:
|
|||||||
Creates a root-level submenu with no entries. SubMenu node is
|
Creates a root-level submenu with no entries. SubMenu node is
|
||||||
returned which can have submenus and commands attached to it.
|
returned which can have submenus and commands attached to it.
|
||||||
"""
|
"""
|
||||||
# NOTE(MG) Fix for Python>=3.7,
|
# NOTE(MG) Fix below strategizes whether to pass in 'required'
|
||||||
# argparse.ArgumentParser added 'required' argument.
|
# paremter to ArgumentParser.add_subparsers()
|
||||||
# Must also be written into SubMenu.create_submenu.
|
# which was added in in Python3.7.
|
||||||
func_args = {
|
func_args = {
|
||||||
'dest': param_name,
|
'dest': param_name,
|
||||||
'metavar': param_name,
|
'metavar': param_name,
|
||||||
@ -90,12 +90,11 @@ class CommandTree:
|
|||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
sys.version_info.major == 3
|
sys.version_info.major == 3
|
||||||
and sys.version_info.minor < 7
|
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('Unable to enforce required submenu: Requires >= Python 3.7')
|
||||||
del func_args['required']
|
del func_args['required']
|
||||||
# END fix for Python<3.7
|
|
||||||
|
|
||||||
# Creates an argument as a slot in the underlying argparse.
|
# Creates an argument as a slot in the underlying argparse.
|
||||||
subparsers = self.root_parser.add_subparsers(
|
subparsers = self.root_parser.add_subparsers(
|
||||||
@ -175,6 +174,13 @@ class CommandTree:
|
|||||||
key,
|
key,
|
||||||
help=helptext)
|
help=helptext)
|
||||||
|
|
||||||
|
# # Wrapper function that instantiates an object and runs a method
|
||||||
|
# # on-demand. The object is created, injected with necessary
|
||||||
|
# # dependencies or services, and the method is invoked.
|
||||||
|
# def func(*args, **kwargs):
|
||||||
|
# obj = constructor()
|
||||||
|
# return cls_method(obj, *args, **kwargs)
|
||||||
|
|
||||||
# Build the CommandEntry structure
|
# Build the CommandEntry structure
|
||||||
cmd = CommandEntry()
|
cmd = CommandEntry()
|
||||||
cmd.argparse_node = self.root_parser
|
cmd.argparse_node = self.root_parser
|
||||||
@ -191,6 +197,11 @@ class CommandTree:
|
|||||||
self._single_command = cmd
|
self._single_command = cmd
|
||||||
self._entries = None
|
self._entries = None
|
||||||
|
|
||||||
|
# def _validate(self):
|
||||||
|
# pass
|
||||||
|
# # TODO(MG):
|
||||||
|
# # subparser can not be empty, needs to have parsers attached
|
||||||
|
|
||||||
def parse(self, args=None):
|
def parse(self, args=None):
|
||||||
if args is None:
|
if args is None:
|
||||||
args = sys.argv[1:]
|
args = sys.argv[1:]
|
||||||
@ -226,15 +237,16 @@ class CommandTree:
|
|||||||
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('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.')
|
print('cmd is None')
|
||||||
|
_bootstrap_logger.error('cli - failed to find command')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return self._invoke_command(cmd, args)
|
return self._invoke_command(cmd, args)
|
||||||
@ -271,6 +283,7 @@ class CommandTree:
|
|||||||
|
|
||||||
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))
|
||||||
|
# print(submenu.entries)
|
||||||
|
|
||||||
# pop value
|
# pop value
|
||||||
del args[argparse_param]
|
del args[argparse_param]
|
||||||
@ -341,14 +354,16 @@ class SubMenu:
|
|||||||
execute the command function.
|
execute the command function.
|
||||||
"""
|
"""
|
||||||
if inspect.isfunction(func):
|
if inspect.isfunction(func):
|
||||||
|
# print('func is function')
|
||||||
pass
|
pass
|
||||||
elif inspect.ismethod(func):
|
elif inspect.ismethod(func):
|
||||||
pass
|
pass
|
||||||
|
# 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:
|
||||||
# TODO(MG) Safer sanitation
|
# safe try/except
|
||||||
cmd_name = func.__name__
|
cmd_name = func.__name__
|
||||||
|
|
||||||
if func_signature is None:
|
if func_signature is None:
|
||||||
@ -371,7 +386,7 @@ class SubMenu:
|
|||||||
child_node = self.subparsers_obj.add_parser(
|
child_node = self.subparsers_obj.add_parser(
|
||||||
cmd_name, # Note: cmd_name here will be the VALUE
|
cmd_name, # Note: cmd_name here will be the VALUE
|
||||||
# passed into the argparse arg VARIABLE NAME
|
# passed into the argparse arg VARIABLE NAME
|
||||||
# created when the SubMenu/argparse.add_subparsers()
|
# created when the SubMenu/argparse.addZ_subparsers()
|
||||||
# was created.
|
# was created.
|
||||||
help=help_text,
|
help=help_text,
|
||||||
description=description_text
|
description=description_text
|
||||||
@ -402,6 +417,13 @@ class SubMenu:
|
|||||||
help=helptext
|
help=helptext
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# # Wrapper function that instantiates an object and runs a method
|
||||||
|
# # on-demand. The object is created, injected with necessary
|
||||||
|
# # dependencies or services, and the method is invoked.
|
||||||
|
# def func(*args, **kwargs):
|
||||||
|
# obj = constructor()
|
||||||
|
# return cls_method(obj, *args, **kwargs)
|
||||||
|
|
||||||
# Build the CommandEntry structure
|
# Build the CommandEntry structure
|
||||||
cmd = CommandEntry()
|
cmd = CommandEntry()
|
||||||
cmd.argparse_node = child_node
|
cmd.argparse_node = child_node
|
||||||
@ -447,29 +469,12 @@ class SubMenu:
|
|||||||
help='sub-submenu help',
|
help='sub-submenu help',
|
||||||
description='sub-sub description')
|
description='sub-sub description')
|
||||||
|
|
||||||
# NOTE(MG) Fix for Python>=3.7,
|
|
||||||
# argparse.ArgumentParser added 'required' argument.
|
|
||||||
# Must also be written into CommandTree.init_submenu
|
|
||||||
func_args = {
|
|
||||||
'dest': var_name,
|
|
||||||
'metavar': var_name,
|
|
||||||
'required': is_required
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
sys.version_info.major == 3
|
|
||||||
and sys.version_info.minor < 7
|
|
||||||
):
|
|
||||||
if is_required:
|
|
||||||
_bootstrap_logger.warn('Unable to enforce required submenu: Requires >= Python 3.7')
|
|
||||||
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
|
dest = var_name,
|
||||||
)
|
metavar = var_name,
|
||||||
|
required = is_required)
|
||||||
|
|
||||||
submenu = SubMenu(
|
submenu = SubMenu(
|
||||||
self.parent,
|
self.parent,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from ._bootstrap import _bootstrap_logger, _logger_name
|
from ._bootstrap import _bootstrap_logger
|
||||||
from . import _util
|
from . import _util
|
||||||
|
|
||||||
import appdirs
|
import appdirs
|
||||||
@ -22,6 +22,7 @@ DEFAULT_LOG_SETTINGS = {
|
|||||||
'level': 'debug',
|
'level': 'debug',
|
||||||
'formatter': 'colored'
|
'formatter': 'colored'
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
'loggers': {
|
'loggers': {
|
||||||
@ -38,11 +39,9 @@ DEFAULT_LOG_SETTINGS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class LoggingLayer:
|
class LoggingLayer:
|
||||||
def __init__(
|
def __init__(self, appname, appauthor, config=None):
|
||||||
self, appname=None, appauthor=None
|
self.appname = appname
|
||||||
):
|
self.appauthor = appauthor
|
||||||
self.appname = appname or ''
|
|
||||||
self.appauthor = appauthor or ''
|
|
||||||
self.loggers = {}
|
self.loggers = {}
|
||||||
|
|
||||||
def __getitem__(self, k):
|
def __getitem__(self, k):
|
||||||
@ -77,6 +76,7 @@ class LoggingLayer:
|
|||||||
self.transform_config(config_dict)
|
self.transform_config(config_dict)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# TODO(MG) switch to pretty-print, as it'd be more human readable
|
||||||
_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')
|
||||||
@ -106,16 +106,13 @@ class LoggingLayer:
|
|||||||
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:
|
||||||
if config_dict['loggers'].get('root') is not None:
|
config_dict['loggers'][''] = config_dict['loggers']['root']
|
||||||
config_dict['loggers'][''] = config_dict['loggers']['root']
|
del config_dict['loggers']['root']
|
||||||
del config_dict['loggers']['root']
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
_bootstrap_logger.warn('was not able to find and patch root logger configuration from arguments')
|
_bootstrap.logger.warn('internal failure patching root logger')
|
||||||
|
|
||||||
|
|
||||||
# Evaluate the full filepath of the file handler
|
# Evaluate the full filepath of the file handler
|
||||||
@ -133,20 +130,19 @@ class LoggingLayer:
|
|||||||
config_dict['handlers']['file']['filename'] = log_filepath
|
config_dict['handlers']['file']['filename'] = log_filepath
|
||||||
|
|
||||||
def _add_own_logconfig(self, config_dict):
|
def _add_own_logconfig(self, config_dict):
|
||||||
# NOTE(MG) Monkey-patch logger
|
# NOTE(MG) This is done twice: once when app_skellington
|
||||||
# This is done twice: once when app_skellington
|
|
||||||
# module is imported again if logging configuration is
|
# module is imported again if logging configuration is
|
||||||
# reloaded. This catches APPSKELLINGTON_DEBUG environment
|
# reloaded. This catches APPSKELLINGTON_DEBUG environment
|
||||||
# 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 _logger_name not in config_dict['loggers']:
|
if 'app_skellington' not in config_dict['loggers']:
|
||||||
config_dict['loggers'][_logger_name] = {
|
config_dict['loggers']['app_skellington'] = {
|
||||||
'level': 'debug', 'propagate': 'false'
|
'level': 'debug', 'propagate': 'false'
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
config_dict['loggers'][_logger_name]['level'] = 'debug'
|
config_dict['loggers']['app_skellington']['level'] = 'debug'
|
||||||
|
|
||||||
def _convert_str_to_loglevel(self, dict_, key):
|
def _convert_str_to_loglevel(self, dict_, key):
|
||||||
"""
|
"""
|
||||||
|
32
setup.py
32
setup.py
@ -12,35 +12,24 @@
|
|||||||
#
|
#
|
||||||
# de-installation:
|
# de-installation:
|
||||||
#
|
#
|
||||||
# $ pip uninstall app_skellington
|
# $ pip uninstall <app>
|
||||||
|
|
||||||
|
|
||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
import os
|
|
||||||
|
|
||||||
__project__ = 'app_skellington'
|
__project__ = 'app_skellington'
|
||||||
__version__ = '0.1.1'
|
__version__ = '0.1.0'
|
||||||
__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(
|
setup(
|
||||||
name = __project__,
|
name = __project__,
|
||||||
version = __version__,
|
version = __version__,
|
||||||
description = 'A high-powered command line menu framework.',
|
description = 'A high-powered 2-level CLI framework',
|
||||||
long_description = long_description,
|
author = 'Mathew Guest',
|
||||||
author = 'Mathew Guest',
|
author_email = 't3h.zavage@gmail.com',
|
||||||
author_email = 't3h.zavage@gmail.com',
|
url = 'https://git-mirror.zavage-software.com/Mirror/app_skellington',
|
||||||
url = 'https://git-mirror.zavage.net/Mirror/app_skellington',
|
license = 'MIT',
|
||||||
license = 'MIT',
|
|
||||||
|
|
||||||
python_requires = '>=3',
|
python_requires = '>=3',
|
||||||
|
|
||||||
classifiers = [
|
classifiers = [
|
||||||
'Development Status :: 3 - Alpha',
|
'Development Status :: 3 - Alpha',
|
||||||
@ -71,5 +60,6 @@ setup(
|
|||||||
packages = (
|
packages = (
|
||||||
'app_skellington',
|
'app_skellington',
|
||||||
),
|
),
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -3,6 +3,6 @@ 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 == False
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user