renamed cython_types.pyx python_types.py to types.py[x], switching between them is setup.py's job
moved help texts to over.docs wip over.app implementation
This commit is contained in:
parent
a4665ae277
commit
031f7627b8
5 changed files with 137 additions and 625 deletions
|
@ -3,24 +3,21 @@
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
#from . import app
|
from . import app
|
||||||
from . import aux
|
from . import aux
|
||||||
from . import cmd
|
from . import cmd
|
||||||
|
from . import docs
|
||||||
from . import file
|
from . import file
|
||||||
from . import misc
|
from . import misc
|
||||||
from . import text
|
from . import text
|
||||||
|
from . import types
|
||||||
from . import version
|
from . import version
|
||||||
|
|
||||||
print = text.Output("over.__init__", stream=sys.stderr)
|
print = text.Output("over.__init__", stream=sys.stderr)
|
||||||
|
|
||||||
try:
|
|
||||||
from . import cython_types as types
|
|
||||||
except:
|
|
||||||
from . import python_types as types
|
|
||||||
|
|
||||||
print("unable to load C implementation, using python instead", print.tl.warn)
|
|
||||||
|
|
||||||
core = aux.DeprecationForwarder(sys.modules[__name__], "over.core", "over")
|
core = aux.DeprecationForwarder(sys.modules[__name__], "over.core", "over")
|
||||||
textui = aux.DeprecationForwarder(text, "over.core.textui", "over.text")
|
textui = aux.DeprecationForwarder(text, "over.core.textui", "over.text")
|
||||||
|
|
||||||
del sys
|
for module in [types]:
|
||||||
|
if module.__file__[-2:] == "py":
|
||||||
|
print("<r>unable to load C implementation<.> of <y>%s<.>, falling back to pure Python instead" %(module.__name__), print.tl.warn)
|
||||||
|
|
706
over/app.py
706
over/app.py
|
@ -1,660 +1,148 @@
|
||||||
#! /usr/bin/env python3
|
#! /usr/bin/env python3
|
||||||
# encoding: utf-8
|
# encoding: utf-8
|
||||||
|
|
||||||
import os
|
from collections import OrderedDict
|
||||||
|
import enum
|
||||||
import sys
|
import sys
|
||||||
import re
|
from . import docs
|
||||||
|
|
||||||
try:
|
|
||||||
import xdg.BaseDirectory as xdg_bd
|
|
||||||
except:
|
|
||||||
xdg_bd = None
|
|
||||||
|
|
||||||
from . import file
|
|
||||||
from . import text
|
from . import text
|
||||||
from . import version
|
from . import types
|
||||||
|
|
||||||
prefix = text.prefix
|
|
||||||
|
|
||||||
# FIXME This very seriously needs to be heavily simplified and de-duplicated
|
|
||||||
# TODO same parser for cmdline and Config
|
|
||||||
# TODO zsh integration
|
|
||||||
|
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
|
|
||||||
def _parse(data, dtype):
|
class Option_sources(enum.Enum):
|
||||||
if dtype == 'str':
|
none = 0
|
||||||
if data.startswith('"') and data.endswith('"'):
|
default = 1
|
||||||
value = data[1:-1].replace('\\"', '"')
|
config = 2
|
||||||
elif data.startswith('\'') and data.endswith('\''):
|
cmdline = 3
|
||||||
value = data[1:-1].replace('\\\'', '\'')
|
|
||||||
|
class Option:
|
||||||
|
def __init__(self, name, description, callback, default=Option_sources.none, count=None, abbr=None, level=2, in_cfg_file=True, in_help=True):
|
||||||
|
self.name = name
|
||||||
|
self.description = description
|
||||||
|
self.callback = callback
|
||||||
|
self.default = default
|
||||||
|
self.count = count
|
||||||
|
self.abbr = abbr
|
||||||
|
self.level = level
|
||||||
|
self.in_cfg_file = in_cfg_file
|
||||||
|
self.in_help = in_help
|
||||||
|
|
||||||
|
if default == Option_sources.none:
|
||||||
|
self.source = Option_sources.none
|
||||||
|
self._value = Option_sources.none
|
||||||
else:
|
else:
|
||||||
raise RuntimeError
|
self.source = Option_sources.default
|
||||||
|
self._value = default
|
||||||
|
|
||||||
elif dtype == 'bool':
|
def set_value(self, value, source):
|
||||||
if data == 'True':
|
self._value = value
|
||||||
value = True
|
self.source = source
|
||||||
elif data == 'False':
|
|
||||||
value = False
|
@property
|
||||||
|
def value(self):
|
||||||
|
if self.source == Option_sources.none:
|
||||||
|
raise AttributeError("option has no value")
|
||||||
else:
|
else:
|
||||||
raise RuntimeError
|
return self._value
|
||||||
|
|
||||||
elif dtype == 'int':
|
def dump(self):
|
||||||
try:
|
if self.source == Option_sources.none:
|
||||||
value = int(data)
|
return None
|
||||||
except ValueError:
|
else:
|
||||||
raise RuntimeError
|
if self.callback == bool:
|
||||||
|
if self.value:
|
||||||
elif dtype == 'float':
|
...
|
||||||
try:
|
|
||||||
value = float(data)
|
|
||||||
except ValueError:
|
|
||||||
raise RuntimeError
|
|
||||||
|
|
||||||
return value
|
|
||||||
|
|
||||||
_over_help_texts = [
|
|
||||||
('over.core: what, how and huh?', ['over.core is a Python 3 module that provides basic functionality for programs. Functionality such as configuration, commandline parsing, text handling and output, file handling, a non-interactive help system and (not all that much) more.']),
|
|
||||||
('Data Types', ['over.core currently supports 4 data types.', 'A §bbool§/ is either exactly §mTrue§/ or exactly §mFalse§/. Bool options that are §mTrue§/ look like this: §B--§goption§/. The same name only §mFalse§/ would be §B--no-§goption§/. Makes sense, doesn\'t it? Their short versions are either §B+§go§/ for §mTrue§/ or §B-§go§/ for §mFalse§/.', 'A §bstr§/ing is just any raw text. Remember to enclose it in quotes if it has spaces or other nasty characters in it.', 'An §bint§/eger is a whole number. Negative, zero, positive.', 'Finally, a §bfloat§/ is any real number.']),
|
|
||||||
('The Commandline Parser', ['This, in conjunction with the configuration system, is the strongest part of over.core and the very reason for its continued existence.', 'Each configurable option can be assigned to on the command line. Take an option named §Bres-file§/ as an example. To assign to it, you can use §B--§gres-file§/ §msomething§/ (it\'s case sensitive). That\'s it, now its value is §msomething§/! Pretty easy, right?', 'Now, you\'re probably thinking: \'I\'m not typing that all over again!\' Well, you don\'t have to! Options can have their short names. It\'s a single letter (again, case sensitive) with a plus or minus sign in front of it. So §B--§gres-file§/ §msomething§/ becomes §B+§gf§/ §msomething§/. That\'s much better, ain\'t it? And there\'s more. Short names can be grouped together. If you have a bunch of bool switches, like §B--§garmed§/ (short: §B+§gA§/), §B--no-§gsound§/ (short: §B-§gS§/), §B--no-§gstore§/ (short: §B-§gs§/) and §B--§gforce§/ (short: §B+§gF§/), you can group their shorts into groups with the same boolean value: §B-§gSs§/ §B+§gAF§/. You can even have a non-bool option in a group, it just has to be on the very right (because it needs to be followed by data): §B-§gSs§/ §B+§gAFf§/ §msomething§/. It doesn\'t matter if that group begins with §B+§/ or §B-§/.', 'If you use an option more than once, its last (rightmost) occurence applies. For example, after §B+§gf§/ §msomething§/ §B+§gf§/ §mor_other§/ is parsed, option §Bres-file§/ holds the value §mor_other§/. It goes the same for bools: after §B+§gA§/ §B--no-§garmed§/ §B+§gA§/, §Barmed§/ is §mTrue§/. However, if an option is §cplural§/, all occurences are used. Sequence §B+§gA§/ §B-§gA§/ §B+§gA§/ would be [§mTrue§/, §mFalse§/, §mTrue§/], and §B--§gnum§/ §m1§/ §B--§gnum§/ §m2§/ would end up looking like [§m1§/, §m2§/]. You don\'t need to type §B--§gnum§/ for every field either: §B--§gnum§/ §m1§/ §m2§/ would work exactly the same. That\'s because the parser keeps reading everything after a plural option (that takes data, i.e. not bools) right until it encounters two dashes, like those of a following option. You can use just the two dashes to stop the parsing manually, usually when you don\'t want to follow with a long option. Example: §B--§gnum§/ §m1§/ §m5§/ §m9§/ §B--§/. I repeat: a plural (non-bool) option needs to be terminated by two dashes. This wouldn\'t work: §B--§gnum§/ §m1§/ §m5§/ §m9§/ §B+§gf§/ §mthat_one§/, everything after §B--§gnum§/ would be consumed as its data, §Bincluding +§gf§/ §mthat_one§/.']),
|
|
||||||
('The Config File', ['If enabled in the program code, a config file will be generated when you first run it. By default, everything in the config file will be filled with default values and commented out. The general syntax as well as individual options are explained inside. If you run a newer version of the program that offers more configurable options, the config file will be automatically updated. If you\'d like to modify an option in the config file, first uncomment it and then change its value. Resolving order for option values is 1) default (hardcoded), 2) config file and 3) commandline arguments.']),
|
|
||||||
('Other', ['When enabled in the program, an over.core program offers two options: §B--§ghelp§/ and §B--§gover-help§/. The first describes the program and its options, including their types, current values and whether is their current value coming from defaults, config files or the command line. The second option displays the help you\'re reading right now. You may now guess which rank I hold in the Obvious Corps.', 'As the brighter amongst you might have noticed, over.core likes colors. A lot. Generally, I use blue for §bdata types§/, magenta for §mvalues§/, white and green for §B--§goptions§/ and reserve red and yellow for when §rshit hits§/ §ythe fan§/, where red letters usually tell you §Bwhy§/ and yellow ones tell you §Bwhat§/.', 'And it\'s not just colors! I like money, too. Push donations to §B1sekErApM4zh35RFW7qGWs5Yeo9EYWjyV§/, or catch me somewhere and force me to accept cash.'])
|
|
||||||
]
|
|
||||||
|
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
|
|
||||||
def _output(raw, indent=0, newlines=1):
|
class ConfigRouter:
|
||||||
sys.stdout.write(text.render(text.paragraph(raw, indent=indent), True))
|
def __init__(self, options):
|
||||||
sys.stdout.write(newlines * '\n')
|
self.options = options
|
||||||
sys.stdout.flush()
|
|
||||||
|
|
||||||
# --------------------------------------------------
|
def __getattr__(self, name):
|
||||||
|
return self.options[name].value
|
||||||
def _print_help(main, help_texts, chapter=None, list_options=False):
|
|
||||||
'''
|
|
||||||
Displays a help text and then quits the program.
|
|
||||||
|
|
||||||
str chapter display only this chapter
|
|
||||||
bool list_options follow the static printout by a list of options and their current values
|
|
||||||
'''
|
|
||||||
|
|
||||||
# dirty as fuck :-)
|
|
||||||
if not chapter and not main.version is None:
|
|
||||||
if help_texts == _over_help_texts:
|
|
||||||
_output('>>> §yover.core§/ version §y%d§/ (%s), licensed under the §yAO-JSL§/' %_version, newlines=2)
|
|
||||||
else:
|
|
||||||
_output('§y%s§/ version §y%s§/, licensed under the §y%s§/' %(main.name, main.version, main.license), newlines=2)
|
|
||||||
|
|
||||||
# general help texts
|
|
||||||
for help_text in help_texts:
|
|
||||||
_output('[§B%s§/]' %(help_text[0]))
|
|
||||||
for p in help_text[1]:
|
|
||||||
_output(p, indent=4, newlines=2)
|
|
||||||
|
|
||||||
if list_options:
|
|
||||||
_output('[§BAvailable Options§/]')
|
|
||||||
|
|
||||||
for option in [opt for opt in main.options if not opt.hidden]:
|
|
||||||
if option.dtype == 'bool' and not option.callback:
|
|
||||||
long_part = '§B--§g{0}§/ or §B--no-§g{0}§/'.format(option.name)
|
|
||||||
else:
|
|
||||||
long_part = '§B--§g%s§/' %(option.name)
|
|
||||||
|
|
||||||
if option.short_name:
|
|
||||||
if option.dtype == 'bool' and not option.callback:
|
|
||||||
short_part = ' (§B+§g{0}§/ or §B-§g{0}§/)'.format(option.short_name)
|
|
||||||
else:
|
|
||||||
short_part = ' (§B+§g%s§/)' %(option.short_name)
|
|
||||||
else:
|
|
||||||
short_part = ''
|
|
||||||
|
|
||||||
if option.plural:
|
|
||||||
plural_part = '§cplural§/ '
|
|
||||||
else:
|
|
||||||
plural_part = ''
|
|
||||||
|
|
||||||
# if option.dtype == 'boolaaa':
|
|
||||||
# _output('%s%s; §bbool§/' %(long_part, short_part), indent=4)
|
|
||||||
# else:
|
|
||||||
_output('%s%s; %s§b%s§/' %(long_part, short_part, plural_part, option.dtype), indent=4)
|
|
||||||
|
|
||||||
if option.source == 'cmdline':
|
|
||||||
src = '§mcmdline§/'
|
|
||||||
elif option.source == 'config':
|
|
||||||
src = '§yconfig§/'
|
|
||||||
else:
|
|
||||||
src = 'default'
|
|
||||||
|
|
||||||
if option.plural:
|
|
||||||
if option.dtype == 'str':
|
|
||||||
value = '§c[§/%s§c]§/' %(', '.join('\'§m%s§/\'' %(x) for x in option.value))
|
|
||||||
else:
|
|
||||||
value = '§c[§m%s§c]§/' %('§/, §m'.join(str(x) for x in option.value))
|
|
||||||
|
|
||||||
else:
|
|
||||||
if option.dtype == 'str':
|
|
||||||
value = '\'§m%s§/\'' %(option.value)
|
|
||||||
else:
|
|
||||||
value = '§m%s§/' %(option.value)
|
|
||||||
|
|
||||||
if not option.callback:
|
|
||||||
_output('Current value (%s): %s' %(src, value), indent=6)
|
|
||||||
|
|
||||||
if not option.use_cfg_file:
|
|
||||||
_output('§c(ignores config file)§/', indent=6)
|
|
||||||
|
|
||||||
_output(option.description, indent=8, newlines=2)
|
|
||||||
|
|
||||||
_output('[§BTargets§/]')
|
|
||||||
|
|
||||||
if main.targets:
|
|
||||||
_output('§m%s§/' %('§/, §m'.join(main.targets)), newlines=2, indent=4)
|
|
||||||
else:
|
|
||||||
_output('n/a', newlines=2, indent=4)
|
|
||||||
|
|
||||||
main.exit(1, True)
|
|
||||||
|
|
||||||
_allowed_types = {
|
|
||||||
'bool': [bool],
|
|
||||||
'str': [str],
|
|
||||||
'int': [int],
|
|
||||||
'float': [int, float]
|
|
||||||
}
|
|
||||||
|
|
||||||
# --------------------------------------------------
|
|
||||||
|
|
||||||
def _verify_type(data, dtype, plural=False):
|
|
||||||
if plural:
|
|
||||||
if type(data) == list:
|
|
||||||
for d in data:
|
|
||||||
v = _verify_type(d, dtype)
|
|
||||||
|
|
||||||
if not v:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
else:
|
|
||||||
if type(data) in _allowed_types[dtype]:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# --------------------------------------------------
|
|
||||||
|
|
||||||
class CfgAccessor:
|
|
||||||
'''
|
|
||||||
Provides convenient access to configuration options' values.
|
|
||||||
|
|
||||||
e.g. something = main.cfg.option_name
|
|
||||||
'''
|
|
||||||
|
|
||||||
def __init__(self, main):
|
|
||||||
self.main = main
|
|
||||||
|
|
||||||
def __getattr__(self, opt_name):
|
|
||||||
matches = [opt for opt in self.main.options if opt.name.replace('-', '_') == opt_name]
|
|
||||||
|
|
||||||
if matches:
|
|
||||||
if len(matches) == 1:
|
|
||||||
return matches[0].value
|
|
||||||
else:
|
|
||||||
self.main.print('game over, lights out! over.core internal buggaroo', prefix.fail)
|
|
||||||
raise RuntimeError
|
|
||||||
else:
|
|
||||||
self.main.print('option §r%s§/ doesn\'t exist' %(opt_name), prefix.fail)
|
|
||||||
raise RuntimeError
|
|
||||||
|
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
|
|
||||||
class Main:
|
class Main:
|
||||||
'''
|
"""
|
||||||
Application backbone. Provides:
|
Application backbone. Provides:
|
||||||
* A configuration system consisting of
|
* A configuration system consisting of
|
||||||
* a given set of (hardcoded) default values
|
* a given set of (hardcoded) default values
|
||||||
* a configuration file that overrides the defaults
|
* a configuration file that overrides the defaults
|
||||||
* a command line parser that overrides all of the above
|
* a command line parser that overrides all of the above
|
||||||
* Help system that generates help text based on program's configurable options.
|
* Help system that generates help text based on the application's configurable options.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
def __init__(self, name, full_name, version=None, license=None, use_cfg_file=False, cmdline=sys.argv[1:]):
|
|
||||||
'''
|
|
||||||
str name program name (e.g. 'grid')
|
|
||||||
str full_name human readable program name (e.g. 'Overwatch Grid')
|
|
||||||
str version program version
|
|
||||||
str license program license name
|
|
||||||
str use_cfg_file if True, a config file at ~/.config/(self.name)/main.cfg will be used
|
|
||||||
list cmdline command line to parse; defaults to system cmdline (sys.argv)
|
|
||||||
|
|
||||||
Initializes over.Main and opens/creates the config file (if desired).
|
|
||||||
'''
|
|
||||||
|
|
||||||
self.cfg = CfgAccessor(self)
|
|
||||||
|
|
||||||
|
def __init__(self, name, version=None, license=None, use_cfg_file=False, auto_add_help=True):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.full_name = full_name
|
|
||||||
self.version = version
|
self.version = version
|
||||||
self.license = license
|
self.license = license
|
||||||
self.cmdline = cmdline
|
self.print = text.Output(name)
|
||||||
self.help_texts = [] # (chapter, [paragraphs])
|
self.options = OrderedDict() # level: ndict
|
||||||
self.print = text.Output(self.name + '.main')
|
self.cfg = ConfigRouter(self.options)
|
||||||
|
|
||||||
if xdg_bd:
|
|
||||||
xdg_data = xdg_bd.xdg_data_home
|
|
||||||
xdg_config = xdg_bd.xdg_config_home
|
|
||||||
else:
|
|
||||||
xdg_data = os.path.expanduser('~/.local/share')
|
|
||||||
xdg_config = os.path.expanduser('~/.config')
|
|
||||||
|
|
||||||
self.data_dir = os.path.join(xdg_data, self.name)
|
|
||||||
file.File(os.path.join(self.data_dir, '.keep'))
|
|
||||||
|
|
||||||
if use_cfg_file:
|
if use_cfg_file:
|
||||||
self.cfg_file = file.File(os.path.join(xdg_config, self.name, 'main.cfg'), encoding='utf-8')
|
...
|
||||||
|
|
||||||
if not self.cfg_file.data:
|
if auto_add_help:
|
||||||
self.cfg_file.data = '''# Configuration file for %s
|
self.enable_help("help", "h")
|
||||||
# Licensed under the %s
|
|
||||||
#
|
|
||||||
# Syntax
|
|
||||||
# There are 4 data types: bool, int, float and str (string).
|
|
||||||
# * Bools are exactly True or False.
|
|
||||||
# * Ints are negative integers, positive integers and zero.
|
|
||||||
# * Floats are all real numbers. They don\'t need to have a decimal point.
|
|
||||||
# * Strings need to be enclosed in double quotes, e.g. 'like this'.
|
|
||||||
#
|
|
||||||
# An option is either singular or plural. Plurals are arrays, so they ecpect to be
|
|
||||||
# in comma-separated lists: ['this', 'is', 'a', 'str', 'example'].
|
|
||||||
#
|
|
||||||
# Only lines beginning with a \'#\' are treated as comments.
|
|
||||||
|
|
||||||
''' %(self.name, self.license)
|
|
||||||
|
|
||||||
self.print('creating configuration file §B%s§/' %(self.cfg_file.path), prefix.start)
|
|
||||||
else:
|
|
||||||
self.cfg_file = None
|
|
||||||
|
|
||||||
# cmdline parser and config
|
|
||||||
self.options = [] # Option objects
|
|
||||||
self.targets = [] # target words directly from the command line
|
|
||||||
self.unknowns = [] # parser couldn't figure out how to chew these
|
|
||||||
|
|
||||||
self.help_texts = [] # (chapter, [paragraphs]) tuples
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return 'over.Main(name=%s)' %(self.name)
|
return 'over.app.Main(name="%s")' %(self.name)
|
||||||
|
|
||||||
def exit(self, retval=0, silent=True):
|
def exit(self, rv=0):
|
||||||
'''
|
"""
|
||||||
int retval return value
|
Terminates the program and returns `rv`.
|
||||||
bool silent don't mention it
|
"""
|
||||||
|
|
||||||
Shutdown the program and exit.
|
sys.exit(rv)
|
||||||
'''
|
|
||||||
|
|
||||||
if not silent:
|
def add_option(self, *args, **kwargs):
|
||||||
self.print('exiting with §B%d§/' %(retval), prefix.info)
|
option = Option(*args, **kwargs)
|
||||||
|
|
||||||
sys.exit(retval)
|
if option.level not in self.options:
|
||||||
|
self.options[option.level] = types.ndict()
|
||||||
|
|
||||||
def dump(self, ignore=[]):
|
self.options[option.level][option.name] = option
|
||||||
'''
|
|
||||||
return current configuration in Et Commandline format
|
|
||||||
'''
|
|
||||||
|
|
||||||
out = []
|
def add_doc(self, chapter, paragraphs):
|
||||||
for option in [option for option in self.options if option.name not in ignore]:
|
...
|
||||||
if option.type == 'bool':
|
|
||||||
if option.plural:
|
|
||||||
for value in option.value:
|
|
||||||
if value:
|
|
||||||
out.append('--%s' %(option.name))
|
|
||||||
else:
|
|
||||||
out.append('--no-%s' %(option.name))
|
|
||||||
elif option.value is not None:
|
|
||||||
if option.value:
|
|
||||||
out.append('--%s' %(option.name))
|
|
||||||
else:
|
|
||||||
out.append('--no-%s' %(option.name))
|
|
||||||
else:
|
|
||||||
if option.plural:
|
|
||||||
out.append('--%s' %(option.name))
|
|
||||||
for value in option.value:
|
|
||||||
if option.type == 'str':
|
|
||||||
value = "'%s'" %(value.replace("'", r"\'"))
|
|
||||||
out.append(str(value))
|
|
||||||
out.append('--')
|
|
||||||
elif option.value is not None:
|
|
||||||
out.append('--%s' %(option.name))
|
|
||||||
if option.type == 'str':
|
|
||||||
option.value = "'%s'" %(option.value.replace("'", r"\'"))
|
|
||||||
out.append(str(option.value))
|
|
||||||
|
|
||||||
return out
|
def enable_help(self, name, abbr):
|
||||||
|
"""
|
||||||
|
Map application help to --name and -abbr, and enable library ---help.
|
||||||
|
"""
|
||||||
|
|
||||||
def reset_to_default(self):
|
self.add_option("help", "Display the application configuration view.", self.help, abbr=abbr, in_cfg_file=False)
|
||||||
'''
|
self.add_option("help", "Display the library help and about views.", lambda: self.help(docs=docs.over_desc), level=3, in_cfg_file=False)
|
||||||
Resets internal state to builtin defaults (i.e. as it is before self.parse() is called).
|
|
||||||
'''
|
|
||||||
|
|
||||||
self.targets = []
|
def help(self, option=None, docs=None):
|
||||||
self.unknowns = []
|
"""
|
||||||
|
Displays a help text and exits the program.
|
||||||
|
|
||||||
for option in self.options:
|
If `option` == `docs` == None, full application help and configuration is displayed:
|
||||||
option.source = 'default'
|
- application name, version and license name
|
||||||
option.value = option.default
|
- application description and other documentation supplied with `Main.add_doc`
|
||||||
|
- a list of application options with their descriptions and current values
|
||||||
|
- a list of currently specified targets.
|
||||||
|
|
||||||
def add_option(self, name, dtype, default, description, short_name=None, plural=False, callback=None, use_cfg_file=True, hidden=False):
|
If `option` is set to an option's name, only that option will be described.
|
||||||
'''
|
|
||||||
str name Name of the option. See #0
|
|
||||||
str dtype Option's data type. See #1
|
|
||||||
str default Option's default value. See #2
|
|
||||||
str description Option's description (displayed in --help).
|
|
||||||
str short_name Option's short name. See #3
|
|
||||||
bool plural True to make the option plural.
|
|
||||||
bool use_cfg_file True to use the config file for this option.
|
|
||||||
fn callback Register a callback for this option. See #4
|
|
||||||
bool hidden hide the option from --help
|
|
||||||
|
|
||||||
Add an option to the configuration system.
|
If `docs` is set to something else, only those docs will be displayed.
|
||||||
|
"""
|
||||||
|
|
||||||
#0 Option naming
|
...
|
||||||
Each option has a name. This name is directly used on the command line prefixed by two dashes.
|
|
||||||
For example option 'warp-speed' would be called like so: --warp-speed.
|
|
||||||
Bools take two appearances: --warp-speed is True, while --no-warp-speed is False.
|
|
||||||
Their values can be accessed from the program as main.cfg.option_name. Options with dashes
|
|
||||||
in their names (an operator in python) can be accessed as well, just substitute dashes
|
|
||||||
for underscores. Option --warp-speed would be available under main.cfg.warp_speed.
|
|
||||||
|
|
||||||
#1 Data types.
|
def parse_config(self):
|
||||||
There are four data types.
|
...
|
||||||
* 'bool' - either exactly True or False.
|
|
||||||
* 'str' - a raw text string, nothing special going on here
|
|
||||||
* 'int' - an integer
|
|
||||||
* 'float' - a float
|
|
||||||
|
|
||||||
#2 Default values
|
def parse_cmdline(self, cmdline=None):
|
||||||
These are the values the option will take if it's not called on the command line or
|
cmdline = cmdline or sys.argv[1:]
|
||||||
in the config file. Defaults need to have the proper data type, of course, and for
|
|
||||||
plural options they have to be lists.
|
|
||||||
|
|
||||||
#3 Short names
|
...
|
||||||
Options can have a short, single letter alias. These are prefixed by a single dash on the command
|
|
||||||
line and don't appear in the config file at all. Again, booleans take special treatment: -a is False,
|
|
||||||
while +a is True.
|
|
||||||
Short names can be grouped: -a -b becomes -ab (both are False), or +a +b becomes +ab (both will be True).
|
|
||||||
If an option requires a parameter (e.g. -f filename), it can still be grouped, it just has to be the
|
|
||||||
rightmost option in the group. Whether the group starts with a + or a - doesn't matter.
|
|
||||||
E.g. -af filename is the same as -a -f filename and +af filename is the same as +a -f filename.
|
|
||||||
|
|
||||||
#4 Callbacks
|
def parse(self, cmdline=None):
|
||||||
An option with a callback will invoke it if it appears on the command line (both +a and -a). This is
|
self.parse_config()
|
||||||
usually done for options like --help (internally), --list-modules, --show-something and similar.
|
self.parse_cmdline(cmdline)
|
||||||
The callback will be passed one argument: the main instance.
|
|
||||||
'''
|
|
||||||
|
|
||||||
for option in self.options:
|
|
||||||
if name == option.name:
|
|
||||||
self.print('option §B--§r%s§/ already exists' %(name), prefix.fail)
|
|
||||||
raise RuntimeError
|
|
||||||
|
|
||||||
if option.short_name and short_name == option.short_name:
|
|
||||||
self.print('option §B--§r%s§/\'s short name §r%s§/ is already taken by §B--§y%s§/' %(name, short_name, option.name), prefix.fail)
|
|
||||||
raise RuntimeError
|
|
||||||
|
|
||||||
if short_name and len(short_name) > 1:
|
|
||||||
self.print('short name must be a single character; option §B--§r%s§/ has §r%s§/'%(name, short_name), prefix.fail)
|
|
||||||
raise RuntimeError
|
|
||||||
|
|
||||||
if callback:
|
|
||||||
use_cfg_file = False
|
|
||||||
|
|
||||||
if dtype not in ['bool', 'str', 'int', 'float']:
|
|
||||||
self.print('option §B--§r%s§/\'s dtype is not of a supported type' %(name), prefix.fail)
|
|
||||||
raise RuntimeError
|
|
||||||
|
|
||||||
v = _verify_type(default, dtype, plural)
|
|
||||||
|
|
||||||
if not v:
|
|
||||||
if plural:
|
|
||||||
type_desc = 'a list of §m%s§rs' %(dtype)
|
|
||||||
else:
|
|
||||||
if dtype == 'int':
|
|
||||||
type_desc = 'an int'
|
|
||||||
else:
|
|
||||||
type_desc = 'a %s' %(dtype)
|
|
||||||
|
|
||||||
self.print('option §B--§r%s§/\'s default must be §y%s§/' %(name, type_desc), prefix.fail)
|
|
||||||
raise RuntimeError
|
|
||||||
|
|
||||||
o = Option(name, dtype, default, description, short_name, plural, use_cfg_file, callback, hidden)
|
|
||||||
self.options.append(o)
|
|
||||||
|
|
||||||
def parse(self, cmdline=None, reset_to_default=False):
|
|
||||||
'''
|
|
||||||
Parses the cfg file and the command line and sets the internal cfg state accordingly.
|
|
||||||
'''
|
|
||||||
|
|
||||||
ptr = 0
|
|
||||||
parsing_plural = False
|
|
||||||
unparsed = []
|
|
||||||
callbacks = []
|
|
||||||
|
|
||||||
if reset_to_default:
|
|
||||||
self.reset_to_default()
|
|
||||||
|
|
||||||
if cmdline is None:
|
|
||||||
cmdline = self.cmdline
|
|
||||||
|
|
||||||
while ptr < len(cmdline):
|
|
||||||
to_parse = []
|
|
||||||
word = cmdline[ptr]
|
|
||||||
|
|
||||||
if len(word) > 2 and word[0:2] == '--':
|
|
||||||
to_parse.append(word)
|
|
||||||
elif len(word) >= 2 and (word[0] == '+' or (word[0] == '-' and word[1] != '-')):
|
|
||||||
# expand and then parse
|
|
||||||
for s in word[1:]:
|
|
||||||
option = [opt for opt in self.options if opt.short_name == s]
|
|
||||||
|
|
||||||
if option:
|
|
||||||
if word[0] == '+' or option[0].dtype != 'bool':
|
|
||||||
to_parse.append('--{0.name}'.format(option[0]))
|
|
||||||
else:
|
|
||||||
to_parse.append('--no-{0.name}'.format(option[0]))
|
|
||||||
else:
|
|
||||||
self.unknowns.append(word[0] + s)
|
|
||||||
else:
|
|
||||||
unparsed.append(word)
|
|
||||||
|
|
||||||
ptr += 1
|
|
||||||
|
|
||||||
if to_parse:
|
|
||||||
for w in to_parse:
|
|
||||||
if w.startswith('--no-'):
|
|
||||||
bool_on = False
|
|
||||||
name = w[5:]
|
|
||||||
else:
|
|
||||||
bool_on = True
|
|
||||||
name = w[2:]
|
|
||||||
|
|
||||||
option = [opt for opt in self.options if opt.name == name]
|
|
||||||
|
|
||||||
if option:
|
|
||||||
o = option[0]
|
|
||||||
|
|
||||||
if o.callback:
|
|
||||||
callbacks.append(o.callback)
|
|
||||||
|
|
||||||
if o.plural and o.source != 'cmdline':
|
|
||||||
o.value = []
|
|
||||||
|
|
||||||
if o.dtype == 'bool':
|
|
||||||
o.source = 'cmdline'
|
|
||||||
|
|
||||||
if o.plural:
|
|
||||||
o.value.append(bool_on)
|
|
||||||
else:
|
|
||||||
o.value = bool_on
|
|
||||||
else:
|
|
||||||
if not bool_on:
|
|
||||||
self.print('§B--no-§r%s§/ makes no sense because this option is of type §y%s§/' %(name, o.dtype), prefix.fail)
|
|
||||||
raise RuntimeError
|
|
||||||
|
|
||||||
# This option takes arguments. These are all words at ptr and higher that don't begin with '--'.
|
|
||||||
# If this option is part of a shortgroup it must be at its end.
|
|
||||||
|
|
||||||
if to_parse.index(w) + 1 != len(to_parse):
|
|
||||||
self.print('option §B{0[0]}§r{1.short_name}§/ needs to be at the end of group §y{0}§/'.format(word, o), prefix.fail)
|
|
||||||
raise RuntimeError
|
|
||||||
|
|
||||||
got_something = False
|
|
||||||
|
|
||||||
# NOTE it shouldn't be hard to implement specific plurality (how many words to chew)
|
|
||||||
# NOTE REPLY: I'm still not doing it.
|
|
||||||
while ptr < len(cmdline) and not cmdline[ptr].startswith('--'):
|
|
||||||
arg = cmdline[ptr]
|
|
||||||
ptr += 1
|
|
||||||
|
|
||||||
if o.dtype == 'int':
|
|
||||||
try:
|
|
||||||
arg = int(arg)
|
|
||||||
except ValueError:
|
|
||||||
self.print('argument §r%s§/ passed to integer option §B--§y%s§/' %(arg, o.name), prefix.fail, exc=ValueError)
|
|
||||||
|
|
||||||
elif o.dtype == 'float':
|
|
||||||
try:
|
|
||||||
arg = float(arg)
|
|
||||||
except ValueError:
|
|
||||||
self.print('argument §r%s§/ passed to float option §B--§y%s§/' %(arg, o.name), prefix.fail, exc=ValueError)
|
|
||||||
|
|
||||||
got_something = True
|
|
||||||
o.source = 'cmdline'
|
|
||||||
|
|
||||||
if o.plural:
|
|
||||||
o.value.append(arg)
|
|
||||||
else:
|
|
||||||
o.value = arg
|
|
||||||
break
|
|
||||||
|
|
||||||
#if not got_something:
|
|
||||||
# self.print('option §B--§r%s§/ needs at least one argument' %(o.name), prefix.fail, exc=GeneralError)
|
|
||||||
|
|
||||||
else:
|
|
||||||
self.unknowns.append(w)
|
|
||||||
|
|
||||||
self.targets = [u for u in unparsed if u != '--']
|
|
||||||
|
|
||||||
if self.unknowns:
|
|
||||||
self.print('unknown options on the command line: §r%s§/' %('§/, §r'.join(self.unknowns)), prefix.fail)
|
|
||||||
raise RuntimeError
|
|
||||||
|
|
||||||
# parse the config file
|
|
||||||
if self.cfg_file:
|
|
||||||
new_lines = []
|
|
||||||
|
|
||||||
for opt in [o for o in self.options if o.use_cfg_file]:
|
|
||||||
match = re.findall('^(#?)\s*%s\s*=\s*(.+)' %(opt.name), self.cfg_file.data, re.M)
|
|
||||||
|
|
||||||
if match:
|
|
||||||
if opt.source == 'default':
|
|
||||||
if not match[0][0]: # if it isn't commented out
|
|
||||||
d = match[0][1]
|
|
||||||
|
|
||||||
opt.source = 'config'
|
|
||||||
|
|
||||||
if opt.plural:
|
|
||||||
opt.value = []
|
|
||||||
|
|
||||||
if opt.dtype != 'str':
|
|
||||||
elements = [x.strip() for x in d[1:-1].split(',')]
|
|
||||||
else:
|
|
||||||
d = re.sub('\',\s*\'', '\',\'', d) # remove spaces
|
|
||||||
d = d[1:-1].strip()[1:-1] # remove [] and outermost ''
|
|
||||||
elements = ['\'%s\'' %(x) for x in d.split('\',\'')] # split and re-pack them with ''
|
|
||||||
|
|
||||||
for element in elements:
|
|
||||||
try:
|
|
||||||
opt.value.append(_parse(element, opt.dtype))
|
|
||||||
except RuntimeError:
|
|
||||||
self.print('config file syntax error for option §B--§r%s§/' %(opt.name), prefix.fail)
|
|
||||||
raise
|
|
||||||
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
opt.value = _parse(d, opt.dtype)
|
|
||||||
except RuntimeError:
|
|
||||||
self.print('config file syntax error for option §B--§r%s§/' %(opt.name), prefix.fail)
|
|
||||||
raise
|
|
||||||
|
|
||||||
else:
|
|
||||||
self.print('updating config file with option §B--§y%s§/' %(opt.name))
|
|
||||||
new_lines.append('')
|
|
||||||
new_lines.append(text.paragraph(text.render(opt.description, colors=False), prefix='#', width=80))
|
|
||||||
new_lines.append('# *** data type: %s' %(opt.dtype))
|
|
||||||
|
|
||||||
if opt.plural:
|
|
||||||
new_lines.append('# *** plural option')
|
|
||||||
|
|
||||||
tmp = []
|
|
||||||
for v in opt.default:
|
|
||||||
if opt.dtype == 'str':
|
|
||||||
tmp.append('\'%s\'' %(v))
|
|
||||||
else:
|
|
||||||
tmp.append(str(v))
|
|
||||||
|
|
||||||
str_value = '[%s]' %(', '.join(tmp))
|
|
||||||
else:
|
|
||||||
if opt.dtype == 'str' and opt.default is not None:
|
|
||||||
str_value = '\'%s\'' %(opt.default)
|
|
||||||
else:
|
|
||||||
str_value = str(opt.default)
|
|
||||||
|
|
||||||
new_lines.append('#%s = %s\n' %(opt.name, str_value))
|
|
||||||
|
|
||||||
self.cfg_file.data += '\n'.join(new_lines)
|
|
||||||
|
|
||||||
# handle callbacks
|
|
||||||
for callback in callbacks:
|
|
||||||
callback(self)
|
|
||||||
|
|
||||||
def enable_help(self, short_name=None):
|
|
||||||
'''
|
|
||||||
Enable --help, --over-help and --help-over. If a short name is provided, it will be used for --help.
|
|
||||||
'''
|
|
||||||
|
|
||||||
self.add_option('help', 'bool', False, 'Display this help message.', callback=self.help, short_name=short_name, use_cfg_file=False)
|
|
||||||
self.add_option('over-help', 'bool', False, 'Display general usage information for over.core.', callback=self.help_over, use_cfg_file=False)
|
|
||||||
self.add_option('help-over', 'bool', False, 'Display general usage information for over.core.', callback=self.help_over, use_cfg_file=False, hidden=True)
|
|
||||||
|
|
||||||
def add_help(self, chapter, paragraphs):
|
|
||||||
self.help_texts.append((chapter, paragraphs))
|
|
||||||
|
|
||||||
def help(self, *dummy):
|
|
||||||
_print_help(self, self.help_texts, list_options=True)
|
|
||||||
|
|
||||||
def help_over(self, *dummy):
|
|
||||||
_print_help(self, _over_help_texts)
|
|
||||||
|
|
||||||
# --------------------------------------------------
|
|
||||||
|
|
||||||
class Option:
|
|
||||||
def __init__(self, name, dtype, default, description, short_name, plural, use_cfg_file, callback, hidden):
|
|
||||||
'''
|
|
||||||
Just a container. Move along...
|
|
||||||
'''
|
|
||||||
|
|
||||||
self.name = name
|
|
||||||
self.dtype = dtype # bool, int, str, float
|
|
||||||
self.default = default
|
|
||||||
self.description = description
|
|
||||||
self.short_name = short_name
|
|
||||||
self.plural = plural
|
|
||||||
self.use_cfg_file = use_cfg_file
|
|
||||||
self.callback = callback
|
|
||||||
self.hidden = hidden
|
|
||||||
|
|
||||||
self.source = 'default'
|
|
||||||
self.value = default
|
|
||||||
|
|
27
over/docs.py
Normal file
27
over/docs.py
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
#! /usr/bin/env python3
|
||||||
|
# encoding: utf-8
|
||||||
|
|
||||||
|
over_desc = [
|
||||||
|
('over.core: what, how and huh?', ['over.core is a Python 3 module that provides basic functionality for programs. Functionality such as configuration, commandline parsing, text handling and output, file handling, a non-interactive help system and (not all that much) more.']),
|
||||||
|
('Data Types', ['over.core currently supports 4 data types.', 'A §bbool§/ is either exactly §mTrue§/ or exactly §mFalse§/. Bool options that are §mTrue§/ look like this: §B--§goption§/. The same name only §mFalse§/ would be §B--no-§goption§/. Makes sense, doesn\'t it? Their short versions are either §B+§go§/ for §mTrue§/ or §B-§go§/ for §mFalse§/.', 'A §bstr§/ing is just any raw text. Remember to enclose it in quotes if it has spaces or other nasty characters in it.', 'An §bint§/eger is a whole number. Negative, zero, positive.', 'Finally, a §bfloat§/ is any real number.']),
|
||||||
|
('The Commandline Parser', ['This, in conjunction with the configuration system, is the strongest part of over.core and the very reason for its continued existence.', 'Each configurable option can be assigned to on the command line. Take an option named §Bres-file§/ as an example. To assign to it, you can use §B--§gres-file§/ §msomething§/ (it\'s case sensitive). That\'s it, now its value is §msomething§/! Pretty easy, right?', 'Now, you\'re probably thinking: \'I\'m not typing that all over again!\' Well, you don\'t have to! Options can have their short names. It\'s a single letter (again, case sensitive) with a plus or minus sign in front of it. So §B--§gres-file§/ §msomething§/ becomes §B+§gf§/ §msomething§/. That\'s much better, ain\'t it? And there\'s more. Short names can be grouped together. If you have a bunch of bool switches, like §B--§garmed§/ (short: §B+§gA§/), §B--no-§gsound§/ (short: §B-§gS§/), §B--no-§gstore§/ (short: §B-§gs§/) and §B--§gforce§/ (short: §B+§gF§/), you can group their shorts into groups with the same boolean value: §B-§gSs§/ §B+§gAF§/. You can even have a non-bool option in a group, it just has to be on the very right (because it needs to be followed by data): §B-§gSs§/ §B+§gAFf§/ §msomething§/. It doesn\'t matter if that group begins with §B+§/ or §B-§/.', 'If you use an option more than once, its last (rightmost) occurence applies. For example, after §B+§gf§/ §msomething§/ §B+§gf§/ §mor_other§/ is parsed, option §Bres-file§/ holds the value §mor_other§/. It goes the same for bools: after §B+§gA§/ §B--no-§garmed§/ §B+§gA§/, §Barmed§/ is §mTrue§/. However, if an option is §cplural§/, all occurences are used. Sequence §B+§gA§/ §B-§gA§/ §B+§gA§/ would be [§mTrue§/, §mFalse§/, §mTrue§/], and §B--§gnum§/ §m1§/ §B--§gnum§/ §m2§/ would end up looking like [§m1§/, §m2§/]. You don\'t need to type §B--§gnum§/ for every field either: §B--§gnum§/ §m1§/ §m2§/ would work exactly the same. That\'s because the parser keeps reading everything after a plural option (that takes data, i.e. not bools) right until it encounters two dashes, like those of a following option. You can use just the two dashes to stop the parsing manually, usually when you don\'t want to follow with a long option. Example: §B--§gnum§/ §m1§/ §m5§/ §m9§/ §B--§/. I repeat: a plural (non-bool) option needs to be terminated by two dashes. This wouldn\'t work: §B--§gnum§/ §m1§/ §m5§/ §m9§/ §B+§gf§/ §mthat_one§/, everything after §B--§gnum§/ would be consumed as its data, §Bincluding +§gf§/ §mthat_one§/.']),
|
||||||
|
('The Config File', ['If enabled in the program code, a config file will be generated when you first run it. By default, everything in the config file will be filled with default values and commented out. The general syntax as well as individual options are explained inside. If you run a newer version of the program that offers more configurable options, the config file will be automatically updated. If you\'d like to modify an option in the config file, first uncomment it and then change its value. Resolving order for option values is 1) default (hardcoded), 2) config file and 3) commandline arguments.']),
|
||||||
|
('Other', ['When enabled in the program, an over.core program offers two options: §B--§ghelp§/ and §B--§gover-help§/. The first describes the program and its options, including their types, current values and whether is their current value coming from defaults, config files or the command line. The second option displays the help you\'re reading right now. You may now guess which rank I hold in the Obvious Corps.', 'As the brighter amongst you might have noticed, over.core likes colors. A lot. Generally, I use blue for §bdata types§/, magenta for §mvalues§/, white and green for §B--§goptions§/ and reserve red and yellow for when §rshit hits§/ §ythe fan§/, where red letters usually tell you §Bwhy§/ and yellow ones tell you §Bwhat§/.', 'And it\'s not just colors! I like money, too. Push donations to §B1sekErApM4zh35RFW7qGWs5Yeo9EYWjyV§/, or catch me somewhere and force me to accept cash.'])
|
||||||
|
]
|
||||||
|
|
||||||
|
config_header = """# Configuration file for %s-%s
|
||||||
|
# Created using over-%s
|
||||||
|
#
|
||||||
|
# Syntax
|
||||||
|
# There are 4 data types: bool, int, float and str (string).
|
||||||
|
# * Bools are exactly True or False.
|
||||||
|
# * Ints are negative integers, positive integers and zero.
|
||||||
|
# * Floats are all real numbers. They don\'t need to have a decimal point.
|
||||||
|
# * Strings need to be enclosed in double quotes, e.g. 'like this'.
|
||||||
|
#
|
||||||
|
# An option is either singular or plural. Plurals are arrays, so they ecpect to be
|
||||||
|
# in comma-separated lists: ['this', 'is', 'a', 'str', 'example'].
|
||||||
|
#
|
||||||
|
# Only lines beginning with a \'#\' are treated as comments.
|
||||||
|
|
||||||
|
""" %(0, 0, 0)
|
Loading…
Add table
Add a link
Reference in a new issue