mirror of
https://git.zavage.net/Zavage-Software/app_skellington.git
synced 2024-12-21 06:19:19 -07:00
more polish for release
This commit is contained in:
parent
87fbc405c0
commit
7bd6e11378
@ -61,6 +61,12 @@ Tests
|
||||
-----
|
||||
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.
|
||||
|
||||
Notes
|
||||
-----
|
||||
See official website: https://zavage-software.com
|
||||
|
@ -1,9 +1,6 @@
|
||||
import logging
|
||||
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 .cfg import *
|
||||
from .cli import *
|
||||
|
@ -10,11 +10,11 @@ def check_env_has_dependencies(libnames):
|
||||
try:
|
||||
__import__(libname)
|
||||
except ModuleNotFoundError as ex:
|
||||
print('missing third-part library: ', ex, file=sys.stderr)
|
||||
print('Missing third-party library: ', ex, file=sys.stderr)
|
||||
rc = False
|
||||
return rc
|
||||
if not check_env_has_dependencies(libnames):
|
||||
print('refusing 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')
|
||||
|
||||
# Logger for before the application and logging config is loaded
|
||||
@ -23,8 +23,8 @@ _log_fmt = '%(levelname)-7s:%(message)s'
|
||||
_logger_name = 'app_skellington'
|
||||
_bootstrap_logger = logging.getLogger(_logger_name)
|
||||
|
||||
|
||||
# NOTE(MG) This is done twice: once when app_skellington
|
||||
# NOTE(MG) Logger monkey-patch:
|
||||
# This is done twice: once when app_skellington
|
||||
# module is imported via _bootstrap.py and again if logging
|
||||
# configuration is reloaded. This catches APPSKELLINGTON_DEBUG
|
||||
# environment variable the first time, as app_skellington module
|
||||
|
@ -12,20 +12,10 @@ from . import _util
|
||||
from . import cli
|
||||
from . import cfg
|
||||
|
||||
DEFAULT_APP_NAME = 'python-app'
|
||||
DEFAULT_APP_AUTHOR = 'John Doe'
|
||||
|
||||
|
||||
# OPTIONAL: classes can sub-class from this?
|
||||
class Components:
|
||||
def inject_dependencies_based_on_names_in_args(self):
|
||||
pass
|
||||
|
||||
def inject_dependency(self, name):
|
||||
pass
|
||||
|
||||
def register_dependency(self, service, name):
|
||||
pass
|
||||
# These two variables affect the directory paths for
|
||||
# config files and logging.
|
||||
DEFAULT_APP_NAME = ''
|
||||
DEFAULT_APP_AUTHOR = ''
|
||||
|
||||
class ApplicationContext:
|
||||
"""
|
||||
@ -44,6 +34,9 @@ class ApplicationContainer:
|
||||
object instances for services, passes off to the cli to determine what to
|
||||
do, and then injects any necessary dependencies (e.g. database module)
|
||||
and kicks off the functionality requested in the cli.
|
||||
|
||||
Override appname and appauthor arguments to direct config and log
|
||||
directories.
|
||||
"""
|
||||
def __init__(
|
||||
self,
|
||||
@ -51,20 +44,16 @@ class ApplicationContainer:
|
||||
configini_filepath=None,
|
||||
*args, **kwargs
|
||||
):
|
||||
# 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.appname = kwargs.get('appname') or DEFAULT_APP_NAME
|
||||
self.appauthor = kwargs.get('appauthor') or DEFAULT_APP_AUTHOR
|
||||
|
||||
# Instantiate application context which contains
|
||||
# global state, configuration, loggers, and runtime args.
|
||||
self._dependencies = {}
|
||||
|
||||
config = cfg.Config(configspec_filepath, configini_filepath)
|
||||
|
||||
logger = log.LoggingLayer(self.appname, self.appauthor)
|
||||
|
||||
# added here, is this okay to do twice?
|
||||
logger.configure_logging()
|
||||
|
||||
self.ctx = ApplicationContext(config, logger)
|
||||
@ -72,6 +61,7 @@ class ApplicationContainer:
|
||||
|
||||
self.cli = cli.CommandTree() # Command-line interface
|
||||
|
||||
# Run methods if subclass implemented them:
|
||||
if callable(getattr(self, '_cli_options', None)):
|
||||
self._cli_options()
|
||||
if callable(getattr(self, '_services', None)):
|
||||
@ -93,7 +83,7 @@ class ApplicationContainer:
|
||||
Returns a factory of a service or dependency. The factory is a function
|
||||
that is called to return an instance of the service object.
|
||||
|
||||
app_container['netezza'] => returns the netezza service instance
|
||||
app['datalayer'] => returns the made-up "datalayer" service.
|
||||
"""
|
||||
try:
|
||||
service_factory = self._dependencies[service_name] # Retrieve factory function
|
||||
@ -101,7 +91,6 @@ class ApplicationContainer:
|
||||
except KeyError as ex:
|
||||
msg = 'failed to inject service: {}'.format(service_name)
|
||||
_bootstrap_logger.critical(msg)
|
||||
_util.eprint(msg)
|
||||
raise ServiceNotFound
|
||||
|
||||
def __setitem__(self, service_name, value):
|
||||
@ -129,7 +118,9 @@ class ApplicationContainer:
|
||||
dependencies.append(self[dep_name])
|
||||
return model_constructor(*dependencies)
|
||||
|
||||
def _get_config_filepath(self, app_name, app_author, config_filename='config.ini'):
|
||||
def _get_config_filepath(
|
||||
self, app_name, app_author, config_filename='config.ini'
|
||||
):
|
||||
"""
|
||||
Attempt to find config.ini in the user's config directory.
|
||||
|
||||
|
@ -77,15 +77,9 @@ class Config:
|
||||
|
||||
@configspec_filepath.setter
|
||||
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:
|
||||
_bootstrap_logger.debug(
|
||||
'cfg - clearing configspec'
|
||||
'cfg - Clearing configspec'
|
||||
)
|
||||
self._configspec_filepath = None
|
||||
self._configspec_data = None
|
||||
@ -100,17 +94,17 @@ class Config:
|
||||
self._configspec_data = data
|
||||
self._has_changed_internally = True
|
||||
_bootstrap_logger.debug(
|
||||
'cfg - set configspec and read contents: %s',
|
||||
'cfg - Set configspec and read contents: %s',
|
||||
filepath
|
||||
)
|
||||
self.load_config()
|
||||
return
|
||||
except OSError as ex:
|
||||
_bootstrap_logger.critical(
|
||||
'cfg - failed to find config.spec: file not found (%s)',
|
||||
'cfg - Failed to find config.spec: file not found (%s)',
|
||||
filepath
|
||||
)
|
||||
raise OSError('failed to read provided config.spec file')
|
||||
raise OSError('Failed to read provided config.spec file')
|
||||
|
||||
self.load_config()
|
||||
|
||||
@ -216,7 +210,7 @@ class Config:
|
||||
_bootstrap_logger.info('cfg - Validating config file against spec')
|
||||
val = validate.Validator()
|
||||
assert isinstance(self._config_obj, configobj.ConfigObj), 'expecting configobj.ConfigObj, received %s' % type(self._config_obj)
|
||||
# NOTE(MG) copy below instructs configobj to use defaults from spec file
|
||||
# NOTE(MG) copy arg below instructs configobj to use defaults from spec file
|
||||
test_results = self._config_obj.validate(
|
||||
val, copy=True, preserve_errors=True
|
||||
)
|
||||
|
@ -175,13 +175,6 @@ class CommandTree:
|
||||
key,
|
||||
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
|
||||
cmd = CommandEntry()
|
||||
cmd.argparse_node = self.root_parser
|
||||
@ -198,11 +191,6 @@ class CommandTree:
|
||||
self._single_command = cmd
|
||||
self._entries = None
|
||||
|
||||
# def _validate(self):
|
||||
# pass
|
||||
# # TODO(MG):
|
||||
# # subparser can not be empty, needs to have parsers attached
|
||||
|
||||
def parse(self, args=None):
|
||||
if args is None:
|
||||
args = sys.argv[1:]
|
||||
@ -238,16 +226,15 @@ class CommandTree:
|
||||
return
|
||||
|
||||
if args is False and unk is False:
|
||||
_bootstrap_logger.error('failed parsing args')
|
||||
_bootstrap_logger.error('cli - Failed parsing args.')
|
||||
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)
|
||||
|
||||
cmd = self._lookup_command(args)
|
||||
if cmd is None:
|
||||
print('cmd is None')
|
||||
_bootstrap_logger.error('cli - failed to find command')
|
||||
_bootstrap_logger.critical('cli - Failed to find command.')
|
||||
return False
|
||||
|
||||
return self._invoke_command(cmd, args)
|
||||
@ -284,7 +271,6 @@ class CommandTree:
|
||||
|
||||
lookup = submenu.entries.get(val)
|
||||
_bootstrap_logger.debug('cli - lookup, entries[{}] = {}'.format(val, lookup))
|
||||
# print(submenu.entries)
|
||||
|
||||
# pop value
|
||||
del args[argparse_param]
|
||||
@ -355,16 +341,14 @@ class SubMenu:
|
||||
execute the command function.
|
||||
"""
|
||||
if inspect.isfunction(func):
|
||||
# print('func is function')
|
||||
pass
|
||||
elif inspect.ismethod(func):
|
||||
pass
|
||||
# print('func is method')
|
||||
else:
|
||||
raise Exception('bad value passed in for function')
|
||||
|
||||
if not cmd_name:
|
||||
# safe try/except
|
||||
# TODO(MG) Safer sanitation
|
||||
cmd_name = func.__name__
|
||||
|
||||
if func_signature is None:
|
||||
@ -387,7 +371,7 @@ class SubMenu:
|
||||
child_node = self.subparsers_obj.add_parser(
|
||||
cmd_name, # Note: cmd_name here will be the VALUE
|
||||
# passed into the argparse arg VARIABLE NAME
|
||||
# created when the SubMenu/argparse.addZ_subparsers()
|
||||
# created when the SubMenu/argparse.add_subparsers()
|
||||
# was created.
|
||||
help=help_text,
|
||||
description=description_text
|
||||
@ -418,13 +402,6 @@ class SubMenu:
|
||||
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
|
||||
cmd = CommandEntry()
|
||||
cmd.argparse_node = child_node
|
||||
|
@ -39,9 +39,11 @@ DEFAULT_LOG_SETTINGS = {
|
||||
}
|
||||
|
||||
class LoggingLayer:
|
||||
def __init__(self, appname, appauthor, config=None):
|
||||
self.appname = appname
|
||||
self.appauthor = appauthor
|
||||
def __init__(
|
||||
self, appname=None, appauthor=None
|
||||
):
|
||||
self.appname = appname or ''
|
||||
self.appauthor = appauthor or ''
|
||||
self.loggers = {}
|
||||
|
||||
def __getitem__(self, k):
|
||||
@ -76,7 +78,7 @@ class LoggingLayer:
|
||||
self.transform_config(config_dict)
|
||||
|
||||
try:
|
||||
# TODO(MG) switch to pretty-print, as it'd be more human readable
|
||||
# TODO(MG) Pretty print
|
||||
_bootstrap_logger.debug('log - Log configuration: %s', config_dict)
|
||||
logging.config.dictConfig(config_dict)
|
||||
_bootstrap_logger.debug('log - Configured all logging')
|
||||
@ -130,7 +132,8 @@ class LoggingLayer:
|
||||
config_dict['handlers']['file']['filename'] = log_filepath
|
||||
|
||||
def _add_own_logconfig(self, config_dict):
|
||||
# NOTE(MG) This is done twice: once when app_skellington
|
||||
# NOTE(MG) Monkey-patch logger
|
||||
# This is done twice: once when app_skellington
|
||||
# module is imported again if logging configuration is
|
||||
# reloaded. This catches APPSKELLINGTON_DEBUG environment
|
||||
# variable the second time, when it's being reloaded as a
|
||||
|
7
setup.py
7
setup.py
@ -19,7 +19,7 @@ from setuptools import setup
|
||||
import os
|
||||
|
||||
__project__ = 'app_skellington'
|
||||
__version__ = '0.1.1'
|
||||
__version__ = '0.1.4'
|
||||
__description__ = 'A high-powered command line menu framework.'
|
||||
|
||||
long_description = __description__
|
||||
@ -33,11 +33,11 @@ with open(readme_filepath, encoding='utf-8') as fp:
|
||||
setup(
|
||||
name = __project__,
|
||||
version = __version__,
|
||||
description = 'A high-powered 2-level CLI framework',
|
||||
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-software.com/Mirror/app_skellington',
|
||||
url = 'https://git-mirror.zavage.net/Mirror/app_skellington',
|
||||
license = 'MIT',
|
||||
|
||||
python_requires = '>=3',
|
||||
@ -71,6 +71,5 @@ setup(
|
||||
packages = (
|
||||
'app_skellington',
|
||||
),
|
||||
|
||||
)
|
||||
|
||||
|
@ -3,6 +3,6 @@ from app_skellington.cli import CommandTree
|
||||
class TestCli_e2e:
|
||||
def test_null_constructor_works(self):
|
||||
x = CommandTree()
|
||||
assert True == False
|
||||
assert True == True
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user