Skip to content

Commit

Permalink
Shift optional and required to properties
Browse files Browse the repository at this point in the history
    * Fold _parse_line into parse_or_prepend
    * Tests and more tests
  • Loading branch information
inno committed May 5, 2024
1 parent 0d75b35 commit 3ce38b3
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 32 deletions.
70 changes: 40 additions & 30 deletions simplecli/simplecli.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from __future__ import annotations
import contextlib
import inspect
import io
Expand All @@ -9,8 +10,6 @@
COMMENT,
NAME,
NL,
NUMBER,
STRING,
TokenError,
TokenInfo,
generate_tokens,
Expand Down Expand Up @@ -44,8 +43,8 @@ class Empty:

class Param(inspect.Parameter):
internal_only: bool # Do not pass to wrapped function
optional: bool # Value can be `None`, mirrors `Optional` type
required: bool # Exit if a value is not present
_required: bool # Exit if a value is not present
_optional: bool | None = None # Mirrors `Optional` type

def __init__(self, *argv: Any, **kwargs: Any) -> None:
kwargs["annotation"] = kwargs.pop("annotation", Empty)
Expand All @@ -71,19 +70,19 @@ def __init__(self, *argv: Any, **kwargs: Any) -> None:
param_line = str(kwargs.pop("line", ""))
param_value = kwargs.pop("value", Empty)
param_internal_only = bool(kwargs.pop("internal_only", False))
param_optional = bool(kwargs.pop("optional", False))
param_optional = kwargs.pop("optional", None)
param_required = bool(kwargs.pop("required", True))
super().__init__(*argv, **kwargs)
self._value = param_value
self.description = param_description
self.internal_only = param_internal_only
self.optional = param_optional
self.required = param_required
self._optional = (
bool(param_optional) if param_optional is not None else None
)
self._required = param_required
# Overrides required as these values are generally unused
if self.internal_only:
self.required = False
if not self.description:
self._parse_line(param_line)
self.parse_or_prepend(param_line)

def __eq__(self, other: object) -> bool:
if not isinstance(other, Param):
Expand All @@ -110,6 +109,29 @@ def __str__(self) -> str:
f"value={value} "
)

@property
def required(self) -> bool:
# Internal only params never requires a value
if self.internal_only:
return False

# Optional implies no required value
if self.optional:
return False

# Existence of a default value implies no required value
if self.default is not Empty:
return False

# Fallback to set/default value
return self._required

@property
def optional(self) -> bool:
if self._optional is not None:
return self._optional
return len(self.datatypes) == 2 and type(None) in self.datatypes

@property
def help_name(self) -> str:
return self.name.replace("_", "-")
Expand Down Expand Up @@ -144,30 +166,18 @@ def parse_or_prepend(
if not overwrite and self.description:
return False

line_set = self._parse_line(line)
line_set = False
try:
for token in tokenize_string(line):
if token.exact_type is COMMENT:
self._set_description(token.string, force=True)
line_set = True
except TokenError:
line_set = False
if comment:
self._set_description(comment)
return line_set

def _parse_line(self, line: str) -> bool:
self.description = ""
try:
tokens = list(tokenize_string(line))
except TokenError:
return False

for token in tokens:
if token.type not in (COMMENT, NAME, NUMBER, STRING):
continue
if token.exact_type is COMMENT:
self._set_description(token.string, force=True)
if token.string == "Optional":
self.required = False
self.optional = True
if self.default is not Empty:
self.required = False
return True

@property
def datatypes(self) -> list[type]:
args = get_args(self.annotation)
Expand Down
2 changes: 1 addition & 1 deletion tests/test_extract_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def code(foo: int = 123):
annotation=int,
default=123,
)
expected_param._parse_line(" def code(foo: int = 123):\n")
expected_param.parse_or_prepend(" def code(foo: int = 123):\n")
assert params == [expected_param]


Expand Down
54 changes: 53 additions & 1 deletion tests/test_param.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import pytest
import re
from simplecli.simplecli import DefaultIfBool, Empty, Param
from typing import Union
from typing import Optional, Union


def test_required_arguments():
Expand Down Expand Up @@ -106,3 +106,55 @@ def test_union_uniontype():
p2 = Param(name="testparam2", annotation=int | bool)
assert p2.datatypes == [int, bool]
assert p2.help_type == "[int, bool]"


def test_union_optional():
p0 = Param(name="testparam1", annotation=Optional[float])
assert p0.datatypes == [float, type(None)]
assert p0.help_type == "[float, NoneType]"
assert p0.optional is True
assert p0.required is False

p1 = Param(name="testparam1", annotation=Union[None, float])
assert p1.datatypes == [type(None), float]
assert p1.help_type == "[NoneType, float]"
assert p1.optional is True
assert p1.required is False

p2 = Param(name="testparam2", annotation=None | bool)
assert p2.datatypes == [type(None), bool]
assert p2.help_type == "[NoneType, bool]"


def test_parse_or_prepend_description():
p1 = Param(name="testparam1")
assert p1.description == ""
p1.parse_or_prepend(" testparam1, # stuff and things")
assert p1.description == "stuff and things"


def test_parse_or_prepend_optional():
p1 = Param(name="testparam1", annotation=Optional[str])
assert p1.optional is True
assert p1.required is False
p1.parse_or_prepend(" testparam1: Optional[str],")
assert p1.optional is True
assert p1.required is False


def test_parse_or_prepend_union_none():
p1 = Param(name="testparam1", annotation=Union[None, str])
assert p1.optional is True
assert p1.required is False
p1.parse_or_prepend(" testparam1: Union[None, str], # blarg")
assert p1.optional is True
assert p1.required is False


def test_parse_or_prepend_uniontype_none():
p1 = Param(name="testparam1", annotation=None | str)
assert p1.optional is True
assert p1.required is False
p1.parse_or_prepend(" testparam1: UnionNone | str, # blarg")
assert p1.optional is True
assert p1.required is False

0 comments on commit 3ce38b3

Please sign in to comment.