Compare commits
20 commits
2384d88336
...
6d01f9ffee
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d01f9ffee | ||
|
|
52d96c2c51 | ||
|
|
15eb20ae11 | ||
|
|
c6f7dc8206 | ||
|
|
295d7f912e | ||
|
|
e61e064c41 | ||
|
|
6de205b260 | ||
|
|
694d9df04b | ||
|
|
e2951f1a21 | ||
|
|
195abb4299 | ||
|
|
285d7316be | ||
|
|
be4aba17dc | ||
|
|
3372a42524 | ||
|
|
b026f9755c | ||
|
|
ba1dc5d8c4 | ||
|
|
d963750cf1 | ||
|
|
7e8613e6fb | ||
|
|
4c528c0a17 | ||
|
|
82d8186062 | ||
|
|
2b3470af3b |
40 changed files with 939 additions and 1 deletions
5
AUTHORS
Normal file
5
AUTHORS
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
Authors
|
||||
--------
|
||||
|
||||
Emmanuel Garette <gnunux@gnunux.info> lead developer
|
||||
|
||||
165
README.md
165
README.md
|
|
@ -1,3 +1,166 @@
|
|||
# tiramisu-cmdline-parser
|
||||
|
||||
Python3 parser for command-line options and arguments using Tiramisu engine.
|
||||
Python3 parser for command-line options and arguments using Tiramisu engine.
|
||||
|
||||
# example
|
||||
|
||||
Let us start with a simple example
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from tiramisu_cmdline_parser import TiramisuCmdlineParser
|
||||
from tiramisu import IntOption, StrOption, BoolOption, ChoiceOption, \
|
||||
SymLinkOption, OptionDescription, Config
|
||||
# build a Config with:
|
||||
# * a choice for select a sub argument (str, list, int)
|
||||
choiceoption = ChoiceOption('cmd',
|
||||
'choice the sub argument',
|
||||
('str', 'list', 'int'),
|
||||
properties=('mandatory',
|
||||
'positional'))
|
||||
# * a boolean to pass script in verbosity mode with argument -v --verbosity
|
||||
booloption = BoolOption('verbosity',
|
||||
'increase output verbosity',
|
||||
default=False)
|
||||
short_booloption = SymLinkOption('v', booloption)
|
||||
# * a string option if cmd is 'str'
|
||||
str_ = StrOption('str',
|
||||
'string option',
|
||||
properties=('mandatory',),
|
||||
requires=[{'option': choiceoption,
|
||||
'expected': 'str',
|
||||
'action': 'disabled',
|
||||
'inverse': True}])
|
||||
# * a list of strings option if cmd is 'list'
|
||||
list_ = StrOption('list',
|
||||
'list string option',
|
||||
multi=True,
|
||||
properties=('mandatory',),
|
||||
requires=[{'option': choiceoption,
|
||||
'expected': 'list',
|
||||
'action': 'disabled',
|
||||
'inverse': True}])
|
||||
# * an integer option if cmd is 'int'
|
||||
int_ = IntOption('int',
|
||||
'int option',
|
||||
properties=('mandatory',),
|
||||
requires=[{'option': choiceoption,
|
||||
'expected': 'int',
|
||||
'action': 'disabled',
|
||||
'inverse': True}])
|
||||
# Now build Config
|
||||
config = Config(OptionDescription('root',
|
||||
'root',
|
||||
[choiceoption,
|
||||
booloption,
|
||||
short_booloption,
|
||||
str_,
|
||||
list_,
|
||||
int_
|
||||
]))
|
||||
# initialise the parser
|
||||
config.property.read_write()
|
||||
parser = TiramisuCmdlineParser(config)
|
||||
# parse arguments of current script
|
||||
parser.parse_args()
|
||||
# now, print the result
|
||||
print('result:')
|
||||
config.property.read_only()
|
||||
for key, value in config.value.dict().items():
|
||||
print('- {} ({}): {}'.format(key,
|
||||
config.option(key).option.doc(),
|
||||
value))
|
||||
```
|
||||
|
||||
Let's print help:
|
||||
|
||||
```bash
|
||||
[gnunux@localhost tiramisu-parser]$ python3 prog.py str -h
|
||||
usage: prog.py [-h] [-v] --str STR --list LIST [LIST ...] --int INT
|
||||
{str,list,int}
|
||||
|
||||
positional arguments:
|
||||
{str,list,int} choice the sub argument
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-v, --verbosity increase output verbosity
|
||||
--str STR string option
|
||||
--list LIST [LIST ...]
|
||||
list string option
|
||||
--int INT int option
|
||||
```
|
||||
|
||||
The positional argument 'cmd' is mandatory:
|
||||
|
||||
```bash
|
||||
[gnunux@localhost tiramisu-parser]$ python3 prog.py
|
||||
usage: prog.py [-h] [-v] --str STR --list LIST [LIST ...] --int INT
|
||||
{str,list,int}
|
||||
prog.py: error: the following arguments are required: cmd
|
||||
```
|
||||
|
||||
If 'cmd' is 'str', --str become mandatory:
|
||||
|
||||
```bash
|
||||
[gnunux@localhost tiramisu-parser]$ python3 prog.py str
|
||||
usage: prog.py [-h] [-v] --str STR --list LIST [LIST ...] --int INT
|
||||
{str,list,int}
|
||||
prog.py: error: the following arguments are required: --str
|
||||
```
|
||||
|
||||
If 'cmd' is 'str', cannot set --int argument:
|
||||
|
||||
```bash
|
||||
[gnunux@localhost tiramisu-parser]$ python3 prog.py str --int 3
|
||||
usage: prog.py [-h] [-v] --str STR --list LIST [LIST ...] --int INT
|
||||
{str,list,int}
|
||||
prog.py: error: unrecognized arguments: --int
|
||||
```
|
||||
|
||||
With all mandatories arguments:
|
||||
|
||||
```bash
|
||||
[gnunux@localhost tiramisu-parser]$ python3 prog.py str --str value
|
||||
result:
|
||||
- cmd (choice the sub argument): str
|
||||
- verbosity (increase output verbosity): False
|
||||
- v (increase output verbosity): False
|
||||
- str (string option): value
|
||||
```
|
||||
|
||||
```bash
|
||||
[gnunux@localhost tiramisu-parser]$ python3 prog.py int --int 3
|
||||
result:
|
||||
- cmd (choice the sub argument): int
|
||||
- verbosity (increase output verbosity): False
|
||||
- v (increase output verbosity): False
|
||||
- int (int option): 3
|
||||
```
|
||||
|
||||
```bash
|
||||
[gnunux@localhost tiramisu-parser]$ python3 prog.py list --list a b c
|
||||
result:
|
||||
- cmd (choice the sub argument): list
|
||||
- verbosity (increase output verbosity): False
|
||||
- v (increase output verbosity): False
|
||||
- list (list string option): ['a', 'b', 'c']
|
||||
```
|
||||
|
||||
Add --verbosity argument:
|
||||
|
||||
```bash
|
||||
[gnunux@localhost tiramisu-parser]$ python3 prog.py list --list a b c -v
|
||||
result:
|
||||
- cmd (choice the sub argument): list
|
||||
- verbosity (increase output verbosity): True
|
||||
- v (increase output verbosity): True
|
||||
- list (list string option): ['a', 'b', 'c']
|
||||
[gnunux@localhost tiramisu-parser]$ python3 prog.py list --list a b c --verbosity
|
||||
result:
|
||||
- cmd (choice the sub argument): list
|
||||
- verbosity (increase output verbosity): True
|
||||
- v (increase output verbosity): True
|
||||
- list (list string option): ['a', 'b', 'c']
|
||||
```
|
||||
|
|
|
|||
178
examples/Hangman/example.py
Normal file
178
examples/Hangman/example.py
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
#!/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_cmdline_parser import TiramisuCmdlineParser
|
||||
|
||||
|
||||
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 = TiramisuCmdlineParser(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()
|
||||
32
examples/remote/example.py
Normal file
32
examples/remote/example.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Hangman example
|
||||
"""
|
||||
from json import loads, dumps
|
||||
from urllib.request import urlopen, Request
|
||||
from tiramisu_json_api import Config
|
||||
from tiramisu_cmdline_parser import TiramisuCmdlineParser
|
||||
|
||||
|
||||
class RemoteConfig(Config):
|
||||
def __init__(self,
|
||||
url):
|
||||
json = loads(urlopen(url).read())
|
||||
super().__init__(json)
|
||||
self.url = url
|
||||
|
||||
def send_data(self,
|
||||
updates):
|
||||
request = Request(self.url, dumps(updates).encode(), method='POST')
|
||||
return loads(urlopen(request).read())
|
||||
|
||||
|
||||
def main():
|
||||
config = RemoteConfig('http://localhost:8000')
|
||||
parser = TiramisuCmdlineParser(config)
|
||||
parser.parse_args()
|
||||
config = parser.get_config()
|
||||
print(config.value.dict())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
29
setup.py
Normal file
29
setup.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
import tiramisu_cmdline_parser
|
||||
|
||||
setup(
|
||||
name='tiramisu_cmdline_parser',
|
||||
version=tiramisu_cmdline_parser.__version__,
|
||||
packages=find_packages(),
|
||||
author="Emmanuel Garette & Tiramisu Team",
|
||||
description="command-line parser using Tiramisu.",
|
||||
long_description="""
|
||||
Python3 parser for command-line options and arguments using Tiramisu engine.
|
||||
""",
|
||||
include_package_data=True,
|
||||
url='https://framagit.org/tiramisu/tiramisu-cmdline-parser',
|
||||
|
||||
classifiers=[
|
||||
"Programming Language :: Python",
|
||||
"Development Status :: 1 - Planning",
|
||||
"License :: LGPLv3",
|
||||
"Natural Language :: French",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Topic :: Communications",
|
||||
],
|
||||
)
|
||||
1
test/data/compare/10_positional/argparse.py
Normal file
1
test/data/compare/10_positional/argparse.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
parser.add_argument("echo", help="echo the string you use here")
|
||||
1
test/data/compare/10_positional/tiramisu.py
Normal file
1
test/data/compare/10_positional/tiramisu.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
parser.add_arguments(StrOption('echo', 'echo the string you use here', properties=('mandatory', 'positional')))
|
||||
1
test/data/compare/10_positional_default/argparse.py
Normal file
1
test/data/compare/10_positional_default/argparse.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
parser.add_argument("echo", help="echo the string you use here", default='blah', nargs='?')
|
||||
1
test/data/compare/10_positional_default/tiramisu.py
Normal file
1
test/data/compare/10_positional_default/tiramisu.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
parser.add_arguments(StrOption('echo', 'echo the string you use here', properties=('mandatory', 'positional'), default='blah'))
|
||||
2
test/data/compare/10_positional_int/argparse.py
Normal file
2
test/data/compare/10_positional_int/argparse.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
parser.add_argument("square", help="display a square of a given number",
|
||||
type=int)
|
||||
1
test/data/compare/10_positional_int/tiramisu.py
Normal file
1
test/data/compare/10_positional_int/tiramisu.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
parser.add_arguments(IntOption('square', 'display a square of a given number', properties=('mandatory', 'positional')))
|
||||
1
test/data/compare/10_positional_list/argparse.py
Normal file
1
test/data/compare/10_positional_list/argparse.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
parser.add_argument("echo", help="echo the string you use here", nargs='+')
|
||||
1
test/data/compare/10_positional_list/tiramisu.py
Normal file
1
test/data/compare/10_positional_list/tiramisu.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
parser.add_arguments(StrOption('echo', 'echo the string you use here', properties=('mandatory', 'positional'), multi=True))
|
||||
1
test/data/compare/20_bool/argparse.py
Normal file
1
test/data/compare/20_bool/argparse.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
parser.add_argument('--verbosity', help='increase output verbosity', action='store_true')
|
||||
1
test/data/compare/20_bool/tiramisu.py
Normal file
1
test/data/compare/20_bool/tiramisu.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
parser.add_arguments(BoolOption('verbosity', 'increase output verbosity', default=False))
|
||||
1
test/data/compare/20_bool_true/argparse.py
Normal file
1
test/data/compare/20_bool_true/argparse.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
parser.add_argument('--verbosity', help='increase output verbosity', action='store_false')
|
||||
1
test/data/compare/20_bool_true/tiramisu.py
Normal file
1
test/data/compare/20_bool_true/tiramisu.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
parser.add_arguments(BoolOption('verbosity', 'increase output verbosity', default=True))
|
||||
2
test/data/compare/20_choice/argparse.py
Normal file
2
test/data/compare/20_choice/argparse.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
parser.add_argument('--door', help='Door numbers', choices=['1', '2', '3'])
|
||||
|
||||
1
test/data/compare/20_choice/tiramisu.py
Normal file
1
test/data/compare/20_choice/tiramisu.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
parser.add_arguments(ChoiceOption('door', 'Door numbers', ('1', '2', '3')))
|
||||
2
test/data/compare/20_int/argparse.py
Normal file
2
test/data/compare/20_int/argparse.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
parser.add_argument('--int', help='integer', type=int)
|
||||
|
||||
1
test/data/compare/20_int/tiramisu.py
Normal file
1
test/data/compare/20_int/tiramisu.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
parser.add_arguments(IntOption('int', 'integer'))
|
||||
2
test/data/compare/20_string/argparse.py
Normal file
2
test/data/compare/20_string/argparse.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
parser.add_argument('--foo', help='foo help')
|
||||
|
||||
2
test/data/compare/20_string/tiramisu.py
Normal file
2
test/data/compare/20_string/tiramisu.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
parser.add_arguments(StrOption('foo', 'foo help'))
|
||||
|
||||
2
test/data/compare/20_string_list/argparse.py
Normal file
2
test/data/compare/20_string_list/argparse.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
parser.add_argument('--foo', help='foo help', nargs='*')
|
||||
|
||||
2
test/data/compare/20_string_list/tiramisu.py
Normal file
2
test/data/compare/20_string_list/tiramisu.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
parser.add_arguments(StrOption('foo', 'foo help', multi=True))
|
||||
|
||||
2
test/data/compare/30_choice_int/argparse.py
Normal file
2
test/data/compare/30_choice_int/argparse.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
parser.add_argument('--door', help='Door numbers', choices=[1, 2, 3])
|
||||
|
||||
1
test/data/compare/30_choice_int/tiramisu.py
Normal file
1
test/data/compare/30_choice_int/tiramisu.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
parser.add_arguments(ChoiceOption('door', 'Door numbers', (1, 2, 3)))
|
||||
2
test/data/compare/30_string_default/argparse.py
Normal file
2
test/data/compare/30_string_default/argparse.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
parser.add_argument('--foo', help='foo help', default='default', nargs='?')
|
||||
|
||||
2
test/data/compare/30_string_default/tiramisu.py
Normal file
2
test/data/compare/30_string_default/tiramisu.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
parser.add_arguments(StrOption('foo', 'foo help', 'default'))
|
||||
|
||||
1
test/data/compare/30_string_short/argparse.py
Normal file
1
test/data/compare/30_string_short/argparse.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
parser.add_argument('-f', '--foo', help='foo help')
|
||||
3
test/data/compare/30_string_short/tiramisu.py
Normal file
3
test/data/compare/30_string_short/tiramisu.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
str_long = StrOption('foo', 'foo help')
|
||||
str_short = SymLinkOption('f', str_long)
|
||||
parser.add_arguments([str_long, str_short])
|
||||
2
test/data/compare/40_multi_bool/argparse.py
Normal file
2
test/data/compare/40_multi_bool/argparse.py
Normal 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')
|
||||
1
test/data/compare/40_multi_bool/tiramisu.py
Normal file
1
test/data/compare/40_multi_bool/tiramisu.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
parser.add_arguments([BoolOption('v', 'increase output verbosity', default=False), BoolOption('s', 'second argument', default=False)])
|
||||
3
test/data/compare/40_positional_optional/argparse.py
Normal file
3
test/data/compare/40_positional_optional/argparse.py
Normal 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')
|
||||
|
||||
3
test/data/compare/40_positional_optional/tiramisu.py
Normal file
3
test/data/compare/40_positional_optional/tiramisu.py
Normal 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)])
|
||||
|
||||
2
test/data/compare/40_short_long/argparse.py
Normal file
2
test/data/compare/40_short_long/argparse.py
Normal 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)
|
||||
4
test/data/compare/40_short_long/tiramisu.py
Normal file
4
test/data/compare/40_short_long/tiramisu.py
Normal 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)])
|
||||
193
test/test_readme.py
Normal file
193
test/test_readme.py
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
from io import StringIO
|
||||
from contextlib import redirect_stdout, redirect_stderr
|
||||
|
||||
|
||||
from tiramisu_cmdline_parser import TiramisuCmdlineParser
|
||||
from tiramisu import IntOption, StrOption, BoolOption, ChoiceOption, \
|
||||
SymLinkOption, OptionDescription, Config
|
||||
|
||||
|
||||
def get_config():
|
||||
choiceoption = ChoiceOption('cmd',
|
||||
'choice the sub argument',
|
||||
('str', 'list', 'int', 'none'),
|
||||
properties=('mandatory',
|
||||
'positional'))
|
||||
booloption = BoolOption('verbosity',
|
||||
'increase output verbosity',
|
||||
default=False)
|
||||
short_booloption = SymLinkOption('v', booloption)
|
||||
str_ = StrOption('str',
|
||||
'string option',
|
||||
properties=('mandatory',),
|
||||
requires=[{'option': choiceoption,
|
||||
'expected': 'str',
|
||||
'action': 'disabled',
|
||||
'inverse': True}])
|
||||
list_ = StrOption('list',
|
||||
'list string option',
|
||||
multi=True,
|
||||
properties=('mandatory',),
|
||||
requires=[{'option': choiceoption,
|
||||
'expected': 'list',
|
||||
'action': 'disabled',
|
||||
'inverse': True}])
|
||||
int_ = IntOption('int',
|
||||
'int option',
|
||||
properties=('mandatory',),
|
||||
requires=[{'option': choiceoption,
|
||||
'expected': 'int',
|
||||
'action': 'disabled',
|
||||
'inverse': True}])
|
||||
config = Config(OptionDescription('root',
|
||||
'root',
|
||||
[choiceoption,
|
||||
booloption,
|
||||
short_booloption,
|
||||
str_,
|
||||
list_,
|
||||
int_
|
||||
]))
|
||||
config.property.read_write()
|
||||
return config
|
||||
|
||||
def test_readme_help():
|
||||
output = """usage: prog.py [-h] [-v] {str,list,int,none}
|
||||
|
||||
positional arguments:
|
||||
{str,list,int,none} choice the sub argument
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-v, --verbosity increase output verbosity
|
||||
"""
|
||||
parser = TiramisuCmdlineParser(get_config(), 'prog.py')
|
||||
f = StringIO()
|
||||
with redirect_stdout(f):
|
||||
parser.print_help()
|
||||
assert f.getvalue() == output
|
||||
|
||||
|
||||
def test_readme_positional_mandatory():
|
||||
output = """usage: prog.py [-h] [-v] {str,list,int,none}
|
||||
prog.py: error: the following arguments are required: cmd
|
||||
"""
|
||||
parser = TiramisuCmdlineParser(get_config(), 'prog.py')
|
||||
f = StringIO()
|
||||
with redirect_stderr(f):
|
||||
try:
|
||||
parser.parse_args([])
|
||||
except SystemExit as err:
|
||||
assert str(err) == "2"
|
||||
else:
|
||||
raise Exception('must raises')
|
||||
assert f.getvalue() == output
|
||||
|
||||
|
||||
def test_readme_mandatory():
|
||||
output = """usage: prog.py [-h] [-v] --str STR {str,list,int,none}
|
||||
prog.py: error: the following arguments are required: --str
|
||||
"""
|
||||
parser = TiramisuCmdlineParser(get_config(), 'prog.py')
|
||||
f = StringIO()
|
||||
with redirect_stderr(f):
|
||||
try:
|
||||
parser.parse_args(['str'])
|
||||
except SystemExit as err:
|
||||
assert str(err) == "2"
|
||||
else:
|
||||
raise Exception('must raises')
|
||||
assert f.getvalue() == output
|
||||
|
||||
|
||||
def test_readme_cross():
|
||||
output = """usage: prog.py [-h] [-v] {str,list,int,none}
|
||||
prog.py: error: unrecognized arguments: --int
|
||||
"""
|
||||
parser = TiramisuCmdlineParser(get_config(), 'prog.py')
|
||||
f = StringIO()
|
||||
with redirect_stderr(f):
|
||||
try:
|
||||
parser.parse_args(['none', '--int'])
|
||||
except SystemExit as err:
|
||||
assert str(err) == "2"
|
||||
else:
|
||||
raise Exception('must raises')
|
||||
assert f.getvalue() == output
|
||||
|
||||
|
||||
def test_readme_int():
|
||||
output = {'cmd': 'int',
|
||||
'int': 3,
|
||||
'verbosity': False,
|
||||
'v': False}
|
||||
config = get_config()
|
||||
parser = TiramisuCmdlineParser(config, 'prog.py')
|
||||
parser.parse_args(['int', '--int', '3'])
|
||||
# assert config.value.dict() == output
|
||||
|
||||
|
||||
def test_readme_int_verbosity():
|
||||
output = {'cmd': 'int',
|
||||
'int': 3,
|
||||
'verbosity': True,
|
||||
'v': True}
|
||||
config = get_config()
|
||||
parser = TiramisuCmdlineParser(config, 'prog.py')
|
||||
parser.parse_args(['int', '--int', '3', '--verbosity'])
|
||||
assert config.value.dict() == output
|
||||
|
||||
|
||||
def test_readme_int_verbosity_short():
|
||||
output = {'cmd': 'int',
|
||||
'int': 3,
|
||||
'verbosity': True,
|
||||
'v': True}
|
||||
config = get_config()
|
||||
parser = TiramisuCmdlineParser(config, 'prog.py')
|
||||
parser.parse_args(['int', '--int', '3', '-v'])
|
||||
assert config.value.dict() == output
|
||||
|
||||
|
||||
def test_readme_str():
|
||||
output = {'cmd': 'str',
|
||||
'str': 'value',
|
||||
'verbosity': False,
|
||||
'v': False}
|
||||
config = get_config()
|
||||
parser = TiramisuCmdlineParser(config, 'prog.py')
|
||||
parser.parse_args(['str', '--str', 'value'])
|
||||
assert config.value.dict() == output
|
||||
|
||||
|
||||
def test_readme_str_int():
|
||||
output = {'cmd': 'str',
|
||||
'str': '3',
|
||||
'verbosity': False,
|
||||
'v': False}
|
||||
config = get_config()
|
||||
parser = TiramisuCmdlineParser(config, 'prog.py')
|
||||
parser.parse_args(['str', '--str', '3'])
|
||||
assert config.value.dict() == output
|
||||
|
||||
|
||||
def test_readme_list():
|
||||
output = {'cmd': 'list',
|
||||
'list': ['a', 'b', 'c'],
|
||||
'verbosity': False,
|
||||
'v': False}
|
||||
config = get_config()
|
||||
parser = TiramisuCmdlineParser(config, 'prog.py')
|
||||
parser.parse_args(['list', '--list', 'a', 'b', 'c'])
|
||||
assert config.value.dict() == output
|
||||
|
||||
|
||||
def test_readme_list_uniq():
|
||||
output = {'cmd': 'list',
|
||||
'list': ['a'],
|
||||
'verbosity': False,
|
||||
'v': False}
|
||||
config = get_config()
|
||||
parser = TiramisuCmdlineParser(config, 'prog.py')
|
||||
parser.parse_args(['list', '--list', 'a'])
|
||||
assert config.value.dict() == output
|
||||
4
tiramisu_cmdline_parser/__init__.py
Normal file
4
tiramisu_cmdline_parser/__init__.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
from .tiramisu_cmdline_parser import TiramisuCmdlineParser
|
||||
|
||||
__version__ = "0.0.1"
|
||||
__all__ = ('TiramisuCmdlineParser',)
|
||||
281
tiramisu_cmdline_parser/tiramisu_cmdline_parser.py
Normal file
281
tiramisu_cmdline_parser/tiramisu_cmdline_parser.py
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
# Copyright (C) 2018 Team tiramisu (see AUTHORS for all contributors)
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Lesser General Public License as published by the
|
||||
# Free Software Foundation, either version 3 of the License, or (at your
|
||||
# option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
from typing import Union, List, Optional
|
||||
from argparse import ArgumentParser, Namespace, SUPPRESS, _HelpAction
|
||||
try:
|
||||
from tiramisu import Config
|
||||
from tiramisu.error import PropertiesOptionError
|
||||
except ImportError:
|
||||
Config = None
|
||||
from tiramisu_json_api.error import PropertiesOptionError
|
||||
try:
|
||||
from tiramisu_json_api import Config as ConfigJson
|
||||
if Config is None:
|
||||
Config = ConfigJson
|
||||
except ImportError:
|
||||
ConfigJson = Config
|
||||
|
||||
|
||||
class TiramisuNamespace(Namespace):
|
||||
def _populate(self):
|
||||
#self._config.property.read_only()
|
||||
for tiramisu_key, tiramisu_value in self._config.value.dict().items():
|
||||
option = self._config.option(tiramisu_key)
|
||||
if not option.option.issymlinkoption():
|
||||
if tiramisu_value == [] and option.option.ismulti() and option.owner.isdefault():
|
||||
tiramisu_value = None
|
||||
super().__setattr__(tiramisu_key, tiramisu_value)
|
||||
#self._config.property.read_write()
|
||||
|
||||
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._populate()
|
||||
return super().__getattribute__(key)
|
||||
|
||||
|
||||
class _TiramisuHelpAction(_HelpAction):
|
||||
needs = False
|
||||
def __call__(self, *args, **kwargs):
|
||||
_TiramisuHelpAction.needs = True
|
||||
|
||||
def display(self, parser):
|
||||
_HelpAction.__call__(self, parser, None, None)
|
||||
|
||||
|
||||
class TiramisuCmdlineParser(ArgumentParser):
|
||||
def __init__(self,
|
||||
config: Union[Config, ConfigJson],
|
||||
*args,
|
||||
fullpath: bool=True,
|
||||
_forhelp: bool=False,
|
||||
**kwargs):
|
||||
self.fullpath = fullpath
|
||||
self.config = config
|
||||
super().__init__(*args, **kwargs)
|
||||
self.register('action', 'help', _TiramisuHelpAction)
|
||||
self._config_to_argparser(_forhelp,
|
||||
self.config.option)
|
||||
|
||||
def _pop_action_class(self, kwargs, default=None):
|
||||
ret = super()._pop_action_class(kwargs, default)
|
||||
if kwargs.get('action') != 'help' and kwargs.get('dest') != 'help':
|
||||
return ret
|
||||
return _TiramisuHelpAction
|
||||
|
||||
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.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 _parse_known_args(self, args=None, namespace=None):
|
||||
|
||||
namespace_, args_ = super()._parse_known_args(args, namespace)
|
||||
if args != args_ and args_ and args_[0].startswith(self.prefix_chars):
|
||||
# option that was disabled are no more disable
|
||||
# so create a new parser
|
||||
new_parser = TiramisuCmdlineParser(self.config,
|
||||
self.prog,
|
||||
fullpath=self.fullpath)
|
||||
namespace_, args_ = new_parser._parse_known_args(args_, namespace)
|
||||
else:
|
||||
if self._registries['action']['help'].needs:
|
||||
# display help only when all variables assignemnt are done
|
||||
self._registries['action']['help'].needs = False
|
||||
helper = self._registries['action']['help'](None)
|
||||
helper.display(self)
|
||||
return namespace_, args_
|
||||
|
||||
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, *args, **kwargs):
|
||||
raise NotImplementedError('do not use add_argument')
|
||||
|
||||
def add_subparsers(self, *args, **kwargs):
|
||||
raise NotImplementedError('do not use add_subparsers')
|
||||
|
||||
def _gen_argument(self, name, properties):
|
||||
if len(name) == 1 and 'longargument' not in properties:
|
||||
return self.prefix_chars + name
|
||||
return self.prefix_chars * 2 + name
|
||||
|
||||
def _config_to_argparser(self,
|
||||
_forhelp: bool,
|
||||
config,
|
||||
prefix: Optional[str]=None,
|
||||
group=None) -> None:
|
||||
if group is None:
|
||||
group = super()
|
||||
actions = {}
|
||||
for obj in config.list(type='all'):
|
||||
option = obj.option
|
||||
if option.isoptiondescription():
|
||||
if _forhelp:
|
||||
group = self.add_argument_group(option.doc())
|
||||
if prefix:
|
||||
prefix_ = prefix + '.' + option.name()
|
||||
else:
|
||||
prefix_ = option.path()
|
||||
self._config_to_argparser(_forhelp, obj, prefix_, group)
|
||||
continue
|
||||
if 'frozen' in option.properties():
|
||||
continue
|
||||
name = option.name()
|
||||
if name.startswith(self.prefix_chars):
|
||||
raise ValueError('name cannot startswith "{}"'.format(self.prefix_chars))
|
||||
if self.fullpath and prefix:
|
||||
name = prefix + '.' + name
|
||||
properties = obj.property.get()
|
||||
kwargs = {'help': option.doc().replace('%', '%%')}
|
||||
if option.issymlinkoption():
|
||||
actions[option.name(follow_symlink=True)][0].insert(0, self._gen_argument(name, properties))
|
||||
continue
|
||||
if 'positional' in properties:
|
||||
if not 'mandatory' in properties:
|
||||
raise ValueError('"positional" argument must be "mandatory" too')
|
||||
args = [option.path()]
|
||||
if _forhelp:
|
||||
kwargs['default'] = obj.value.default()
|
||||
else:
|
||||
kwargs['default'] = obj.value.get()
|
||||
kwargs['nargs'] = '?'
|
||||
else:
|
||||
kwargs['dest'] = option.path()
|
||||
kwargs['default'] = SUPPRESS
|
||||
args = [self._gen_argument(name, properties)]
|
||||
if _forhelp and 'mandatory' in properties:
|
||||
kwargs['required'] = True
|
||||
if option.type() == 'boolean':
|
||||
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 obj.value.get() is False:
|
||||
action = 'store_true'
|
||||
else:
|
||||
action = 'store_false'
|
||||
kwargs['action'] = action
|
||||
else:
|
||||
if _forhelp:
|
||||
value = obj.value.default()
|
||||
else:
|
||||
value = obj.value.get()
|
||||
if value not in [None, []]:
|
||||
#kwargs['default'] = kwargs['const'] = option.default()
|
||||
#kwargs['action'] = 'store_const'
|
||||
kwargs['nargs'] = '?'
|
||||
if option.ismulti():
|
||||
if _forhelp and 'mandatory' in properties:
|
||||
kwargs['nargs'] = '+'
|
||||
else:
|
||||
kwargs['nargs'] = '*'
|
||||
if option.type() == 'string':
|
||||
pass
|
||||
elif option.type() == 'integer':
|
||||
kwargs['type'] = int
|
||||
elif option.type() == 'choice':
|
||||
kwargs['choices'] = obj.value.list()
|
||||
else:
|
||||
pass
|
||||
#raise NotImplementedError('not supported yet')
|
||||
actions[option.name()] = (args, kwargs)
|
||||
for args, kwargs in actions.values():
|
||||
group.add_argument(*args, **kwargs)
|
||||
|
||||
|
||||
def parse_args(self,
|
||||
*args,
|
||||
valid_mandatory=True,
|
||||
**kwargs):
|
||||
kwargs['namespace'] = TiramisuNamespace(self.config)
|
||||
try:
|
||||
namespaces = super().parse_args(*args, **kwargs)
|
||||
del namespaces.__dict__['_config']
|
||||
except PropertiesOptionError as err:
|
||||
name = err._option_bag.option.impl_getname()
|
||||
properties = self.config.option(name).property.get()
|
||||
if 'positional' not in properties:
|
||||
if len(name) == 1 and 'longargument' not in properties:
|
||||
name = self.prefix_chars + name
|
||||
else:
|
||||
name = self.prefix_chars * 2 + name
|
||||
if err.proptype == ['mandatory']:
|
||||
self.error('the following arguments are required: {}'.format(name))
|
||||
else:
|
||||
self.error('unrecognized arguments: {}'.format(name))
|
||||
if valid_mandatory:
|
||||
for key in self.config.value.mandatory():
|
||||
properties = self.config.option(key).property.get()
|
||||
if 'positional' not in properties:
|
||||
if self.fullpath or '.' not in key:
|
||||
name = key
|
||||
else:
|
||||
name = key.rsplit('.', 1)[1]
|
||||
args = self._gen_argument(name, self.config.option(key).property.get())
|
||||
else:
|
||||
args = key
|
||||
self.error('the following arguments are required: {}'.format(args))
|
||||
return namespaces
|
||||
|
||||
def format_usage(self,
|
||||
*args,
|
||||
**kwargs):
|
||||
help_formatter = TiramisuCmdlineParser(self.config,
|
||||
self.prog,
|
||||
fullpath=self.fullpath,
|
||||
_forhelp=True)
|
||||
return super(TiramisuCmdlineParser, help_formatter).format_usage(*args, **kwargs)
|
||||
|
||||
def format_help(self,
|
||||
*args,
|
||||
**kwargs):
|
||||
help_formatter = TiramisuCmdlineParser(self.config,
|
||||
self.prog,
|
||||
fullpath=self.fullpath,
|
||||
_forhelp=True)
|
||||
return super(TiramisuCmdlineParser, help_formatter).format_help(*args, **kwargs)
|
||||
|
||||
def get_config(self):
|
||||
return self.config
|
||||
Loading…
Reference in a new issue