From f84d775f266128002db8c0a2c2d947f9001abbf4 Mon Sep 17 00:00:00 2001 From: Clif Bratcher Date: Sun, 19 May 2024 16:10:09 -0400 Subject: [PATCH] Fixes #5 - Clearly define boolean handling * Shift to a custom "type" * Clearly specify truth table for boolean parameters * Change handling to align with truth table * Update tests where applicable --- README.md | 8 ++++++++ simplecli/simplecli.py | 26 ++++++++++++++++---------- tests/test_param.py | 2 +- tests/test_wrap.py | 22 +++++++++++++++++++--- 4 files changed, 44 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 0fd50e4..2fbad8b 100644 --- a/README.md +++ b/README.md @@ -154,6 +154,14 @@ A parameter becomes "required" if it is an `int`, `float` or `str` and does not With more than one decorator, it's impossible to tell which function you'd like to wrap. Because of this, we enforce a single `@wrap` per file. Importing modules using `pysimplecli` is supported, as is calling said wrapped functions. +### Truth table for boolean parameters + +| Parameter Default | Without Argument | With Argument | +| --- | --- | --- | +| `True` | `True` | `False` | +| `False` | `False` | `True` | +| no default | `False` | `True` | + ## How It Works The `wrap` decorator takes the annotated parameters of a given function and maps them to corresponding command-line arguments. It relies heavily on Python's `inspect` and `tokenize` modules to gather parameters, parse comments for parameter descriptions, determine default functionality, etc... In fact, a core part of this module is a are heavily extended `inspect.Parameter` objects. diff --git a/simplecli/simplecli.py b/simplecli/simplecli.py index d97f2ca..e8127b8 100644 --- a/simplecli/simplecli.py +++ b/simplecli/simplecli.py @@ -38,10 +38,12 @@ class Empty: pass +class DefaultIfBool: + pass + + _wrapped = False -# Overloaded placeholder for a potential boolean -DefaultIfBool = "Crazy going slowly am I, 6, 5, 4, 3, 2, 1, switch!" -ValueType = Union[type[Empty], bool, float, int, str] +ValueType = Union[type[DefaultIfBool], type[Empty], bool, float, int, str] ArgDict = dict[str, ValueType] ArgList = list[str] @@ -208,6 +210,7 @@ def validate(self, value: ValueType) -> bool: return passed def set_value(self, value: ValueType) -> None: + result = value if self.validate(value) is False: raise ValueError( f"'{self.help_name}' must be of type {self.help_type}" @@ -225,13 +228,10 @@ def set_value(self, value: ValueType) -> None: if value is DefaultIfBool: if bool not in self.datatypes: raise ValueError(f"'{self.help_name}' requires a value") - if self.default != Empty: - self._value = self.annotation(self.default) - return - else: - self._value = self.annotation(True) - return - self._value = self.annotation(value) + result = self.default if self.default is not Empty else True + elif bool in self.datatypes and self.default is Empty: + result = value + self._value = self.annotation(result) def tokenize_string(string: str) -> Generator[TokenInfo, None, None]: @@ -315,6 +315,12 @@ def params_to_kwargs( if pos_args: param.set_value(pos_args.pop(0)) elif param.name in kw_args: + if kw_args[param.name] is DefaultIfBool: + # Invert the default value + param.set_value( + True if param.default is Empty else not param.default + ) + continue param.set_value(kw_args[param.name]) continue elif param.required: diff --git a/tests/test_param.py b/tests/test_param.py index e5c99a4..420ea03 100644 --- a/tests/test_param.py +++ b/tests/test_param.py @@ -74,7 +74,7 @@ def test_set_value(): with pytest.raises(ValueError, match="[int, float]"): p1.set_value("this is the value") - with pytest.raises(ValueError, match="[int, float]"): + with pytest.raises(TypeError, match="[int, float]"): p1.set_value(DefaultIfBool) assert p1.value is Empty p1.set_value(3) diff --git a/tests/test_wrap.py b/tests/test_wrap.py index cb4db3a..695c72b 100644 --- a/tests/test_wrap.py +++ b/tests/test_wrap.py @@ -181,22 +181,38 @@ def code(a: int, b: int, c: typing.Union[int, float]): def test_wrap_boolean_false(monkeypatch): - monkeypatch.setattr(sys, "argv", ["filename", "--is-false"]) + monkeypatch.setattr(sys, "argv", ["filename"]) @simplecli.wrap def code(is_false: bool = False): assert is_false is False +def test_wrap_boolean_false_invert(monkeypatch): + monkeypatch.setattr(sys, "argv", ["filename", "--invert"]) + + @simplecli.wrap + def code(invert: bool = False): + assert invert is True + + def test_wrap_boolean_true(monkeypatch): - monkeypatch.setattr(sys, "argv", ["filename", "--is-true"]) + monkeypatch.setattr(sys, "argv", ["filename"]) @simplecli.wrap def code(is_true: bool = True): assert is_true is True -def test_wrap_boolean_true_no_default(monkeypatch): +def test_wrap_boolean_true_invert(monkeypatch): + monkeypatch.setattr(sys, "argv", ["filename", "--invert"]) + + @simplecli.wrap + def code(invert: bool = True): + assert invert is False + + +def test_wrap_boolean_true_no_default_invert(monkeypatch): monkeypatch.setattr(sys, "argv", ["filename", "--is-something"]) @simplecli.wrap