Skip to content

Commit

Permalink
Merge pull request #9 from fybx/issue-6
Browse files Browse the repository at this point in the history
add positional arguments
  • Loading branch information
fybx authored Dec 21, 2023
2 parents d68792f + 51b133d commit d0e9eee
Show file tree
Hide file tree
Showing 5 changed files with 185 additions and 12 deletions.
64 changes: 55 additions & 9 deletions crispy/crispy.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from crispy.duplicate_name_exception import DuplicateNameException
from crispy.missing_value_exception import MissingValueException
from crispy.no_arguments_exception import NoArgumentsException
from crispy.parsing_exception import ParsingException
from crispy.unexpected_argument_exception import UnexpectedArgumentException
from crispy.too_many_subcommands_exception import TooManySubcommandsException

Expand All @@ -36,13 +37,30 @@ def __init__(self, accept_shortform=True, accept_longform=True):
"""
self.accepted_keys: Dict[str, str] = {}
self.variables: Dict[str, Type[Union[str, bool, int, float]]] = {}
self.lookup: Dict[int, str] = {}
self.subcommands: Dict[str, str] = {}

if not (accept_shortform or accept_longform):
raise ValueError("crispy: At least one form must be accepted!")
self.accept_shortform = accept_shortform
self.accept_longform = accept_longform

def add_positional(self, name: str, var_type: Type[Union[str, bool, int, float]], position: int):
"""
Adds a positional argument to the parser.
:param name: Name of the positional argument
:param var_type: Type of the positional argument
:param position: Position of the positional argument
:return: None
"""
if name in self.variables:
raise DuplicateNameException(f"crispy: variable with name '{name}' is present! Choose something else.")
if position < 0 or position in self.lookup:
raise ValueError(f"crispy: invalid position '{position}'!")
self.variables[name] = var_type
self.lookup[position] = name

def add_subcommand(self, name: str, description: str):
"""Adds a subcommand to the parser.
Expand Down Expand Up @@ -107,7 +125,7 @@ def parse_arguments(self, args: List[str]) -> Tuple[str, Dict[str, Union[str, bo

subcommand: str = ""
keys: Dict[str, Union[str, bool, int, float]] = {}
i, len_args = 0, len(args)
i, j, len_args = 0, 0, len(args)
while i < len_args:
key = args[i]

Expand All @@ -116,17 +134,32 @@ def parse_arguments(self, args: List[str]) -> Tuple[str, Dict[str, Union[str, bo
continue

if not key.startswith("-"):
if subcommand != "":
raise TooManySubcommandsException(f"crispy: too many subcommands! '{key}' is unexpected!")
subcommand = key
if key in self.subcommands:
if subcommand != "":
raise TooManySubcommandsException(f"crispy: too many subcommands! '{key}' is unexpected!")
subcommand = key
else:
name = self.lookup[j]
expected_type, found_type = self.variables[name], self.deduce_type(key)
if expected_type != found_type:
raise ParsingException(f"crispy: type mismatch! '{key}' is not of type '{expected_type}'", expected_type, j, found_type)

keys[name] = self.try_parse(key, expected_type)
j += 1
i += 1
continue

elif "=" not in key:
if (i + 1 < len_args) and (args[i + 1] not in self.accepted_keys) and ("=" not in args[i + 1]):
if key not in self.accepted_keys:
raise UnexpectedArgumentException(f"crispy: unexpected argument: '{key}'")
if ((i + 1 < len_args) and
(args[i + 1] not in self.accepted_keys) and
("=" not in args[i + 1]) and
(self.variables[self.accepted_keys[key]] != bool)):
value = args[i + 1]
i += 2
else:
expected_type = self.variables.get(self.accepted_keys.get(key))
expected_type = self.variables[self.accepted_keys[key]]
if expected_type == bool:
value = "True"
i += 1
Expand All @@ -138,9 +171,7 @@ def parse_arguments(self, args: List[str]) -> Tuple[str, Dict[str, Union[str, bo

accepted_key = self.accepted_keys.get(key)
if accepted_key:
keys[accepted_key] = self.try_parse(value, self.variables.get(accepted_key))
else:
raise UnexpectedArgumentException(f"crispy: unexpected argument: '{key}'")
keys[accepted_key] = self.try_parse(value, self.variables[accepted_key])

for key, value in self.variables.items():
if value == bool and key not in keys:
Expand Down Expand Up @@ -177,3 +208,18 @@ def try_parse(value: str, expected_type: type) -> Union[str, bool, int, float]:
if expected_type == float:
return float(value)
return value

@staticmethod
def deduce_type(value: str):
"""
Deduces the type of the value.
:param value: Value in string type
:return: Returns the deduced type of value in string representation
"""
if value.lower() == "true" or value.lower() == "false":
return bool
if value.isdigit():
return int
if value.replace(".", "", 1).isdigit():
return float
return str
5 changes: 4 additions & 1 deletion crispy/parsing_exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@
# USA.


from typing import Type


class ParsingException(Exception):
def __init__(self, reason: str, expected: str, at_position: int, found: str):
def __init__(self, reason: str, expected: Type, at_position: int, found: Type):
super().__init__(reason)
self.expected = expected
self.at_position = at_position
Expand Down
46 changes: 46 additions & 0 deletions tests/test_deduce_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#
# This file is part of the crispy-parser library.
# Copyright (C) 2023 Ferit Yiğit BALABAN
#
# This library 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 2.1 of the License, or (at your option) any later version.
#
# This library 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 library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
# USA.

from unittest import TestCase
from crispy.crispy import Crispy

class TestDeduceType(TestCase):
def setUp(self) -> None:
self.c = Crispy()
return super().setUp()

def test_deduce_type_returns_bool_for_true(self):
result = self.c.deduce_type("true")
self.assertEqual(result, bool)

def test_deduce_type_returns_bool_for_false(self):
result = self.c.deduce_type("false")
self.assertEqual(result, bool)

def test_deduce_type_returns_int_for_integer(self):
result = self.c.deduce_type("123")
self.assertEqual(result, int)

def test_deduce_type_returns_float_for_float(self):
result = self.c.deduce_type("3.14")
self.assertEqual(result, float)

def test_deduce_type_returns_str_for_other_values(self):
result = self.c.deduce_type("hello")
self.assertEqual(result, str)
77 changes: 77 additions & 0 deletions tests/test_positional_arguments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#
# This file is part of the crispy-parser library.
# Copyright (C) 2023 Ferit Yiğit BALABAN
#
# This library 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 2.1 of the License, or (at your option) any later version.
#
# This library 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 library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
# USA.


from unittest import TestCase

from crispy.crispy import Crispy
from crispy.parsing_exception import ParsingException


class TestPositionalArguments(TestCase):
def setUp(self) -> None:
self.c = Crispy()
self.c.add_positional("name", str, 0)
self.c.add_positional("age", int, 1)
self.c.add_variable("is_student", bool)
self.c.add_variable("height", float)
self.c.add_subcommand("create", "creates the user")
return super().setUp()

def test_correct_order(self):
expected = ("", {
"name": "Ferit",
"age": 21,
"is_student": False
})
actual = self.c.parse_string("Ferit 21")
self.assertEqual(expected, actual)


def test_type_mismatch(self):
with self.assertRaises(ParsingException) as context:
self.c.parse_string("21 Ferit")
self.assertEqual(context.exception.expected, str)
self.assertEqual(context.exception.at_position, 0)
self.assertEqual(context.exception.found, int)

def test_with_keys(self):
expected = ("", {
"name": "Ferit",
"age": 21,
"is_student": True,
"height": 1.86
})
actual1 = self.c.parse_string("Ferit 21 --is_student --height=1.86")
actual2 = self.c.parse_string("--is_student --height=1.86 Ferit 21")
actual3 = self.c.parse_string("--height 1.86 Ferit -i 21")
actual4 = self.c.parse_string("-h 1.86 Ferit 21 -i")
self.assertEqual(expected, actual1)
self.assertEqual(expected, actual2)
self.assertEqual(expected, actual3)
self.assertEqual(expected, actual4)

def test_with_subcommand(self):
pass

def test_fail_with_keys_and_subcommand(self):
pass

def test_pass_with_keys_and_subcommand(self):
pass
5 changes: 3 additions & 2 deletions tests/test_subcommands.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,13 @@ class Test_Subcommands(TestCase):
def setUp(self):
self.c = Crispy()
self.c.add_subcommand('add', 'adds two numbers given by keys -a and -b')
self.c.add_subcommand('test', 'to test toomanysubcommands exception')
self.c.add_variable('a', int)
self.c.add_variable('b', int)

def test_add_subcommand(self):
self.c.add_subcommand('test', 'description of subcommand test')
self.assertEqual(self.c.subcommands['test'], 'description of subcommand test')
self.c.add_subcommand('test2', 'description of subcommand test')
self.assertEqual(self.c.subcommands['test2'], 'description of subcommand test')

def test_add_duplicate_subcommand(self):
with self.assertRaises(DuplicateNameException):
Expand Down

0 comments on commit d0e9eee

Please sign in to comment.