From 7f02ed71bedbdc3996abde5ce62128648ad25bfe Mon Sep 17 00:00:00 2001 From: Daniel Townsend Date: Sat, 28 Jun 2025 11:46:36 +0100 Subject: [PATCH 1/4] add tests for new optional syntax (`|`) --- example_script.py | 11 +++ targ/__init__.py | 8 +- tests/test_command.py | 182 +++++++++++++++++++++++++++++++----------- 3 files changed, 152 insertions(+), 49 deletions(-) diff --git a/example_script.py b/example_script.py index 1f4267e..d8bd1ec 100644 --- a/example_script.py +++ b/example_script.py @@ -139,6 +139,16 @@ def raise_error(): raise ValueError("Something went wrong!") +def optional_value(value: Optional[int] = None): + """ + A command which accepts an optional value. + """ + if value: + print(value + 1) + else: + print("Unknown") + + if __name__ == "__main__": cli = CLI() cli.register(say_hello) @@ -152,4 +162,5 @@ def raise_error(): cli.register(add, command_name="sum") cli.register(print_address) cli.register(raise_error) + cli.register(optional_value) cli.run() diff --git a/targ/__init__.py b/targ/__init__.py index c9ec7fb..18e1ce3 100644 --- a/targ/__init__.py +++ b/targ/__init__.py @@ -14,6 +14,12 @@ from .format import Color, format_text, get_underline +try: + from types import NoneType +except ImportError: + NoneType = type(None) + + __VERSION__ = "0.6.0" @@ -213,7 +219,7 @@ def call_with(self, arg_class: Arguments): elif get_origin(annotation) is Union: # type: ignore # Union is used to detect Optional inner_annotations = get_args(annotation) - filtered = [i for i in inner_annotations if i is not None] + filtered = [i for i in inner_annotations if i is not NoneType] if len(filtered) == 1: annotation = filtered[0] if annotation in CONVERTABLE_TYPES: diff --git a/tests/test_command.py b/tests/test_command.py index a55265c..cb02778 100644 --- a/tests/test_command.py +++ b/tests/test_command.py @@ -1,7 +1,7 @@ import dataclasses import decimal import sys -from typing import Any, Optional +from typing import Any, Optional, Union from unittest import TestCase from unittest.mock import MagicMock, patch @@ -206,64 +206,89 @@ def test_command(arg1: bool = False): @patch("targ.CLI._get_cleaned_args") def test_optional_bool_arg(self, _get_cleaned_args: MagicMock): """ - Test command arguments which are of type Optional[bool]. + Test command arguments which are optional booleans. """ - def test_command(arg1: Optional[bool] = None): - """ - A command for testing optional boolean arguments. - """ - if arg1 is None: + def print_arg(arg): + if arg is None: print("arg1 is None") - elif arg1 is True: + elif arg is True: print("arg1 is True") - elif arg1 is False: + elif arg is False: print("arg1 is False") else: raise ValueError("arg1 is the wrong type") - cli = CLI() - cli.register(test_command) + def test_optional(arg1: Optional[bool] = None): + """ + A command for testing `Optional[bool]` arguments. + """ + print_arg(arg1) - with patch("builtins.print", side_effect=print_) as print_mock: + def test_union(arg1: Union[bool, None] = None): + """ + A command for testing `Union[bool, None]` arguments. + """ + print_arg(arg1) - configs: list[Config] = [ - Config( - params=["test_command", "--arg1"], - output="arg1 is True", - ), - Config( - params=["test_command", "--arg1=True"], - output="arg1 is True", - ), - Config( - params=["test_command", "--arg1=true"], - output="arg1 is True", - ), - Config( - params=["test_command", "--arg1=t"], - output="arg1 is True", - ), - Config( - params=["test_command", "--arg1=False"], - output="arg1 is False", - ), - Config( - params=["test_command", "--arg1=false"], - output="arg1 is False", - ), - Config( - params=["test_command", "--arg1=f"], - output="arg1 is False", - ), - Config(params=["test_command"], output="arg1 is None"), - ] + commands = [test_optional, test_union] - for config in configs: - _get_cleaned_args.return_value = config.params - cli.run() - print_mock.assert_called_with(config.output) - print_mock.reset_mock() + if sys.version_info.major == 3 and sys.version_info.minor >= 10: + + def test_union_syntax(arg1: bool | None = None): + """ + A command for testing `bool | None` arguments. + """ + print_arg(arg1) + + commands.append(test_union_syntax) + + cli = CLI() + + for command in commands: + cli.register(command) + + with patch("builtins.print", side_effect=print_) as print_mock: + for command in commands: + command_name = command.__name__ + + configs: list[Config] = [ + Config( + params=[command_name, "--arg1"], + output="arg1 is True", + ), + Config( + params=[command_name, "--arg1=True"], + output="arg1 is True", + ), + Config( + params=[command_name, "--arg1=true"], + output="arg1 is True", + ), + Config( + params=[command_name, "--arg1=t"], + output="arg1 is True", + ), + Config( + params=[command_name, "--arg1=False"], + output="arg1 is False", + ), + Config( + params=[command_name, "--arg1=false"], + output="arg1 is False", + ), + Config( + params=[command_name, "--arg1=f"], + output="arg1 is False", + ), + Config(params=[command_name], output="arg1 is None"), + ] + + for config in configs: + _get_cleaned_args.return_value = config.params + cli.run() + print_mock.assert_called_with(config.output) + print_mock.reset_mock() @patch("targ.CLI._get_cleaned_args") def test_int_arg(self, _get_cleaned_args: MagicMock): @@ -302,6 +327,67 @@ def test_command(arg1: decimal.Decimal): print_mock.assert_called_with(config.output) print_mock.reset_mock() + @patch("targ.CLI._get_cleaned_args") + def test_optional_int_arg(self, _get_cleaned_args: MagicMock): + """ + Test command arguments which are optional int. + """ + + def print_arg(arg): + if arg is None: + print("arg1 is None") + elif isinstance(arg, int): + print("arg1 is an int") + else: + raise ValueError("arg1 is the wrong type") + + def test_optional(arg1: Optional[int] = None): + """ + A command for testing `Optional[int]` arguments. + """ + print_arg(arg1) + + def test_union(arg1: Union[int, None] = None): + """ + A command for testing `Union[int, None]` arguments. + """ + print_arg(arg1) + + commands = [test_optional, test_union] + + if sys.version_info.major == 3 and sys.version_info.minor >= 10: + + def test_union_syntax(arg1: int | None = None): + """ + A command for testing `int | None` arguments. + """ + print_arg(arg1) + + commands.append(test_union_syntax) + + cli = CLI() + + for command in commands: + cli.register(command) + + with patch("builtins.print", side_effect=print_) as print_mock: + for command in commands: + command_name = command.__name__ + + configs: list[Config] = [ + Config( + params=[command_name, "--arg1=1"], + output="arg1 is an int", + ), + Config(params=[command_name], output="arg1 is None"), + ] + + for config in configs: + _get_cleaned_args.return_value = config.params + cli.run() + print_mock.assert_called_with(config.output) + print_mock.reset_mock() + @patch("targ.CLI._get_cleaned_args") def test_decimal_arg(self, _get_cleaned_args: MagicMock): """ From c9cdf65c755e89a074fe6bd3614b97cd32a3e819 Mon Sep 17 00:00:00 2001 From: Daniel Townsend Date: Wed, 9 Jul 2025 22:25:32 +0100 Subject: [PATCH 2/4] ignore type warnings --- targ/__init__.py | 3 ++- tests/test_command.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/targ/__init__.py b/targ/__init__.py index 18e1ce3..3dbe0d7 100644 --- a/targ/__init__.py +++ b/targ/__init__.py @@ -14,8 +14,9 @@ from .format import Color, format_text, get_underline +# Only available in Python 3.10 and above: try: - from types import NoneType + from types import NoneType # type: ignore except ImportError: NoneType = type(None) diff --git a/tests/test_command.py b/tests/test_command.py index cb02778..ace7cea 100644 --- a/tests/test_command.py +++ b/tests/test_command.py @@ -235,7 +235,7 @@ def test_union(arg1: Union[bool, None] = None): if sys.version_info.major == 3 and sys.version_info.minor >= 10: - def test_union_syntax(arg1: bool | None = None): + def test_union_syntax(arg1: bool | None = None): # type: ignore """ A command for testing `bool | None` arguments. """ @@ -357,7 +357,7 @@ def test_union(arg1: Union[int, None] = None): if sys.version_info.major == 3 and sys.version_info.minor >= 10: - def test_union_syntax(arg1: int | None = None): + def test_union_syntax(arg1: int | None = None): # type: ignore """ A command for testing `int | None` arguments. """ From aca6fa46354aa8091b85d3c945a60935a62ff569 Mon Sep 17 00:00:00 2001 From: Daniel Townsend Date: Wed, 9 Jul 2025 22:35:53 +0100 Subject: [PATCH 3/4] ignore type error --- targ/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/targ/__init__.py b/targ/__init__.py index 3dbe0d7..89e45c6 100644 --- a/targ/__init__.py +++ b/targ/__init__.py @@ -18,7 +18,7 @@ try: from types import NoneType # type: ignore except ImportError: - NoneType = type(None) + NoneType = type(None) # type: ignore __VERSION__ = "0.6.0" From f980867679bfb49abb78c3b8e1cf7db0169ca789 Mon Sep 17 00:00:00 2001 From: Daniel Townsend Date: Wed, 9 Jul 2025 22:49:25 +0100 Subject: [PATCH 4/4] check for `UnionType` --- targ/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/targ/__init__.py b/targ/__init__.py index 89e45c6..50cedef 100644 --- a/targ/__init__.py +++ b/targ/__init__.py @@ -16,10 +16,13 @@ # Only available in Python 3.10 and above: try: - from types import NoneType # type: ignore + from types import NoneType, UnionType # type: ignore except ImportError: NoneType = type(None) # type: ignore + class UnionType: # type: ignore + pass + __VERSION__ = "0.6.0" @@ -217,7 +220,7 @@ def call_with(self, arg_class: Arguments): if annotation in CONVERTABLE_TYPES: value = annotation(value) - elif get_origin(annotation) is Union: # type: ignore + elif get_origin(annotation) in [Union, UnionType]: # type: ignore # Union is used to detect Optional inner_annotations = get_args(annotation) filtered = [i for i in inner_annotations if i is not NoneType]