first commit

This commit is contained in:
Emmanuel Garette 2018-11-29 22:10:08 +01:00
parent 9376c866d6
commit 226a509d53
35 changed files with 526 additions and 0 deletions

179
examples/Hangman/hangman.py Normal file
View file

@ -0,0 +1,179 @@
#!/usr/bin/env python3
"""Hangman example
"""
from random import choice
import unicodedata
import re
from os import unlink
from os.path import isfile
from tiramisu import RegexpOption, OptionDescription, Config, IntOption, UnicodeOption, BoolOption, ParamOption, Params
from tiramisu.storage import storage_type
from tiramisu.storage.sqlite3.storage import SETTING
from tiramisu_parser import TiramisuParser
LANG = 'fr_FR'
DICT_FILE = '/usr/share/myspell/{}.dic'.format(LANG)
WORD_REGEXP = re.compile(r'^[a-z]{7,12}$')
PROPOSALS_LEN = 27
NB_PROPOSALS = 6
def remove_accent(word):
"""remove all accent"""
word = unicodedata.normalize('NFD', word)
return word.encode('ascii', 'ignore').decode()
def get_random_word():
"""get line randomly in myspell file
"""
with open(DICT_FILE, 'r') as file_content:
word = choice(file_content.readlines()).strip()
if word.endswith('/S.'):
word = word[:-3]
if word.endswith('/X.'):
word = word[:-3]
if word.endswith('/F.'):
word = word[:-3]
if word.endswith('/a0p+') or word.endswith('/d0p+') or word.endswith('/a3p+'):
word = word[:-5]
if not WORD_REGEXP.search(remove_accent(word)):
return get_random_word()
return word
def display_uncomplete_word(word, *proposals):
"""display response with proposals
"""
if display_proposals_left(display_misses(word, *proposals)) == 0:
return word
display = ['-'] * len(word)
for idx, char in enumerate(remove_accent(word)):
if char in proposals:
display[idx] = word[idx]
return ''.join(display)
def display_misses(word, *proposals):
"""display all proposals
"""
ret = list(set(proposals) - set(list(remove_accent(word))))
if None in ret:
ret.remove(None)
ret.sort()
return ' '.join(ret)
def validate_misses(misses):
if display_proposals_left(misses) == 0:
raise ValueError('No more guest possible')
def display_proposals_left(misses):
if not misses:
return NB_PROPOSALS
return max(NB_PROPOSALS - len(misses.split(' ')), 0)
def display_proposal(word, *proposals):
if display_uncomplete_word(word, *proposals) == word:
return False
return display_proposals_left(display_misses(word, *proposals)) != 0
class ProposalOption(RegexpOption):
__slots__ = tuple()
_regexp = re.compile(r'^[a-z]$')
_display_name = 'proposal'
def main():
options = []
proposal = None
word = UnicodeOption('word',
'Word',
properties=('hidden', 'force_store_value'),
callback=get_random_word)
proposals = [ParamOption(word)]
for idx in range(PROPOSALS_LEN):
requires = [{'option': 'self',
'expected': None,
'action': 'hidden',
'inverse': True}]
if proposal is not None:
display = BoolOption('display{}'.format(idx),
'Display {}'.format(idx),
properties=('hidden',),
callback=display_proposal,
callback_params=Params(tuple(proposals)))
options.append(display)
requires.append({'option': proposal,
'expected': None,
'action': 'disabled'})
requires.append({'option': display,
'expected': False,
'action': 'disabled'})
proposal = ProposalOption('guess{}'.format(idx),
'Guess {}'.format(idx),
requires=requires,
properties=('positional',))
#FIXME maximum recursion ...
#if proposals:
# proposal.impl_add_consistency('not_equal', proposals[0])
proposals.append(ParamOption(proposal, True))
options.append(proposal)
#
proposal_word = UnicodeOption('proposal_word',
'Word',
properties=('frozen',),
callback=display_uncomplete_word,
callback_params=Params(tuple(proposals)))
misses = UnicodeOption('misses',
'Misses',
properties=('frozen',),
callback=display_misses,
callback_params=Params(tuple(proposals)),
validator=validate_misses)
proposals_left = IntOption('proposals_left',
'Proposals left',
properties=('frozen',),
callback=display_proposals_left,
callback_params=Params(ParamOption(misses)))
#descr = OptionDescription('proposals',
# 'Suggesting letters',
# options)
storage_type.set('sqlite3')
config = Config(OptionDescription('root', 'root', [word, proposal_word, misses, proposals_left] + options), persistent=True, session_id='hangman')
parser = TiramisuParser()
parser.add_arguments(config)
try:
parser.parse_args()
except ValueError:
pass
config = parser.get_config()
filename = '{}/tiramisu.db'.format(SETTING.dir_database)
lost = False
for name in ['proposal_word', 'misses', 'proposals_left']:
option = config.option(name)
try:
value = option.value.get()
print('{}: {}'.format(option.option.doc(), value))
except ValueError as err:
lost = True
err.prefix = ''
print(option.option.doc(), str(err))
if isfile(filename):
unlink(filename)
if not lost and \
config.option('proposal_word').value.get() == config.forcepermissive.option('word').value.get():
print('You win')
if isfile(filename):
unlink(filename)
if __name__ == "__main__":
main()

