Files
linux-kernel-module-cheat/cli_function.py
Ciro Santilli 六四事件 法轮功 fa1e4ffa7d run kind of runs
2018-12-09 00:00:01 +00:00

263 lines
9.4 KiB
Python
Executable File

#!/usr/bin/env python3
import argparse
import imp
import os
class Argument:
def __init__(
self,
long_or_short_1,
long_or_short_2=None,
default=None,
help=None,
nargs=None,
**kwargs
):
if long_or_short_2 is None:
shortname = None
longname = long_or_short_1
else:
shortname = long_or_short_1
longname = long_or_short_2
self.args = []
# argparse is crappy and cannot tell us if arguments were given or not.
# We need that information to decide if the config file should override argparse or not.
# So we just use None as a sentinel.
self.kwargs = {'default': None}
if shortname is not None:
self.args.append(shortname)
if longname[0] == '-':
self.args.append(longname)
self.key = longname.lstrip('-').replace('-', '_')
self.is_option = True
else:
self.key = longname.replace('-', '_')
self.args.append(self.key)
self.kwargs['metavar'] = longname
self.is_option = False
if default is not None and nargs is None:
self.kwargs['nargs'] = '?'
if nargs is not None:
self.kwargs['nargs'] = nargs
if default is True:
bool_action = 'store_false'
self.is_bool = True
elif default is False:
bool_action = 'store_true'
self.is_bool = True
else:
self.is_bool = False
if default is None and nargs in ('*', '+'):
default = []
if self.is_bool and not 'action' in kwargs:
self.kwargs['action'] = bool_action
if help is not None:
if not self.is_bool and default:
help += ' Default: {}'.format(default)
self.kwargs['help'] = help
self.optional = (
default is not None or
self.is_bool or
self.is_option or
nargs in ('?', '*', '+')
)
self.kwargs.update(kwargs)
self.default = default
self.longname = longname
def __str__(self):
return str(self.args) + ' ' + str(self.kwargs)
class CliFunction:
'''
Represent a function that can be called either from Python code, or
from the command line.
Features:
* single argument description in format very similar to argparse
* handle default arguments transparently in both cases
* expose a configuration file mechanism to get default parameters from a file
* fix some argparse.ArgumentParser() annoyances:
** allow dashes in positional arguments:
https://stackoverflow.com/questions/12834785/having-options-in-argparse-with-a-dash
** boolean defaults automatically use store_true or store_false, and add a --no-* CLI
option to invert them if set from the config
This somewhat duplicates: https://click.palletsprojects.com but:
* that decorator API is insane
* CLI + Python for single functions was wontfixed: https://github.com/pallets/click/issues/40
'''
def __call__(self, **args):
'''
Python version of the function call.
:type arguments: Dict
'''
args_with_defaults = args.copy()
# Add missing args from config file.
if 'config_file' in args_with_defaults and args_with_defaults['config_file'] is not None:
config_file = args_with_defaults['config_file']
else:
config_file = self._config_file
if os.path.exists(config_file):
config_configs = {}
config = imp.load_source('config', config_file)
config.set_args(config_configs)
for key in config_configs:
if key not in self._all_keys:
raise Exception('Unknown key in config file: ' + key)
if (not key in args_with_defaults) or args_with_defaults[key] is None:
args_with_defaults[key] = config_configs[key]
# Add missing args from hard-coded defaults.
for argument in self._arguments:
key = argument.key
if (not key in args_with_defaults) or args_with_defaults[key] is None:
if argument.optional:
args_with_defaults[key] = argument.default
else:
raise Exception('Value not given for mandatory argument: ' + key)
return self.main(**args_with_defaults)
def __init__(self, config_file=None, description=None):
self._all_keys = set()
self._arguments = []
self._config_file = config_file
self._description = description
if self._config_file is not None:
self.add_argument(
'--config-file',
default=self._config_file,
help='Path to the configuration file to use'
)
def __str__(self):
return '\n'.join(str(arg) for arg in self._arguments)
def add_argument(
self,
*args,
**kwargs
):
argument = Argument(*args, **kwargs)
self._arguments.append(argument)
self._all_keys.add(argument.key)
def cli(self, cli_args=None):
'''
Call the function from the CLI. Parse command line arguments
to get all arguments.
'''
parser = argparse.ArgumentParser(
description=self._description,
formatter_class=argparse.RawTextHelpFormatter,
)
for argument in self._arguments:
parser.add_argument(*argument.args, **argument.kwargs)
if argument.is_bool:
new_longname = '--no' + argument.longname[1:]
kwargs = argument.kwargs.copy()
kwargs['default'] = not argument.default
if kwargs['action'] == 'store_false':
kwargs['action'] = 'store_true'
elif kwargs['action'] == 'store_true':
kwargs['action'] = 'store_false'
if 'help' in kwargs:
del kwargs['help']
parser.add_argument(new_longname, dest=argument.key, **kwargs)
args = parser.parse_args(args=cli_args)
return self(**vars(args))
def main(self, **kwargs):
'''
Do the main function call work.
:type arguments: Dict
'''
raise NotImplementedError
if __name__ == '__main__':
class OneCliFunction(CliFunction):
def __init__(self):
super().__init__(
config_file='cli_function_test_config.py',
description = '''\
Description of this
amazing function!
''',
)
self.add_argument('-a', '--asdf', default='A', help='Help for asdf'),
self.add_argument('-q', '--qwer', default='Q', help='Help for qwer'),
self.add_argument('-b', '--bool', default=True, help='Help for bool'),
self.add_argument('--bool-cli', default=False, help='Help for bool'),
self.add_argument('--bool-nargs', default=False, nargs='?', action='store', const='')
self.add_argument('--no-default', help='Help for no-bool'),
self.add_argument('pos-mandatory', help='Help for pos-mandatory', type=int),
self.add_argument('pos-optional', default=0, help='Help for pos-optional', type=int),
self.add_argument('args-star', help='Help for args-star', nargs='*'),
def main(self, **kwargs):
del kwargs['config_file']
return kwargs
one_cli_function = OneCliFunction()
# Default code call.
default = one_cli_function(pos_mandatory=1)
assert default == {
'asdf': 'A',
'qwer': 'Q',
'bool': True,
'bool_nargs': False,
'bool_cli': True,
'no_default': None,
'pos_mandatory': 1,
'pos_optional': 0,
'args_star': []
}
# Default CLI call.
out = one_cli_function.cli(['1'])
assert out == default
# asdf
out = one_cli_function(pos_mandatory=1, asdf='B')
assert out['asdf'] == 'B'
out['asdf'] = default['asdf']
assert(out == default)
# asdf and qwer
out = one_cli_function(pos_mandatory=1, asdf='B', qwer='R')
assert out['asdf'] == 'B'
assert out['qwer'] == 'R'
out['asdf'] = default['asdf']
out['qwer'] = default['qwer']
assert(out == default)
if '--bool':
out = one_cli_function(pos_mandatory=1, bool=False)
cli_out = one_cli_function.cli(['--bool', '1'])
assert out == cli_out
assert out['bool'] == False
out['bool'] = default['bool']
assert(out == default)
if '--bool-nargs':
out = one_cli_function(pos_mandatory=1, bool_nargs=True)
assert out['bool_nargs'] == True
out['bool_nargs'] = default['bool_nargs']
assert(out == default)
out = one_cli_function(pos_mandatory=1, bool_nargs='asdf')
assert out['bool_nargs'] == 'asdf'
out['bool_nargs'] = default['bool_nargs']
assert(out == default)
# Force a boolean value set on the config to be False on CLI.
assert one_cli_function.cli(['--no-bool-cli', '1'])['bool_cli'] is False
# CLI call.
print(one_cli_function.cli())