Browse Source

more polish for release

Mathew Guest 6 months ago
parent
commit
7bd6e11378

+ 6 - 0
README.md

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

+ 0 - 3
app_skellington/__init__.py

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

+ 4 - 4
app_skellington/_bootstrap.py

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

+ 15 - 24
app_skellington/app_container.py

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

+ 5 - 11
app_skellington/cfg.py

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

+ 5 - 28
app_skellington/cli.py

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

+ 8 - 5
app_skellington/log.py

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

+ 3 - 4
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',
     ),
-
 )
 

+ 1 - 1
tests/test_cli.py

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