View file

@ -0,0 +1 @@
parser.add_argument("echo", help="echo the string you use here")

View file

@ -0,0 +1 @@
parser.add_arguments(StrOption('echo', 'echo the string you use here', properties=('mandatory', 'positional')))

View file

@ -0,0 +1 @@
parser.add_argument("echo", help="echo the string you use here", default='blah', nargs='?')

View file

@ -0,0 +1 @@
parser.add_arguments(StrOption('echo', 'echo the string you use here', properties=('mandatory', 'positional'), default='blah'))

View file

@ -0,0 +1,2 @@
parser.add_argument("square", help="display a square of a given number",
type=int)

View file

@ -0,0 +1 @@
parser.add_arguments(IntOption('square', 'display a square of a given number', properties=('mandatory', 'positional')))

View file

@ -0,0 +1 @@
parser.add_argument("echo", help="echo the string you use here", nargs='+')

View file

@ -0,0 +1 @@
parser.add_arguments(StrOption('echo', 'echo the string you use here', properties=('mandatory', 'positional'), multi=True))

View file

@ -0,0 +1 @@
parser.add_argument('--verbosity', help='increase output verbosity', action='store_true')

View file

@ -0,0 +1 @@
parser.add_arguments(BoolOption('verbosity', 'increase output verbosity', default=False))

View file

@ -0,0 +1 @@
parser.add_argument('--verbosity', help='increase output verbosity', action='store_false')

View file

@ -0,0 +1 @@
parser.add_arguments(BoolOption('verbosity', 'increase output verbosity', default=True))

View file

@ -0,0 +1,2 @@
parser.add_argument('--door', help='Door numbers', choices=['1', '2', '3'])

View file

@ -0,0 +1 @@
parser.add_arguments(ChoiceOption('door', 'Door numbers', ('1', '2', '3')))

View file

@ -0,0 +1,2 @@
parser.add_argument('--int', help='integer', type=int)

View file

@ -0,0 +1 @@
parser.add_arguments(IntOption('int', 'integer'))

View file

@ -0,0 +1,2 @@
parser.add_argument('--foo', help='foo help')

View file

@ -0,0 +1,2 @@
parser.add_arguments(StrOption('foo', 'foo help'))

View file

@ -0,0 +1,2 @@
parser.add_argument('--foo', help='foo help', nargs='*')

View file

@ -0,0 +1,2 @@
parser.add_arguments(StrOption('foo', 'foo help', multi=True))

View file

@ -0,0 +1,2 @@
parser.add_argument('--door', help='Door numbers', choices=[1, 2, 3])

View file

@ -0,0 +1 @@
parser.add_arguments(ChoiceOption('door', 'Door numbers', (1, 2, 3)))

