Skip to content

Commit

Permalink
Cleanly support UnionType where possible in 3.9
Browse files Browse the repository at this point in the history
    * Conditionalize some tests as 3.9 seems to not like treating UnionType as values
    * Simplify test skipping for uniontype
  • Loading branch information
inno committed May 5, 2024
1 parent 3ce38b3 commit 83708f3
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 49 deletions.
13 changes: 8 additions & 5 deletions simplecli/simplecli.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from __future__ import annotations
import contextlib
import inspect
import io
Expand All @@ -14,9 +13,6 @@
TokenInfo,
generate_tokens,
)
from types import (
UnionType,
)
from typing import (
Any,
Callable,
Expand All @@ -25,6 +21,13 @@
get_origin,
)

try:
from types import UnionType
except ImportError:
# Adapted from cpython/Lib/types.py which defines it as `int | str`.
# This is ok because if UnionType is not imported, it is not supported.
UnionType = Union[int, str] # type: ignore


# 2023 - Clif Bratcher WIP

Expand All @@ -44,7 +47,7 @@ class Empty:
class Param(inspect.Parameter):
internal_only: bool # Do not pass to wrapped function
_required: bool # Exit if a value is not present
_optional: bool | None = None # Mirrors `Optional` type
_optional: Union[bool, None] = None # Mirrors `Optional` type

def __init__(self, *argv: Any, **kwargs: Any) -> None:
kwargs["annotation"] = kwargs.pop("annotation", Empty)
Expand Down
11 changes: 3 additions & 8 deletions tests/test_help_text.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
import pytest
import sys
from simplecli.simplecli import Param, help_text
from tests.utils import skip_if_uniontype_unsupported
from typing import Optional, Union


def min_py(major: int, minor: int) -> bool:
return sys.version_info < (major, minor)


