Initial import with the args kata.
This commit is contained in:
commit
f6beb1c197
6 changed files with 271 additions and 0 deletions
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
.DS_Store
|
||||
.idea
|
||||
*.log
|
||||
tmp/
|
||||
|
||||
*.py[cod]
|
||||
*.egg
|
||||
build
|
||||
htmlcov
|
||||
__pycache__
|
19
LICENSE
Normal file
19
LICENSE
Normal file
|
@ -0,0 +1,19 @@
|
|||
Copyright 2023 David Soulayrol.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the “Software”), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
26
args/README.md
Normal file
26
args/README.md
Normal file
|
@ -0,0 +1,26 @@
|
|||
# A Simple and stupid Args Parser.
|
||||
|
||||
The subject was found here: https://codingdojo.org/kata/Args/
|
||||
|
||||
This implementation requires no dependencies (but probably comes with some bugs).
|
||||
|
||||
## Defining the pattern
|
||||
|
||||
An arguments pattern is defined by a comma-separated expressions with the following format:
|
||||
|
||||
FLAG:NAME:TYPE[*]
|
||||
|
||||
- `FLAG` must be one character only, and is the token that should be encountered on the line to parse.
|
||||
- `NAME` is the argument name, and will be the name of the available matching property once the command line was parsed.
|
||||
- `TYPE` can be *b* for *boolean*, *d* for an integer (think C), *s* for a string. If the format is followed by a star, then the argument should be a comma-separated list of values with the given type.
|
||||
|
||||
The tests are full of samples.
|
||||
|
||||
## Known limitations
|
||||
|
||||
- Pattern parsing is fragile, as it misses many tests on the input.
|
||||
- Boolean arrays were not considered.
|
||||
|
||||
## tests
|
||||
|
||||
Unit tests for **pytest** and siblingss are available in the `tests` directory. Because of laziness and lack of time, they only examine the parse function as a black box.
|
108
args/args.py
Normal file
108
args/args.py
Normal file
|
@ -0,0 +1,108 @@
|
|||
# A Simple and stupid Args Parser.
|
||||
# Copyright 2023 David Soulayrol.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the “Software”), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
|
||||
|
||||
class ArgParser:
|
||||
def __init__(self, name, reducer, default_value, init_value=None):
|
||||
self.__name = name
|
||||
self.__reducer = reducer
|
||||
self.__init_value = init_value
|
||||
self.__value = default_value
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.__name
|
||||
|
||||
@property
|
||||
def reducer(self):
|
||||
return self.__reducer
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
return self.__value
|
||||
|
||||
def init(self):
|
||||
self.__value = self.__init_value
|
||||
|
||||
def parse_value(self, value):
|
||||
if self.__reducer is None:
|
||||
raise PatternException()
|
||||
|
||||
self.__value = self.__reducer(value)
|
||||
|
||||
|
||||
class Namespace:
|
||||
def __init__(self, values):
|
||||
print(values)
|
||||
self.__dict__.update(dict(map(lambda v: (v.name, v.value), values)))
|
||||
|
||||
|
||||
def boolean(value):
|
||||
raise ValueError(value)
|
||||
|
||||
|
||||
def create_list(reducer):
|
||||
def list_reducer(value):
|
||||
return [reducer(v) for v in value.split(',')]
|
||||
|
||||
return list_reducer
|
||||
|
||||
|
||||
def parse_pattern(pattern):
|
||||
# Format is one_letter_flag:name:format
|
||||
# with format : b | d | s [*]
|
||||
parsers = {}
|
||||
|
||||
reducers = {
|
||||
'b': (boolean, False, True),
|
||||
'd': (int, 0, 0),
|
||||
's': (str, '', '')
|
||||
}
|
||||
|
||||
for desc in pattern.replace(' ', '').split(','):
|
||||
flag, name, format = desc.split(':')
|
||||
|
||||
if '-' + flag in parsers:
|
||||
raise ValueError('"%s" is defined twice' % flag)
|
||||
|
||||
try:
|
||||
fn, default_value, init_value = reducers[format[0]]
|
||||
|
||||
if (format.endswith('*')):
|
||||
fn = create_list(fn)
|
||||
default_value = []
|
||||
|
||||
parsers['-' + flag] = ArgParser(name, fn, default_value, init_value)
|
||||
|
||||
except KeyError as e:
|
||||
raise ValueError('Unknown flag type "%s"' % format[0])
|
||||
|
||||
return parsers
|
||||
|
||||
|
||||
def parse(pattern, args):
|
||||
current_parser = None
|
||||
parsers = parse_pattern(pattern)
|
||||
|
||||
for arg in args:
|
||||
if arg in parsers:
|
||||
# This is a new flag. Update context.
|
||||
current_parser = parsers[arg]
|
||||
current_parser.init()
|
||||
|
||||
else:
|
||||
# This is a value.
|
||||
if current_parser is None:
|
||||
raise ValueError('Unexpected "%s"' % arg)
|
||||
|
||||
current_parser.parse_value(arg)
|
||||
current_parser = None
|
||||
|
||||
return Namespace(parsers.values())
|
7
args/tests/context.py
Normal file
7
args/tests/context.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
# A nice trick found in https://docs.python-guide.org/writing/structure/
|
||||
|
||||
import os
|
||||
import sys
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
||||
|
||||
import args
|
101
args/tests/test_args.py
Normal file
101
args/tests/test_args.py
Normal file
|
@ -0,0 +1,101 @@
|
|||
import pytest
|
||||
from context import args
|
||||
|
||||
|
||||
def test_invalid_pattern():
|
||||
with pytest.raises(ValueError):
|
||||
values = args.parse('F:flag:z', [])
|
||||
|
||||
def test_pattern_with_double_definition():
|
||||
with pytest.raises(ValueError):
|
||||
values = args.parse('F:flag:b,F:flag:b', [])
|
||||
|
||||
def test_unepected_value():
|
||||
with pytest.raises(ValueError):
|
||||
values = args.parse('F:flag:b', ['F'])
|
||||
|
||||
def test_boolean_default():
|
||||
values = args.parse('F:flag:b', [])
|
||||
|
||||
assert len(values.__dict__) == 1
|
||||
assert values.flag is False
|
||||
|
||||
def test_boolean():
|
||||
values = args.parse('F:flag:b', ['-F'])
|
||||
|
||||
assert len(values.__dict__) == 1
|
||||
assert values.flag is True
|
||||
|
||||
def test_boolean_unexpected_value():
|
||||
with pytest.raises(ValueError):
|
||||
values = args.parse('F:flag:b', ['-F', 'True'])
|
||||
|
||||
def test_integer_default():
|
||||
values = args.parse('I:integer:d', [])
|
||||
|
||||
assert len(values.__dict__) == 1
|
||||
assert values.integer == 0
|
||||
|
||||
def test_integer():
|
||||
values = args.parse('I:integer:d', ['-I', '512'])
|
||||
|
||||
assert len(values.__dict__) == 1
|
||||
assert values.integer == 512
|
||||
|
||||
def test_integer_negative_value():
|
||||
values = args.parse('I:integer:d', ['-I', '-42'])
|
||||
|
||||
assert len(values.__dict__) == 1
|
||||
assert values.integer == -42
|
||||
|
||||
def test_integer_array_default():
|
||||
values = args.parse('a:iarray:d*', [])
|
||||
|
||||
assert len(values.__dict__) == 1
|
||||
assert type(values.iarray) is list and len(values.iarray) == 0
|
||||
|
||||
def test_integer_array():
|
||||
values = args.parse('z:iarray:s*', ['-z', '-1,47,-42,0,1'])
|
||||
|
||||
assert len(values.__dict__) == 1
|
||||
assert type(values.iarray) is list and len(values.iarray) == 5
|
||||
len([i for i, j in zip(values.iarray, [-1, 47, -42, 0, 1]) if i == j]) == 5
|
||||
|
||||
def test_string_default():
|
||||
values = args.parse('S:string:s', [])
|
||||
|
||||
assert len(values.__dict__) == 1
|
||||
assert values.string == ''
|
||||
|
||||
def test_string():
|
||||
values = args.parse('S:string:s', ['-S', 'some_string'])
|
||||
|
||||
assert len(values.__dict__) == 1
|
||||
assert values.string == 'some_string'
|
||||
|
||||
def test_string_empty():
|
||||
values = args.parse('S:string:s', ['-S', ''])
|
||||
|
||||
assert len(values.__dict__) == 1
|
||||
assert values.string == ''
|
||||
|
||||
def test_string_array_default():
|
||||
values = args.parse('A:array:s*', [])
|
||||
|
||||
assert len(values.__dict__) == 1
|
||||
assert type(values.array) is list and len(values.array) == 0
|
||||
|
||||
def test_string_array():
|
||||
values = args.parse('A:array:s*', ['-A', 'a,b,c,long'])
|
||||
|
||||
assert len(values.__dict__) == 1
|
||||
assert type(values.array) is list and len(values.array) == 4
|
||||
len([i for i, j in zip(values.array, ['a', 'b', 'c', 'long']) if i == j]) == 4
|
||||
|
||||
def test_full():
|
||||
values = args.parse('l:logging:b,p:port:d,d:directory:s', ['-l', '-p', '8080', '-d', '/usr/logs'])
|
||||
|
||||
assert len(values.__dict__) == 3
|
||||
assert values.directory == '/usr/logs'
|
||||
assert values.logging is True
|
||||
assert values.port == 8080
|
Loading…
Reference in a new issue