View file

@ -0,0 +1,2 @@
parser.add_argument('--foo', help='foo help', default='default', nargs='?')

View file

@ -0,0 +1,2 @@
parser.add_arguments(StrOption('foo', 'foo help', 'default'))

View file

@ -0,0 +1 @@
parser.add_argument('-f', '--foo', help='foo help')

View file

@ -0,0 +1,3 @@
str_long = StrOption('foo', 'foo help')
str_short = SymLinkOption('f', str_long)
parser.add_arguments([str_long, str_short])

View file

@ -0,0 +1,2 @@
parser.add_argument('-v', help='increase output verbosity', action='store_true')
parser.add_argument('-s', help='second argument', action='store_true')

View file

@ -0,0 +1 @@
parser.add_arguments([BoolOption('v', 'increase output verbosity', default=False), BoolOption('s', 'second argument', default=False)])

View file

@ -0,0 +1,3 @@
parser.add_argument("echo", help="echo the string you use here")
parser.add_argument('--verbosity', help='increase output verbosity', action='store_true')

View file

@ -0,0 +1,3 @@
parser.add_arguments([StrOption('echo', 'echo the string you use here', properties=('mandatory', 'positional')),
BoolOption('verbosity', 'increase output verbosity', default=False)])

View file

@ -0,0 +1,2 @@
parser.add_argument('-v', help='increase output verbosity', action='store_true')
parser.add_argument('-i', '--int', help='integer', type=int)

View file

@ -0,0 +1,4 @@
int_long = IntOption('int', 'integer')
parser.add_arguments([BoolOption('v', 'increase output verbosity', default=False),
int_long,
SymLinkOption('i', int_long)])

145
test/test_simple.py Normal file
View file