def test_help_text_union():
text = help_text(
filename="filename",
Expand All @@ -18,7 +13,7 @@ def test_help_text_union():
assert "OPTIONAL" not in text


@pytest.mark.skipif(min_py(3, 10), reason="Union Type requires >= py3.10")
@skip_if_uniontype_unsupported
def test_help_text_uniontype():
text = help_text(
filename="filename",
Expand Down Expand Up @@ -47,7 +42,7 @@ def test_help_text_union_none():
assert "OPTIONAL" in text


@pytest.mark.skipif(min_py(3, 10), reason="Union Type requires >= py3.10")
@skip_if_uniontype_unsupported
def test_help_text_uniontype_none():
text = help_text(
filename="filename",
Expand Down
24 changes: 13 additions & 11 deletions tests/test_param.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from __future__ import annotations
import pytest
import re
from simplecli.simplecli import DefaultIfBool, Empty, Param
from tests.utils import skip_if_uniontype_unsupported
from typing import Optional, Union


Expand Down Expand Up @@ -98,11 +100,14 @@ def test_set_value():
p3.set_value(DefaultIfBool)


def test_union_uniontype():
def test_union():
p1 = Param(name="testparam1", annotation=Union[str, float])
assert p1.datatypes == [str, float]
assert p1.help_type == "[str, float]"


@skip_if_uniontype_unsupported
def test_uniontype():
p2 = Param(name="testparam2", annotation=int | bool)
assert p2.datatypes == [int, bool]
assert p2.help_type == "[int, bool]"
Expand All @@ -121,7 +126,10 @@ def test_union_optional():
assert p1.optional is True
assert p1.required is False

p2 = Param(name="testparam2", annotation=None | bool)

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

Expand All @@ -146,15 +154,9 @@ 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

assert p1.description == ""

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")
p1.parse_or_prepend(" testparam1: Union[None, str], # blarg")
assert p1.optional is True
assert p1.required is False
assert p1.description == "blarg"
75 changes: 50 additions & 25 deletions tests/test_wrap.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import pytest
import re
import sys
import typing
from simplecli import simplecli
from tests.utils import skip_if_uniontype_unsupported


@pytest.fixture(autouse=True)
Expand Down Expand Up @@ -75,9 +75,9 @@ def code1(this_var: int): # stuff and things
simplecli.wrap(code1)

help_msg = e.value.args[0]
assert re.search(r"--this-var", help_msg)
assert re.search(r"\(int\)", help_msg)
assert re.search(r"stuff and things", help_msg)
assert "--this-var" in help_msg
assert "(int)" in help_msg
assert "stuff and things" in help_msg


def test_wrap_help_complex(monkeypatch):
Expand All @@ -97,12 +97,12 @@ def code(
simplecli.wrap(code)

help_msg = e.value.args[0]
assert re.search(r"--that-var", help_msg)
assert re.search(r"\[str, int\]", help_msg)
assert re.search(r"that is the var", help_msg)
assert re.search(r"--count", help_msg)
assert re.search(r"Default: 54", help_msg)
assert re.search(r"OPTIONAL", help_msg)
assert "--that-var" in help_msg
assert "[str, int]" in help_msg
assert "that is the var" in help_msg
assert "--count" in help_msg
assert "Default: 54" in help_msg
assert "OPTIONAL" in help_msg


def test_wrap_simple_type_error(monkeypatch):
Expand Down Expand Up @@ -144,11 +144,11 @@ def code2(this_var: int): # stuff and things
simplecli.wrap(code2)

help_msg = e.value.args[0]
assert not re.search(r"Description:", help_msg)
assert not re.search(r"Version: 1.2.3", help_msg)
assert re.search(r"--this-var", help_msg)
assert re.search(r"\(int\)", help_msg)
assert re.search(r"stuff and things", help_msg)
assert "Description:" not in help_msg
assert "Version: 1.2.3" not in help_msg
assert "--this-var" in help_msg
assert "(int)" in help_msg
assert "stuff and things" in help_msg


def test_wrap_version_exists(monkeypatch):
Expand All @@ -165,11 +165,11 @@ def code1(this_var: int): # stuff and things
simplecli.wrap(code1)

help_msg = e.value.args[0]
assert re.search(r"Version: 1.2.3", help_msg)
assert not re.search(r"Description:", help_msg)
assert not re.search(r"--this-var", help_msg)
assert not re.search(r"\(int\)", help_msg)
assert not re.search(r"stuff and things", help_msg)
assert "Version: 1.2.3" in help_msg
assert "Description:" not in help_msg
assert "--this-var" not in help_msg
assert "(int)" not in help_msg
assert "stuff and things" not in help_msg


def test_docstring(monkeypatch):
Expand All @@ -187,8 +187,33 @@ def code2(this_var: int): # stuff and things
simplecli.wrap(code2)

help_msg = e.value.args[0]
assert re.search(r"Description:", help_msg)
assert re.search(r"this is a description", help_msg)
assert re.search(r"--this-var", help_msg)
assert re.search(r"\(int\)", help_msg)
assert re.search(r"stuff and things", help_msg)
assert "Description:" in help_msg
assert "this is a description" in help_msg
assert "--this-var" in help_msg
assert "(int)" in help_msg
assert "stuff and things" in help_msg


@skip_if_uniontype_unsupported
def test_wrap_uniontype(monkeypatch):
monkeypatch.setattr(sys, "argv", ["filename", "--help"])

def code(
that_var: str | int, # that is the var
count: int = 54, # number of things
):
pass

with (
PatchGlobal(code, "__name__", "__main__"),
pytest.raises(SystemExit) as e,
):
simplecli.wrap(code)

help_msg = e.value.args[0]
assert "--that-var" in help_msg
assert "[str, int]" in help_msg
assert "that is the var" in help_msg
assert "--count" in help_msg
assert "Default: 54" in help_msg
assert "OPTIONAL" not in help_msg
21 changes: 21 additions & 0 deletions tests/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import sys
import pytest
from functools import wraps
from typing import Any, Callable


def min_py(major: int, minor: int) -> bool:
return sys.version_info < (major, minor)


def skip_if_uniontype_unsupported(
func: Callable[..., Any],
) -> Callable[..., Any]:
@wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> None:
if min_py(3, 10):
pytest.skip("UnionType requires >= py3.10")
return None
return func(*args, **kwargs)

return wrapper

0 comments on commit 83708f3

Please sign in to comment.