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
|
||||
===============
|
||||
Application framework for Python, features include:
|
||||
- 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
|
||||
- INI-style config and and validation (provided through ConfigObj).
|
||||
- Colored logging (provided through colorlog)
|
||||
- Works on Linux, Windows, and Mac.
|
||||
* 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
|
||||
* INI-style config and and validation (provided through ConfigObj).
|
||||
* Colored logging (provided through colorlog)
|
||||
* Works on Linux, Windows, and Mac.
|
||||
|
||||
Principles:
|
||||
- Lend to creating beautiful, easy to read and understand code in the application.
|
||||
- Minimize coupling of applications to this framework.
|
||||
- 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
|
||||
* Lend to creating beautiful, easy to read and understand code in the application.
|
||||
* Minimize coupling of applications to this framework.
|
||||
* 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
|
||||
|
||||
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
|
||||
format. See:
|
||||
|
||||
- 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#the-config-file-format
|
||||
* https://configobj.readthedocs.io/en/latest/configobj.html#validation
|
||||
|
||||
Config files (config.ini) are created if they don't exist. The
|
||||
file always contains the full specification of parameters; i.e. even default
|
||||
@ -38,9 +38,9 @@ Linux:
|
||||
|
||||
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>
|
||||
argument.
|
||||
@ -50,26 +50,19 @@ Debug - Turn on Logging
|
||||
Set 'APPSKELLINGTON_ENABLE_LOGGING' environment variable to any value which turns
|
||||
on AppSkellington-level logging. For example,
|
||||
|
||||
APPSKELLINGTON_DEBUG=1 <executable>
|
||||
APP_SKELLINGTON_DEBUG=1 <executable>
|
||||
|
||||
or
|
||||
|
||||
export APPSKELLINGTON_DEBUG=1
|
||||
export APP_SKELLINGTON_DEBUG=1
|
||||
<executable>
|
||||
|
||||
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. Alternatively, you are permitted you
|
||||
to use any of this under the GPL to the fullest legal extent allowed.
|
||||
|
||||
Notes
|
||||
-----
|
||||
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 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,21 +10,21 @@ def check_env_has_dependencies(libnames):
|
||||
try:
|
||||
__import__(libname)
|
||||
except ModuleNotFoundError as ex:
|
||||
print('Missing third-party library: ', ex, file=sys.stderr)
|
||||
print('missing third-part library: ', ex, file=sys.stderr)
|
||||
rc = False
|
||||
return rc
|
||||
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')
|
||||
|
||||
# Logger for before the application and logging config is loaded
|
||||
# - used to log before logging is configured
|
||||
_log_fmt = '%(levelname)-7s:%(message)s'
|
||||
_logger_name = 'skell'
|
||||
_logger_name = 'app_skellington'
|
||||
_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
|
||||
# configuration is reloaded. This catches APPSKELLINGTON_DEBUG
|
||||
# environment variable the first time, as app_skellington module
|
||||
|
@ -12,12 +12,20 @@ from . import _util
|
||||
from . import cli
|
||||
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.
|
||||
DEFAULT_APP_NAME = ''
|
||||
DEFAULT_APP_AUTHOR = ''
|
||||
|
||||
# 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
|
||||
|
||||
class ApplicationContext:
|
||||
"""
|
||||
@ -36,9 +44,6 @@ 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,
|
||||
@ -46,35 +51,27 @@ class ApplicationContainer:
|
||||
configini_filepath=None,
|
||||
*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
|
||||
|
||||
# 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)
|
||||
# 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)
|
||||
|
||||
# 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 = 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)):
|
||||
@ -96,7 +93,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['datalayer'] => returns the made-up "datalayer" service.
|
||||
app_container['netezza'] => returns the netezza service instance
|
||||
"""
|
||||
try:
|
||||
service_factory = self._dependencies[service_name] # Retrieve factory function
|
||||
@ -104,6 +101,7 @@ 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):
|
||||
@ -131,9 +129,7 @@ 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.
|
||||
|
||||
|
@ -18,11 +18,6 @@ class Config:
|
||||
"""
|
||||
Structure to store application runtime configuration. Also contains
|
||||
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 = {
|
||||
@ -82,9 +77,15 @@ 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
|
||||
@ -99,17 +100,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()
|
||||
|
||||
@ -151,19 +152,6 @@ class Config:
|
||||
"""
|
||||
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(
|
||||
self, configspec_filepath=None, configini_filepath=None
|
||||
):
|
||||
@ -228,7 +216,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 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(
|
||||
val, copy=True, preserve_errors=True
|
||||
)
|
||||
|
@ -80,9 +80,9 @@ class CommandTree:
|
||||
Creates a root-level submenu with no entries. SubMenu node is
|
||||
returned which can have submenus and commands attached to it.
|
||||
"""
|
||||
# NOTE(MG) Fix for Python>=3.7,
|
||||
# argparse.ArgumentParser added 'required' argument.
|
||||
# Must also be written into SubMenu.create_submenu.
|
||||
# NOTE(MG) Fix below strategizes whether to pass in 'required'
|
||||
# paremter to ArgumentParser.add_subparsers()
|
||||
# which was added in in Python3.7.
|
||||
func_args = {
|
||||
'dest': param_name,
|
||||
'metavar': param_name,
|
||||
@ -90,12 +90,11 @@ class CommandTree:
|
||||
}
|
||||
if (
|
||||
sys.version_info.major == 3
|
||||
and sys.version_info.minor < 7
|
||||
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']
|
||||
# END fix for Python<3.7
|
||||
|
||||
# Creates an argument as a slot in the underlying argparse.
|
||||
subparsers = self.root_parser.add_subparsers(
|
||||
@ -175,6 +174,13 @@ 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
|
||||
@ -191,6 +197,11 @@ 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:]
|
||||
@ -226,15 +237,16 @@ class CommandTree:
|
||||
return
|
||||
|
||||
if args is False and unk is False:
|
||||
_bootstrap_logger.error('cli - Failed parsing args.')
|
||||
_bootstrap_logger.error('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:
|
||||
_bootstrap_logger.critical('cli - Failed to find command.')
|
||||
print('cmd is None')
|
||||
_bootstrap_logger.error('cli - failed to find command')
|
||||
return False
|
||||
|
||||
return self._invoke_command(cmd, args)
|
||||
@ -271,6 +283,7 @@ 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]
|
||||
@ -341,14 +354,16 @@ 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:
|
||||
# TODO(MG) Safer sanitation
|
||||
# safe try/except
|
||||
cmd_name = func.__name__
|
||||
|
||||
if func_signature is None:
|
||||
@ -371,7 +386,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.add_subparsers()
|
||||
# created when the SubMenu/argparse.addZ_subparsers()
|
||||
# was created.
|
||||
help=help_text,
|
||||
description=description_text
|
||||
@ -402,6 +417,13 @@ 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
|
||||
@ -447,29 +469,12 @@ class SubMenu:
|
||||
help='sub-submenu help',
|
||||
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:
|
||||
# type = _SubParsersAction
|
||||
subp_node = entry_node.add_subparsers(
|
||||
**func_args
|
||||
)
|
||||
dest = var_name,
|
||||
metavar = var_name,
|
||||
required = is_required)
|
||||
|
||||
submenu = SubMenu(
|
||||
self.parent,
|
||||
|
@ -1,4 +1,4 @@
|
||||
from ._bootstrap import _bootstrap_logger, _logger_name
|
||||
from ._bootstrap import _bootstrap_logger
|
||||
from . import _util
|
||||
|
||||
import appdirs
|
||||
@ -22,6 +22,7 @@ DEFAULT_LOG_SETTINGS = {
|
||||
'level': 'debug',
|
||||
'formatter': 'colored'
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
'loggers': {
|
||||
@ -38,11 +39,9 @@ DEFAULT_LOG_SETTINGS = {
|
||||
}
|
||||
|
||||
class LoggingLayer:
|
||||
def __init__(
|
||||
self, appname=None, appauthor=None
|
||||
):
|
||||
self.appname = appname or ''
|
||||
self.appauthor = appauthor or ''
|
||||
def __init__(self, appname, appauthor, config=None):
|
||||
self.appname = appname
|
||||
self.appauthor = appauthor
|
||||
self.loggers = {}
|
||||
|
||||
def __getitem__(self, k):
|
||||
@ -77,6 +76,7 @@ class LoggingLayer:
|
||||
self.transform_config(config_dict)
|
||||
|
||||
try:
|
||||
# TODO(MG) switch to pretty-print, as it'd be more human readable
|
||||
_bootstrap_logger.debug('log - Log configuration: %s', config_dict)
|
||||
logging.config.dictConfig(config_dict)
|
||||
_bootstrap_logger.debug('log - Configured all logging')
|
||||
@ -106,16 +106,13 @@ class LoggingLayer:
|
||||
d = config_dict['loggers'][logger]
|
||||
self._convert_str_to_loglevel(d, 'level')
|
||||
|
||||
|
||||
# Implementation note:
|
||||
# app_skellington expects root logger configuration to be under 'root'
|
||||
# instead of '' (python spec) because '' is not a valid name in ConfigObj.
|
||||
# Replace 'root' logger with '', logging module convention for root handler
|
||||
# Note: '' is disallowed in ConfigObj (hence the reason for this replacement)
|
||||
try:
|
||||
if config_dict['loggers'].get('root') is not None:
|
||||
config_dict['loggers'][''] = config_dict['loggers']['root']
|
||||
del config_dict['loggers']['root']
|
||||
config_dict['loggers'][''] = config_dict['loggers']['root']
|
||||
del config_dict['loggers']['root']
|
||||
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
|
||||
@ -133,20 +130,19 @@ class LoggingLayer:
|
||||
config_dict['handlers']['file']['filename'] = log_filepath
|
||||
|
||||
def _add_own_logconfig(self, config_dict):
|
||||
# NOTE(MG) Monkey-patch logger
|
||||
# This is done twice: once when app_skellington
|
||||
# NOTE(MG) 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
|
||||
# logging configuration is read from config file.
|
||||
# See _bootstrap.py
|
||||
if os.environ.get('APPSKELLINGTON_DEBUG', None):
|
||||
if _logger_name not in config_dict['loggers']:
|
||||
config_dict['loggers'][_logger_name] = {
|
||||
if 'app_skellington' not in config_dict['loggers']:
|
||||
config_dict['loggers']['app_skellington'] = {
|
||||
'level': 'debug', 'propagate': 'false'
|
||||
}
|
||||
else:
|
||||
config_dict['loggers'][_logger_name]['level'] = 'debug'
|
||||
config_dict['loggers']['app_skellington']['level'] = 'debug'
|
||||
|
||||
def _convert_str_to_loglevel(self, dict_, key):
|
||||
"""
|
||||
|
32
setup.py
32
setup.py
@ -12,35 +12,24 @@
|
||||
#
|
||||
# de-installation:
|
||||
#
|
||||
# $ pip uninstall app_skellington
|
||||
# $ pip uninstall <app>
|
||||
|
||||
|
||||
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()
|
||||
__version__ = '0.1.0'
|
||||
|
||||
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',
|
||||
name = __project__,
|
||||
version = __version__,
|
||||
description = 'A high-powered 2-level CLI framework',
|
||||
author = 'Mathew Guest',
|
||||
author_email = 't3h.zavage@gmail.com',
|
||||
url = 'https://git-mirror.zavage-software.com/Mirror/app_skellington',
|
||||
license = 'MIT',
|
||||
|
||||
python_requires = '>=3',
|
||||
python_requires = '>=3',
|
||||
|
||||
classifiers = [
|
||||
'Development Status :: 3 - Alpha',
|
||||
@ -71,5 +60,6 @@ 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 == True
|
||||
assert True == False
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user