@ -0,0 +1,145 @@
from tiramisu import StrOption, BoolOption, IntOption, ChoiceOption, OptionDescription, SymLinkOption
from py.test import raises, fixture
from io import StringIO
import sys
from os import listdir
from os.path import join, isdir
from contextlib import redirect_stdout, redirect_stderr
from argparse import ArgumentParser
#from pouet import TiramisuParser
from tiramisu_parser import TiramisuParser
DATA_DIR = 'test/data/compare'
TEST_DIRS = []
for test in listdir(DATA_DIR):
test_file = join(DATA_DIR, test)
if isdir(test_file):
TEST_DIRS.append(test_file)
TEST_DIRS.sort()
# TEST_DIRS.remove('test/data/compare/10_positional_list')
# TEST_DIRS = ['test/data/compare/50_conditional_disable']
@fixture(scope="module", params=TEST_DIRS)
def test_list(request):
return request.param
def import_subfile_and_test(filename, parser, arg):
parser_dict = []
parser_system_err = []
f = StringIO()
with redirect_stderr(f):
exec(open(filename).read())
# print('arg', arg)
try:
parser_dict.append(parser.parse_args(arg).__dict__)
except SystemExit as err:
parser_system_err.append(str(err))
else:
parser_system_err.append(None)
parser_error = f.getvalue()
f = StringIO()
with redirect_stdout(f):
parser.print_help()
parser_help = f.getvalue()
return parser_dict, parser_system_err, parser_error, parser_help
def test_files(test_list):
args = [[],
# 10_positional
['bar'], ['foo', 'bar'],
# 10_positional_int
['4'],
# 20_bool
['--verbosity'], ['--verbosity', 'arg'],
# 20_string
['--foo'], ['--foo', '--bar'], ['--foo', 'a'],
['--foo', 'a', '--foo', 'b'],
# 20_int
['--int', '3'], ['--int', 'a'],
# 20 choice
['--door', 'a'], ['--door', '1'],
# 30_string_short
['-f', 'b'], ['--foo', 'c', '-f', 'b'],
# 40 multi_bool
['-v'], ['-v', '-s'], ['-vs'],
# 40_short_long
['-v', '--foo', '1'], ['-vf', '2'], ['-vf'], ['-vf', '-v'],
# 40_positional_optional
['bar', '--verbosity'], ['--verbosity', 'bar'],
]
for arg in args:
tiramparser = TiramisuParser('prog.py')
tiramparser_dict, tiramparser_system_err, tiramparser_error, tiramparser_help = import_subfile_and_test(test_list + '/tiramisu.py',
tiramparser, arg)
#
argparser = ArgumentParser('prog.py')
argparser_dict, argparser_system_err, argparser_error, argparser_help = import_subfile_and_test(test_list + '/argparse.py',
argparser, arg)
#print(tiramparser_dict)
#print(tiramparser_system_err)
#print(tiramparser_error)
#print(tiramparser_help)
#print('-----')
#print(argparser_dict)
#print(argparser_system_err)
#print(argparser_error)
#print(argparser_help)
assert tiramparser_dict == argparser_dict
assert tiramparser_error == argparser_error
assert tiramparser_help == argparser_help
assert tiramparser_system_err == argparser_system_err
#FIXME --verbose sans --quiet
#parser = argparse.ArgumentParser(description="calculate X to the power of Y")
#group = parser.add_mutually_exclusive_group()
#group.add_argument("-v", "--verbose", action="store_true")
#group.add_argument("-q", "--quiet", action="store_true")
#parser.add_argument("x", type=int, help="the base")
#parser.add_argument("y", type=int, help="the exponent")
#args = parser.parse_args()
#answer = args.x**args.y
#FIXME --sum ?
#parser = argparse.ArgumentParser(description='Process some integers.')
#parser.add_argument('integers', metavar='N', type=int, nargs='+',
# help='an integer for the accumulator')
#parser.add_argument('--sum', dest='accumulate', action='store_const',
# const=sum, default=max,
# help='sum the integers (default: find the max)')
#args = parser.parse_args()
#print(args.accumulate(args.integers))
# +++++++++++++++++++++++++++++ nargs
#FIXME longueur fixe
#>>> parser = argparse.ArgumentParser()
#>>> parser.add_argument('--foo', nargs=2)
#>>> parser.add_argument('bar', nargs=1)
#>>> parser.parse_args('c --foo a b'.split())
#Namespace(bar=['c'], foo=['a', 'b'])
#FIXME const
#>>> parser = argparse.ArgumentParser()
#>>> parser.add_argument('--foo', nargs='?', const='c', default='d')
#>>> parser.add_argument('bar', nargs='?', default='d')
#>>> parser.parse_args(['XX', '--foo', 'YY'])
#Namespace(bar='XX', foo='YY')
#>>> parser.parse_args(['XX', '--foo'])
#Namespace(bar='XX', foo='c')
#>>> parser.parse_args([])
#Namespace(bar='d', foo='d')
#FIXME ? | * | +
# * == list
# + == list + mandatory

149
tiramisu_parser.py Normal file
View file

@ -0,0 +1,149 @@
from typing import Union, List
from argparse import ArgumentParser, Namespace, SUPPRESS
from tiramisu import Option, OptionDescription, Config, BoolOption, StrOption, IntOption, \
ChoiceOption, SymLinkOption
from tiramisu.error import PropertiesOptionError
class TiramisuNamespace(Namespace):
def _populate(self):
for tiramisu_key, tiramisu_value in self._config.value.dict().items():
option = self._config.option(tiramisu_key)
if not isinstance(option.option.get(), SymLinkOption):
if tiramisu_value == [] and option.option.ismulti() and option.owner.isdefault():
tiramisu_value = None
super().__setattr__(tiramisu_key, tiramisu_value)
def __init__(self, config):
self._config = config
super().__init__()
def __setattr__(self, key, value):
if key == '_config':
super().__setattr__(key, value)
return
self._config.property.read_write()
option = self._config.option(key)
if option.option.ismulti() and value is not None and not isinstance(value, list):
value = [value]
option.value.set(value)
def __getattribute__(self, key):
if key == '__dict__' and hasattr(self, '_config'):
self._config.property.read_only()
self._populate()
self._config.property.read_write()
return super().__getattribute__(key)
class TiramisuParser(ArgumentParser):
def __init__(self, *args, **kwargs):
self.config = None
super().__init__(*args, **kwargs)
def _match_arguments_partial(self, actions, arg_string_pattern):
# used only when check first proposal for first value
# we have to remove all actions with propertieserror
# so only first settable option will be returned
actions_pop = []
for idx, action in enumerate(actions):
if self.config.unrestraint.option(action.dest).property.get(only_raises=True):
actions_pop.append(idx)
else:
break
for idx in actions_pop:
actions.pop(0)
return super()._match_arguments_partial(actions, arg_string_pattern)
def add_argument(self, *args, **kwargs):
if args == ('-h', '--help'):
super().add_argument(*args, **kwargs)
else:
raise NotImplementedError('do not use add_argument')
def add_arguments(self, tiramisu: Union[Config, Option, List[Option], OptionDescription]) -> None:
if not isinstance(tiramisu, Config):
if not isinstance(tiramisu, OptionDescription):
if isinstance(tiramisu, Option):
tiramisu = [tiramisu]
tiramisu = OptionDescription('root', 'root', tiramisu)
tiramisu = Config(tiramisu)
self.config = tiramisu
actions = {}
for obj in tiramisu.unrestraint.option.list():
if obj.option.properties(only_raises=True) or 'frozen' in obj.option.properties():
continue
option = obj.option
tiramisu_option = option.get()
name = option.name()
if name.startswith(self.prefix_chars):
raise ValueError('name cannot startswith "{}"'.format(self.prefix_chars))
properties = obj.property.get()
kwargs = {'help': option.doc(),
'default': SUPPRESS}
if 'positional' in properties:
#if not 'mandatory' in properties:
# raise ValueError('"positional" argument must be "mandatory" too')
args = [name]
if option.requires():
kwargs['nargs'] = '?'
else:
if len(name) == 1 and 'longargument' not in properties:
args = [self.prefix_chars + name]
else:
args = [self.prefix_chars * 2 + name]
if 'mandatory' in properties:
kwargs['required'] = True
if isinstance(tiramisu_option, BoolOption):
if 'mandatory' in properties:
raise ValueError('"mandatory" property is not allowed for BoolOption')
#if not isinstance(option.default(), bool):
# raise ValueError('default value is mandatory for BoolOption')
if option.default() is False:
action = 'store_true'
else:
action = 'store_false'
kwargs['action'] = action
else:
if option.default() not in [None, []]:
#kwargs['default'] = kwargs['const'] = option.default()
#kwargs['action'] = 'store_const'
kwargs['nargs'] = '?'
if option.ismulti():
if 'mandatory' in properties:
kwargs['nargs'] = '+'
else:
kwargs['nargs'] = '*'
if isinstance(tiramisu_option, StrOption):
pass
elif isinstance(tiramisu_option, IntOption):
kwargs['type'] = int
elif isinstance(tiramisu_option, SymLinkOption):
tiramisu_option = tiramisu_option.impl_getopt()
actions[tiramisu_option.impl_getname()][0].insert(0, args[0])
continue
elif isinstance(tiramisu_option, ChoiceOption):
kwargs['choices'] = obj.value.list()
else:
pass
#raise NotImplementedError('not supported yet')
actions[option.name()] = (args, kwargs)
for args, kwargs in actions.values():
super().add_argument(*args, **kwargs)
def parse_args(self, *args, **kwargs):
kwargs['namespace'] = TiramisuNamespace(self.config)
try:
namespaces = super().parse_args(*args, **kwargs)
except PropertiesOptionError as err:
# import traceback
# traceback.print_exc()
if err.proptype == ('mandatory',):
self.error('the following arguments are required: {}'.format(err._option_bag.option.impl_getname()))
else:
self.error('unexpected error: {}'.format(err))
del namespaces.__dict__['_config']
return namespaces
def get_config(self):
return self.config