Repository: swansonk14/typed-argument-parser

Issue:
[suggestion] `tapify` should create positional args for signatures w/ positional-only params
Currently, the `tapify` interface creates "options"/flags (`--` prefixed, position-agnostic arguments) for each parameter in the function signature. e.g.:

```python
from tap import tapify
def foo(num1: int, num2: float, /, bar: str) -> None:
    ...

if __name__ == "__main__":
    tapify(foo)
```

produces:

```
usage: ... [-h] --num1 NUM1 --num2 NUM2 --bar BAR

options:
  -h, --help   show this help message and exit
  --num1 NUM1  (int, required)
  --num2 NUM2  (float, required)
  --bar BAR    (str, required)
```

Since parameters that follow `/` are [positional-only](https://peps.python.org/pep-0570/), `tapify` should omit the `--` prefix when it calls `add_argument(...)`, so that `argparse` creates a positional argument.

Discussion:

Patch:
diff --git a/tap/tapify.py b/tap/tapify.py
index 9f7909f..fb39f91 100644
--- a/tap/tapify.py
+++ b/tap/tapify.py
@@ -53,11 +53,14 @@ class _ArgData:
     "Whether or not the argument must be passed in"
 
     default: Any
-    "Value of the argument if the argument isn't passed in. This gets ignored if is_required"
+    "Value of the argument if the argument isn't passed in. This gets ignored if `is_required`"
 
     description: Optional[str] = ""
     "Human-readable description of the argument"
 
+    is_positional_only: bool = False
+    "Whether or not the argument must be provided positionally"
+
 
 @dataclasses.dataclass(frozen=True)
 class _TapData:
@@ -218,6 +221,7 @@ def _tap_data_from_class_or_function(
             is_required=is_required,
             default=default,
             description=param_to_description.get(param.name),
+            is_positional_only=param.kind == inspect.Parameter.POSITIONAL_ONLY,
         )
         args_data.append(arg_data)
     return _TapData(args_data, has_kwargs, known_only)
@@ -255,6 +259,10 @@ def _tap_data(class_or_function: _ClassOrFunction, param_to_description: Dict[st
 
 
 def _tap_class(args_data: Sequence[_ArgData]) -> Type[Tap]:
+    """
+    Transfers argument data to a :class:`tap.Tap` class. Arguments will be added to the parser on initialization.
+    """
+
     class ArgParser(Tap):
         # Overwriting configure would force a user to remember to call super().configure if they want to overwrite it
         # Instead, overwrite _configure
@@ -298,14 +306,14 @@ def tapify(
 
     :param class_or_function: The class or function to run with the provided arguments.
     :param known_only: If true, ignores extra arguments and only parses known arguments.
-    :param command_line_args: A list of command line style arguments to parse (e.g., ['--arg', 'value']).
-                              If None, arguments are parsed from the command line (default behavior).
-    :param explicit_bool: Booleans can be specified on the command line as "--arg True" or "--arg False"
-                        rather than "--arg". Additionally, booleans can be specified by prefixes of True and False
-                        with any capitalization as well as 1 or 0.
-    :param func_kwargs: Additional keyword arguments for the function. These act as default values when
-                        parsing the command line arguments and overwrite the function defaults but
-                        are overwritten by the parsed command line arguments.
+    :param command_line_args: A list of command line style arguments to parse (e.g., ['--arg', 'value']). If None,
+                              arguments are parsed from the command line (default behavior).
+    :param explicit_bool: Booleans can be specified on the command line as "--arg True" or "--arg False" rather than
+                          "--arg". Additionally, booleans can be specified by prefixes of True and False with any
+                          capitalization as well as 1 or 0.
+    :param func_kwargs: Additional keyword arguments for the function. These act as default values when parsing the
+                        command line arguments and overwrite the function defaults but are overwritten by the parsed
+                        command line arguments.
     """
     # We don't directly call to_tap_class b/c we need tap_data, not just tap_class
     docstring = _docstring(class_or_function)
@@ -324,13 +332,21 @@ def tapify(
     # Parse command line arguments
     command_line_args: Tap = tap.parse_args(args=command_line_args, known_only=known_only)
 
-    # Get command line arguments as a dictionary
+    # Prepare command line arguments for class_or_function, respecting positional-only args
+    class_or_function_args: list[Any] = []
+    class_or_function_kwargs: Dict[str, Any] = {}
     command_line_args_dict = command_line_args.as_dict()
+    for arg_data in tap_data.args_data:
+        arg_value = command_line_args_dict[arg_data.name]
+        if arg_data.is_positional_only:
+            class_or_function_args.append(arg_value)
+        else:
+            class_or_function_kwargs[arg_data.name] = arg_value
 
     # Get **kwargs from extra command line arguments
     if tap_data.has_kwargs:
         kwargs = {tap.extra_args[i].lstrip("-"): tap.extra_args[i + 1] for i in range(0, len(tap.extra_args), 2)}
-        command_line_args_dict.update(kwargs)
+        class_or_function_kwargs.update(kwargs)
 
     # Initialize the class or run the function with the parsed arguments
-    return class_or_function(**command_line_args_dict)
+    return class_or_function(*class_or_function_args, **class_or_function_kwargs)


Test patch:
diff --git a/tests/test_tapify.py b/tests/test_tapify.py
index 5c69b97..612accd 100644
--- a/tests/test_tapify.py
+++ b/tests/test_tapify.py
@@ -85,6 +85,9 @@ class TapifyTests(TestCase):
         def concat(a: int, simple: str, test: float, of: float, types: bool) -> str:
             return f"{a} {simple} {test} {of} {types}"
 
+        def concat_with_positionals(a: int, simple: str, test: float, of: float, types: bool, /) -> str:
+            return f"{a} {simple} {test} {of} {types}"
+
         class Concat:
             def __init__(self, a: int, simple: str, test: float, of: float, types: bool):
                 self.kwargs = {"a": a, "simple": simple, "test": test, "of": of, "types": types}
@@ -92,6 +95,13 @@ class TapifyTests(TestCase):
             def __eq__(self, other: str) -> bool:
                 return other == concat(**self.kwargs)
 
+        class ConcatWithPositionals:
+            def __init__(self, a: int, simple: str, test: float, of: float, types: bool, /):
+                self.kwargs = {"a": a, "simple": simple, "test": test, "of": of, "types": types}
+
+            def __eq__(self, other: str) -> bool:
+                return other == concat(**self.kwargs)
+
         @dataclass
         class ConcatDataclass:
             a: int
@@ -130,7 +140,13 @@ class TapifyTests(TestCase):
         else:
             pydantic_data_models = []
 
-        for class_or_function in [concat, Concat, ConcatDataclass] + pydantic_data_models:
+        for class_or_function in [
+            concat,
+            concat_with_positionals,
+            Concat,
+            ConcatWithPositionals,
+            ConcatDataclass,
+        ] + pydantic_data_models:
             output = tapify(
                 class_or_function,
                 command_line_args=["--a", "1", "--simple", "simple", "--test", "3.14", "--of", "2.718", "--types"],
@@ -142,6 +158,11 @@ class TapifyTests(TestCase):
         def concat(a: int, simple: str, test: float, of: float = -0.3, types: bool = False, wow: str = "abc") -> str:
             return f"{a} {simple} {test} {of} {types} {wow}"
 
+        def concat_with_positionals(
+            a: int, simple: str, test: float, /, of: float = -0.3, types: bool = False, wow: str = "abc"
+        ) -> str:
+            return f"{a} {simple} {test} {of} {types} {wow}"
+
         class Concat:
             def __init__(
                 self, a: int, simple: str, test: float, of: float = -0.3, types: bool = False, wow: str = "abc"
@@ -151,6 +172,15 @@ class TapifyTests(TestCase):
             def __eq__(self, other: str) -> bool:
                 return other == concat(**self.kwargs)
 
+        class ConcatWithPositionals:
+            def __init__(
+                self, a: int, simple: str, test: float, /, of: float = -0.3, types: bool = False, wow: str = "abc"
+            ):
+                self.kwargs = {"a": a, "simple": simple, "test": test, "of": of, "types": types, "wow": wow}
+
+            def __eq__(self, other: str) -> bool:
+                return other == concat(**self.kwargs)
+
         @dataclass
         class ConcatDataclass:
             a: int
@@ -192,8 +222,13 @@ class TapifyTests(TestCase):
         else:
             pydantic_data_models = []
 
-        for class_or_function in [concat, Concat, ConcatDataclass] + pydantic_data_models:
-            print(class_or_function.__name__)
+        for class_or_function in [
+            concat,
+            concat_with_positionals,
+            Concat,
+            ConcatWithPositionals,
+            ConcatDataclass,
+        ] + pydantic_data_models:
             output = tapify(
                 class_or_function,
                 command_line_args=["--a", "1", "--simple", "simple", "--test", "3.14", "--types", "--wow", "wee"],
@@ -205,6 +240,9 @@ class TapifyTests(TestCase):
         def concat(complexity: List[str], requires: Tuple[int, int], intelligence: Person) -> str:
             return f'{" ".join(complexity)} {requires[0]} {requires[1]} {intelligence}'
 
+        def concat_with_positionals(complexity: List[str], /, requires: Tuple[int, int], intelligence: Person) -> str:
+            return f'{" ".join(complexity)} {requires[0]} {requires[1]} {intelligence}'
+
         class Concat:
             def __init__(self, complexity: List[str], requires: Tuple[int, int], intelligence: Person):
                 self.kwargs = {"complexity": complexity, "requires": requires, "intelligence": intelligence}
@@ -212,6 +250,13 @@ class TapifyTests(TestCase):
             def __eq__(self, other: str) -> bool:
                 return other == concat(**self.kwargs)
 
+        class ConcatWithPositionals:
+            def __init__(self, complexity: List[str], /, requires: Tuple[int, int], intelligence: Person):
+                self.kwargs = {"complexity": complexity, "requires": requires, "intelligence": intelligence}
+
+            def __eq__(self, other: str) -> bool:
+                return other == concat(**self.kwargs)
+
         @dataclass
         class ConcatDataclass:
             complexity: List[str]
@@ -252,7 +297,13 @@ class TapifyTests(TestCase):
         else:
             pydantic_data_models = []
 
-        for class_or_function in [concat, Concat, ConcatDataclass] + pydantic_data_models:
+        for class_or_function in [
+            concat,
+            concat_with_positionals,
+            Concat,
+            ConcatWithPositionals,
+            ConcatDataclass,
+        ] + pydantic_data_models:
             output = tapify(
                 class_or_function,
                 command_line_args=[
@@ -277,6 +328,9 @@ class TapifyTests(TestCase):
         def concat(complexity: list[int], requires: tuple[int, int], intelligence: Person) -> str:
             return f'{" ".join(map(str, complexity))} {requires[0]} {requires[1]} {intelligence}'
 
+        def concat_with_positionals(complexity: list[int], requires: tuple[int, int], /, intelligence: Person) -> str:
+            return f'{" ".join(map(str, complexity))} {requires[0]} {requires[1]} {intelligence}'
+
         class Concat:
             def __init__(self, complexity: list[int], requires: tuple[int, int], intelligence: Person):
                 self.kwargs = {"complexity": complexity, "requires": requires, "intelligence": intelligence}
@@ -284,6 +338,13 @@ class TapifyTests(TestCase):
             def __eq__(self, other: str) -> bool:
                 return other == concat(**self.kwargs)
 
+        class ConcatWithPositionals:
+            def __init__(self, complexity: list[int], requires: tuple[int, int], /, intelligence: Person):
+                self.kwargs = {"complexity": complexity, "requires": requires, "intelligence": intelligence}
+
+            def __eq__(self, other: str) -> bool:
+                return other == concat(**self.kwargs)
+
         @dataclass
         class ConcatDataclass:
             complexity: list[int]
@@ -324,7 +385,13 @@ class TapifyTests(TestCase):
         else:
             pydantic_data_models = []
 
-        for class_or_function in [concat, Concat, ConcatDataclass] + pydantic_data_models:
+        for class_or_function in [
+            concat,
+            concat_with_positionals,
+            Concat,
+            ConcatWithPositionals,
+            ConcatDataclass,
+        ] + pydantic_data_models:
             output = tapify(
                 class_or_function,
                 command_line_args=[
@@ -352,6 +419,16 @@ class TapifyTests(TestCase):
         ) -> str:
             return f'{" ".join(complexity)} {requires[0]} {requires[1]} {intelligence} {maybe} {possibly}'
 
+        def concat_with_positionals(
+            complexity: List[str],
+            requires: Tuple[int, int] = (2, 5),
+            intelligence: Person = Person("kyle"),
+            maybe: Optional[str] = None,
+            possibly: Optional[str] = None,
+            /,
+        ) -> str:
+            return f'{" ".join(complexity)} {requires[0]} {requires[1]} {intelligence} {maybe} {possibly}'
+
         class Concat:
             def __init__(
                 self,
@@ -372,6 +449,27 @@ class TapifyTests(TestCase):
             def __eq__(self, other: str) -> bool:
                 return other == concat(**self.kwargs)
 
+        class ConcatWithPositionals:
+            def __init__(
+                self,
+                complexity: List[str],
+                requires: Tuple[int, int] = (2, 5),
+                intelligence: Person = Person("kyle"),
+                maybe: Optional[str] = None,
+                possibly: Optional[str] = None,
+                /,
+            ):
+                self.kwargs = {
+                    "complexity": complexity,
+                    "requires": requires,
+                    "intelligence": intelligence,
+                    "maybe": maybe,
+                    "possibly": possibly,
+                }
+
+            def __eq__(self, other: str) -> bool:
+                return other == concat(**self.kwargs)
+
         @dataclass
         class ConcatDataclass:
             complexity: List[str]
@@ -418,7 +516,13 @@ class TapifyTests(TestCase):
         else:
             pydantic_data_models = []
 
-        for class_or_function in [concat, Concat, ConcatDataclass] + pydantic_data_models:
+        for class_or_function in [
+            concat,
+            concat_with_positionals,
+            Concat,
+            ConcatWithPositionals,
+            ConcatDataclass,
+        ] + pydantic_data_models:
             output = tapify(
                 class_or_function,
                 command_line_args=[
@@ -440,6 +544,9 @@ class TapifyTests(TestCase):
         def concat(so: int, many: float, args: str) -> str:
             return f"{so} {many} {args}"
 
+        def concat_with_positionals(so: int, many: float, /, args: str) -> str:
+            return f"{so} {many} {args}"
+
         class Concat:
             def __init__(self, so: int, many: float, args: str):
                 self.kwargs = {"so": so, "many": many, "args": args}
@@ -447,6 +554,13 @@ class TapifyTests(TestCase):
             def __eq__(self, other: str) -> bool:
                 return other == concat(**self.kwargs)
 
+        class ConcatWithPositionals:
+            def __init__(self, so: int, many: float, /, args: str):
+                self.kwargs = {"so": so, "many": many, "args": args}
+
+            def __eq__(self, other: str) -> bool:
+                return other == concat(**self.kwargs)
+
         @dataclass
         class ConcatDataclass:
             so: int
@@ -479,7 +593,13 @@ class TapifyTests(TestCase):
         else:
             pydantic_data_models = []
 
-        for class_or_function in [concat, Concat, ConcatDataclass] + pydantic_data_models:
+        for class_or_function in [
+            concat,
+            concat_with_positionals,
+            Concat,
+            ConcatWithPositionals,
+            ConcatDataclass,
+        ] + pydantic_data_models:
             with self.assertRaises(SystemExit):
                 tapify(class_or_function, command_line_args=["--so", "23", "--many", "9.3"])
 
@@ -487,6 +607,9 @@ class TapifyTests(TestCase):
         def concat(so: int, few: float) -> str:
             return f"{so} {few}"
 
+        def concat_with_positionals(so: int, few: float, /) -> str:
+            return f"{so} {few}"
+
         class Concat:
             def __init__(self, so: int, few: float):
                 self.kwargs = {"so": so, "few": few}
@@ -494,6 +617,13 @@ class TapifyTests(TestCase):
             def __eq__(self, other: str) -> bool:
                 return other == concat(**self.kwargs)
 
+        class ConcatWithPositionals:
+            def __init__(self, so: int, few: float):
+                self.kwargs = {"so": so, "few": few}
+
+            def __eq__(self, other: str) -> bool:
+                return other == concat(**self.kwargs)
+
         @dataclass
         class ConcatDataclass:
             so: int
@@ -523,7 +653,13 @@ class TapifyTests(TestCase):
         else:
             pydantic_data_models = []
 
-        for class_or_function in [concat, Concat, ConcatDataclass] + pydantic_data_models:
+        for class_or_function in [
+            concat,
+            concat_with_positionals,
+            Concat,
+            ConcatWithPositionals,
+            ConcatDataclass,
+        ] + pydantic_data_models:
             with self.assertRaises(SystemExit):
                 tapify(class_or_function, command_line_args=["--so", "23", "--few", "9.3", "--args", "wow"])
 
@@ -531,6 +667,9 @@ class TapifyTests(TestCase):
         def concat(so: int, few: float) -> str:
             return f"{so} {few}"
 
+        def concat_with_positionals(so: int, few: float, /) -> str:
+            return f"{so} {few}"
+
         class Concat:
             def __init__(self, so: int, few: float):
                 self.kwargs = {"so": so, "few": few}
@@ -538,6 +677,13 @@ class TapifyTests(TestCase):
             def __eq__(self, other: str) -> bool:
                 return other == concat(**self.kwargs)
 
+        class ConcatWithPositionals:
+            def __init__(self, so: int, few: float, /):
+                self.kwargs = {"so": so, "few": few}
+
+            def __eq__(self, other: str) -> bool:
+                return other == concat(**self.kwargs)
+
         @dataclass
         class ConcatDataclass:
             so: int
@@ -567,7 +713,13 @@ class TapifyTests(TestCase):
         else:
             pydantic_data_models = []
 
-        for class_or_function in [concat, Concat, ConcatDataclass] + pydantic_data_models:
+        for class_or_function in [
+            concat,
+            concat_with_positionals,
+            Concat,
+            ConcatWithPositionals,
+            ConcatDataclass,
+        ] + pydantic_data_models:
             output = tapify(
                 class_or_function, command_line_args=["--so", "23", "--few", "9.3", "--args", "wow"], known_only=True
             )
@@ -578,6 +730,11 @@ class TapifyTests(TestCase):
         def concat(i: int, like: float, k: int, w: str = "w", args: str = "argy", always: bool = False) -> str:
             return f"{i} {like} {k} {w} {args} {always}"
 
+        def concat_with_positionals(
+            i: int, like: float, k: int, w: str = "w", /, args: str = "argy", *, always: bool = False
+        ) -> str:
+            return f"{i} {like} {k} {w} {args} {always}"
+
         class Concat:
             def __init__(self, i: int, like: float, k: int, w: str = "w", args: str = "argy", always: bool = False):
                 self.kwargs = {"i": i, "like": like, "k": k, "w": w, "args": args, "always": always}
@@ -585,6 +742,15 @@ class TapifyTests(TestCase):
             def __eq__(self, other: str) -> bool:
                 return other == concat(**self.kwargs)
 
+        class ConcatWithPositionals:
+            def __init__(
+                self, i: int, like: float, k: int, w: str = "w", /, args: str = "argy", *, always: bool = False
+            ):
+                self.kwargs = {"i": i, "like": like, "k": k, "w": w, "args": args, "always": always}
+
+            def __eq__(self, other: str) -> bool:
+                return other == concat(**self.kwargs)
+
         @dataclass
         class ConcatDataclass:
             i: int
@@ -626,7 +792,13 @@ class TapifyTests(TestCase):
         else:
             pydantic_data_models = []
 
-        for class_or_function in [concat, Concat, ConcatDataclass] + pydantic_data_models:
+        for class_or_function in [
+            concat,
+            concat_with_positionals,
+            Concat,
+            ConcatWithPositionals,
+            ConcatDataclass,
+        ] + pydantic_data_models:
             output = tapify(
                 class_or_function,
                 command_line_args=[
@@ -650,6 +822,11 @@ class TapifyTests(TestCase):
         def concat(i: int, like: float, k: int, w: str = "w", args: str = "argy", always: bool = False) -> str:
             return f"{i} {like} {k} {w} {args} {always}"
 
+        def concat_with_positionals(
+            i: int, like: float, k: int, /, *, w: str = "w", args: str = "argy", always: bool = False
+        ) -> str:
+            return f"{i} {like} {k} {w} {args} {always}"
+
         class Concat:
             def __init__(self, i: int, like: float, k: int, w: str = "w", args: str = "argy", always: bool = False):
                 self.kwargs = {"i": i, "like": like, "k": k, "w": w, "args": args, "always": always}
@@ -657,6 +834,15 @@ class TapifyTests(TestCase):
             def __eq__(self, other: str) -> bool:
                 return other == concat(**self.kwargs)
 
+        class ConcatWithPositionals:
+            def __init__(
+                self, i: int, like: float, k: int, /, *, w: str = "w", args: str = "argy", always: bool = False
+            ):
+                self.kwargs = {"i": i, "like": like, "k": k, "w": w, "args": args, "always": always}
+
+            def __eq__(self, other: str) -> bool:
+                return other == concat(**self.kwargs)
+
         @dataclass
         class ConcatDataclass:
             i: int
@@ -698,7 +884,13 @@ class TapifyTests(TestCase):
         else:
             pydantic_data_models = []
 
-        for class_or_function in [concat, Concat, ConcatDataclass] + pydantic_data_models:
+        for class_or_function in [
+            concat,
+            concat_with_positionals,
+            Concat,
+            ConcatWithPositionals,
+            ConcatDataclass,
+        ] + pydantic_data_models:
             with self.assertRaises(ValueError):
                 tapify(
                     class_or_function,
@@ -720,6 +912,9 @@ class TapifyTests(TestCase):
         def concat(problems: Problems) -> str:
             return f"{problems}"
 
+        def concat_with_positionals(problems: Problems, /) -> str:
+            return f"{problems}"
+
         class Concat:
             def __init__(self, problems: Problems):
                 self.problems = problems
@@ -727,6 +922,13 @@ class TapifyTests(TestCase):
             def __eq__(self, other: str) -> bool:
                 return other == concat(self.problems)
 
+        class ConcatWithPositionals:
+            def __init__(self, problems: Problems, /):
+                self.problems = problems
+
+            def __eq__(self, other: str) -> bool:
+                return other == concat(self.problems)
+
         @dataclass
         class ConcatDataclass:
             problems: Problems
@@ -761,7 +963,13 @@ class TapifyTests(TestCase):
         else:
             pydantic_data_models = []
 
-        for class_or_function in [concat, Concat, ConcatDataclass] + pydantic_data_models:
+        for class_or_function in [
+            concat,
+            concat_with_positionals,
+            Concat,
+            ConcatWithPositionals,
+            ConcatDataclass,
+        ] + pydantic_data_models:
             output = tapify(class_or_function, command_line_args=[], problems=Problems("oh", "no!"))
 
             self.assertEqual(output, "Problems(oh, no!)")
@@ -775,6 +983,11 @@ class TapifyTests(TestCase):
         ) -> str:
             return f"{untyped_1} {typed_1} {untyped_2} {typed_2} {untyped_3} {typed_3}"
 
+        def concat_with_positionals(
+            untyped_1, typed_1: int, untyped_2=5, typed_2: str = "now", untyped_3="hi", /, typed_3: bool = False
+        ) -> str:
+            return f"{untyped_1} {typed_1} {untyped_2} {typed_2} {untyped_3} {typed_3}"
+
         class Concat:
             def __init__(
                 self, untyped_1, typed_1: int, untyped_2=5, typed_2: str = "now", untyped_3="hi", typed_3: bool = False
@@ -791,6 +1004,29 @@ class TapifyTests(TestCase):
             def __eq__(self, other: str) -> bool:
                 return other == concat(**self.kwargs)
 
+        class ConcatWithPositionals:
+            def __init__(
+                self,
+                untyped_1,
+                typed_1: int,
+                untyped_2=5,
+                typed_2: str = "now",
+                untyped_3="hi",
+                /,
+                typed_3: bool = False,
+            ):
+                self.kwargs = {
+                    "untyped_1": untyped_1,
+                    "typed_1": typed_1,
+                    "untyped_2": untyped_2,
+                    "typed_2": typed_2,
+                    "untyped_3": untyped_3,
+                    "typed_3": typed_3,
+                }
+
+            def __eq__(self, other: str) -> bool:
+                return other == concat(**self.kwargs)
+
         @dataclass
         class ConcatDataclass:
             untyped_1: Any
@@ -838,7 +1074,13 @@ class TapifyTests(TestCase):
         else:
             pydantic_data_models = []
 
-        for class_or_function in [concat, Concat, ConcatDataclass] + pydantic_data_models:
+        for class_or_function in [
+            concat,
+            concat_with_positionals,
+            Concat,
+            ConcatWithPositionals,
+            ConcatDataclass,
+        ] + pydantic_data_models:
             output = tapify(
                 class_or_function,
                 command_line_args=[
@@ -860,6 +1102,10 @@ class TapifyTests(TestCase):
             """Concatenate three numbers."""
             return f"{a} {b} {c}"
 
+        def concat_with_positionals(a: int, b: int, c: int, /) -> str:
+            """Concatenate three numbers."""
+            return f"{a} {b} {c}"
+
         class Concat:
             """Concatenate three numbers."""
 
@@ -870,6 +1116,16 @@ class TapifyTests(TestCase):
             def __eq__(self, other: str) -> bool:
                 return other == concat(**self.kwargs)
 
+        class ConcatWithPositionals:
+            """Concatenate three numbers."""
+
+            def __init__(self, a: int, b: int, c: int, /):
+                """Concatenate three numbers."""
+                self.kwargs = {"a": a, "b": b, "c": c}
+
+            def __eq__(self, other: str) -> bool:
+                return other == concat(**self.kwargs)
+
         @dataclass
         class ConcatDataclass:
             """Concatenate three numbers."""
@@ -907,7 +1163,13 @@ class TapifyTests(TestCase):
             pydantic_data_models = [ConcatDataclassPydantic, ConcatModel]
         else:
             pydantic_data_models = []
-        for class_or_function in [concat, Concat, ConcatDataclass] + pydantic_data_models:
+        for class_or_function in [
+            concat,
+            concat_with_positionals,
+            Concat,
+            ConcatWithPositionals,
+            ConcatDataclass,
+        ] + pydantic_data_models:
             output_1 = tapify(class_or_function, command_line_args=["--a", "1", "--b", "2", "--c", "3"])
             output_2 = tapify(class_or_function, command_line_args=["--a", "4", "--b", "5", "--c", "6"])
 
@@ -918,6 +1180,9 @@ class TapifyTests(TestCase):
         def concat(a: int, *args, b: int, **kwargs) -> str:
             return f"{a} {args} {b} {kwargs}"
 
+        def concat_with_positionals(a: int, /, *args, b: int, **kwargs) -> str:
+            return f"{a} {args} {b} {kwargs}"
+
         class Concat:
             def __init__(self, a: int, *args, b: int, **kwargs):
                 self.a = a
@@ -928,7 +1193,17 @@ class TapifyTests(TestCase):
             def __eq__(self, other: str) -> bool:
                 return other == concat(a=self.a, *self.args, b=self.b, **self.kwargs)
 
-        for class_or_function in [concat, Concat]:
+        class ConcatWithPositionals:
+            def __init__(self, a: int, /, *args, b: int, **kwargs):
+                self.a = a
+                self.args = args
+                self.b = b
+                self.kwargs = kwargs
+
+            def __eq__(self, other: str) -> bool:
+                return other == concat(a=self.a, *self.args, b=self.b, **self.kwargs)
+
+        for class_or_function in [concat, concat_with_positionals, Concat, ConcatWithPositionals]:
             with self.assertRaises(SystemExit):
                 tapify(class_or_function, command_line_args=["--a", "1", "--b", "2"])
 
@@ -942,6 +1217,15 @@ class TapifyTests(TestCase):
             """
             return f"{a} {b} {c}"
 
+        def concat_with_positionals(a: int, b: int, /, c: int) -> str:
+            """Concatenate three numbers.
+
+            :param a: The first number.
+            :param b: The second number.
+            :param c: The third number.
+            """
+            return f"{a} {b} {c}"
+
         class Concat:
             def __init__(self, a: int, b: int, c: int):
                 """Concatenate three numbers.
@@ -955,6 +1239,19 @@ class TapifyTests(TestCase):
             def __eq__(self, other: str) -> bool:
                 return other == concat(**self.kwargs)
 
+        class ConcatWithPositionals:
+            def __init__(self, a: int, b: int, /, c: int):
+                """Concatenate three numbers.
+
+                :param a: The first number.
+                :param b: The second number.
+                :param c: The third number.
+                """
+                self.kwargs = {"a": a, "b": b, "c": c}
+
+            def __eq__(self, other: str) -> bool:
+                return other == concat(**self.kwargs)
+
         @dataclass
         class ConcatDataclass:
             """Concatenate three numbers.
@@ -1008,7 +1305,13 @@ class TapifyTests(TestCase):
         else:
             pydantic_data_models = []
 
-        for class_or_function in [concat, Concat, ConcatDataclass] + pydantic_data_models:
+        for class_or_function in [
+            concat,
+            concat_with_positionals,
+            Concat,
+            ConcatWithPositionals,
+            ConcatDataclass,
+        ] + pydantic_data_models:
             f = io.StringIO()
             with contextlib.redirect_stdout(f):
                 with self.assertRaises(SystemExit):
@@ -1030,7 +1333,12 @@ class TestTapifyExplicitBool(unittest.TestCase):
             """
             return Cool(is_cool)
 
-        self.cool_fun = cool
+        def cool_with_positionals(is_cool: bool = False, /) -> "Cool":
+            """cool.
+
+            :param is_cool: is it cool?
+            """
+            return Cool(is_cool)
 
         class Cool:
             def __init__(self, is_cool: bool = False):
@@ -1043,10 +1351,26 @@ class TestTapifyExplicitBool(unittest.TestCase):
             def __eq__(self, other: "Cool") -> bool:
                 return other.is_cool == cool(self.is_cool).is_cool
 
-        self.cool_class = Cool
+        class CoolWithPositionals:
+            def __init__(self, is_cool: bool = False, /):
+                """cool.
+
+                :param is_cool: is it cool?
+                """
+                self.is_cool = is_cool
+
+            def __eq__(self, other: "Cool") -> bool:
+                return other.is_cool == cool(self.is_cool).is_cool
+
+        self.class_or_functions = [
+            cool,
+            cool_with_positionals,
+            Cool,
+            CoolWithPositionals,
+        ]
 
     def test_explicit_bool_true(self):
-        for class_or_function in [self.cool_fun, self.cool_class]:
+        for class_or_function in self.class_or_functions:
             # Since the boolean argument is_cool is set to False by default and explicit_bool is False,
             # the argument is_cool is False.
             a_cool = tapify(class_or_function, command_line_args=[], explicit_bool=False)
@@ -1084,6 +1408,14 @@ class TestTapifyKwargs(unittest.TestCase):
             """
             return f'{a}_{b}_{"-".join(f"{k}={v}" for k, v in kwargs.items())}'
 
+        def concat_with_positionals(a: int, b: int = 2, /, **kwargs) -> str:
+            """Concatenate three numbers.
+
+            :param a: The first number.
+            :param b: The second number.
+            """
+            return f'{a}_{b}_{"-".join(f"{k}={v}" for k, v in kwargs.items())}'
+
         if _IS_PYDANTIC_V1 is not None:
 
             class ConcatModel(pydantic.BaseModel):
@@ -1138,7 +1470,26 @@ class TestTapifyKwargs(unittest.TestCase):
             def __eq__(self, other: str) -> bool:
                 return other == concat(self.a, self.b, **self.kwargs)
 
-        self.class_or_functions = [concat, Concat] + pydantic_data_models
+        class ConcatWithPositionals:
+            def __init__(self, a: int, /, b: int = 2, **kwargs: Dict[str, str]):
+                """Concatenate three numbers.
+
+                :param a: The first number.
+                :param b: The second number.
+                """
+                self.a = a
+                self.b = b
+                self.kwargs = kwargs
+
+            def __eq__(self, other: str) -> bool:
+                return other == concat(self.a, self.b, **self.kwargs)
+
+        self.class_or_functions = [
+            concat,
+            concat_with_positionals,
+            Concat,
+            ConcatWithPositionals,
+        ] + pydantic_data_models
 
     def test_tapify_empty_kwargs(self) -> None:
         for class_or_function in self.class_or_functions:
diff --git a/tests/test_to_tap_class.py b/tests/test_to_tap_class.py
index 2992943..7c30da6 100644
--- a/tests/test_to_tap_class.py
+++ b/tests/test_to_tap_class.py
@@ -333,23 +333,20 @@ def test_subclasser_simple(
     _test_subclasser(subclasser_simple, class_or_function_, args_string_and_arg_to_expected_value)
 
 
-# @pytest.mark.skipif(sys.version_info < (3, 10), reason="argparse is different. Need to fix help_message_expected")
 def test_subclasser_simple_help_message(class_or_function_: Any):
     description = "Script description"
     help_message_expected = f"""
-usage: pytest --arg_int ARG_INT [--arg_bool] [--arg_list [ARG_LIST {_ARG_LIST_DOTS}]] [-h]
-
-{description}
-
-{_OPTIONS_TITLE}:
-  --arg_int ARG_INT     (int, required) some integer
-  --arg_bool            (bool, default=True)
-  --arg_list [ARG_LIST {_ARG_LIST_DOTS}]
-                        ({type_to_str(Optional[List[str]])}, default=None) some list of strings
-  -h, --help            show this help message and exit
-""".lstrip(
-        "\n"
-    )
+    usage: pytest --arg_int ARG_INT [--arg_bool] [--arg_list [ARG_LIST {_ARG_LIST_DOTS}]] [-h]
+
+    {description}
+
+    {_OPTIONS_TITLE}:
+    --arg_int ARG_INT     (int, required) some integer
+    --arg_bool            (bool, default=True)
+    --arg_list [ARG_LIST {_ARG_LIST_DOTS}]
+                            ({type_to_str(Optional[List[str]])}, default=None) some list of strings
+    -h, --help            show this help message and exit
+    """
     _test_subclasser_message(subclasser_simple, class_or_function_, help_message_expected, description=description)
 
 
@@ -410,25 +407,24 @@ def test_subclasser_complex(
     _test_subclasser(subclasser_complex, class_or_function_, args_string_and_arg_to_expected_value, test_call=False)
 
 
-# @pytest.mark.skipif(sys.version_info < (3, 10), reason="argparse is different. Need to fix help_message_expected")
 def test_subclasser_complex_help_message(class_or_function_: Any):
     description = "Script description"
     help_message_expected = f"""
-usage: pytest [-arg ARGUMENT_WITH_REALLY_LONG_NAME] --arg_int ARG_INT [--arg_bool] [--arg_list [ARG_LIST {_ARG_LIST_DOTS}]] [-h]
-
-{description}
-
-{_OPTIONS_TITLE}:
-  -arg ARGUMENT_WITH_REALLY_LONG_NAME, --argument_with_really_long_name ARGUMENT_WITH_REALLY_LONG_NAME
-                        (Union[float, int], default=3) This argument has a long name and will be aliased with a short one
-  --arg_int ARG_INT     (int, required) some integer
-  --arg_bool            (bool, default=True)
-  --arg_list [ARG_LIST {_ARG_LIST_DOTS}]
-                        ({type_to_str(Optional[List[str]])}, default=None) some list of strings
-  -h, --help            show this help message and exit
-""".lstrip(
-        "\n"
-    )
+    usage: pytest [-arg ARGUMENT_WITH_REALLY_LONG_NAME] --arg_int ARG_INT [--arg_bool]
+                  [--arg_list [ARG_LIST {_ARG_LIST_DOTS}]] [-h]
+
+    {description}
+
+    {_OPTIONS_TITLE}:
+    -arg ARGUMENT_WITH_REALLY_LONG_NAME, --argument_with_really_long_name ARGUMENT_WITH_REALLY_LONG_NAME
+                            (Union[float, int], default=3) This argument has a long name and will be aliased with a short
+                            one
+    --arg_int ARG_INT     (int, required) some integer
+    --arg_bool            (bool, default=True)
+    --arg_list [ARG_LIST {_ARG_LIST_DOTS}]
+                            ({type_to_str(Optional[List[str]])}, default=None) some list of strings
+    -h, --help            show this help message and exit
+    """
     _test_subclasser_message(subclasser_complex, class_or_function_, help_message_expected, description=description)
 
 
@@ -501,47 +497,48 @@ def test_subclasser_subparser(
             "Script description",
             # foo help likely missing b/c class nesting. In a demo in a Python 3.8 env, foo help appears in -h
             f"""
-usage: pytest [--foo] --arg_int ARG_INT [--arg_bool] [--arg_list [ARG_LIST {_ARG_LIST_DOTS}]] [-h] {{a,b}} ...
-
-Script description
-
-positional arguments:
-  {{a,b}}               sub-command help
-    a                   a help
-    b                   b help
-
-{_OPTIONS_TITLE}:
-  --foo                 (bool, default=False) {'' if sys.version_info < (3, 9) else 'foo help'}
-  --arg_int ARG_INT     (int, required) some integer
-  --arg_bool            (bool, default=True)
-  --arg_list [ARG_LIST {_ARG_LIST_DOTS}]
-                        ({type_to_str(Optional[List[str]])}, default=None) some list of strings
-  -h, --help            show this help message and exit
-""",
+            usage: pytest [--foo] --arg_int ARG_INT [--arg_bool] [--arg_list [ARG_LIST {_ARG_LIST_DOTS}]] [-h]
+                          {{a,b}} ...
+
+            Script description
+
+            positional arguments:
+            {{a,b}}               sub-command help
+                a                   a help
+                b                   b help
+
+            {_OPTIONS_TITLE}:
+            --foo                 (bool, default=False) {'' if sys.version_info < (3, 9) else 'foo help'}
+            --arg_int ARG_INT     (int, required) some integer
+            --arg_bool            (bool, default=True)
+            --arg_list [ARG_LIST {_ARG_LIST_DOTS}]
+                                    ({type_to_str(Optional[List[str]])}, default=None) some list of strings
+            -h, --help            show this help message and exit
+            """,
         ),
         (
             "a -h",
             "Description (a)",
             f"""
-usage: pytest a --bar BAR [-h]
+            usage: pytest a --bar BAR [-h]
 
-Description (a)
+            Description (a)
 
-{_OPTIONS_TITLE}:
-  --bar BAR   (int, required) bar help
-  -h, --help  show this help message and exit
-""",
+            {_OPTIONS_TITLE}:
+            --bar BAR   (int, required) bar help
+            -h, --help  show this help message and exit
+            """,
         ),
         (
             "b -h",
-            "",
+            "",  # no description
             f"""
-usage: pytest b --baz {{X,Y,Z}} [-h]
+            usage: pytest b --baz {{X,Y,Z}} [-h]
 
-{_OPTIONS_TITLE}:
-  --baz {{X,Y,Z}}  (Literal['X', 'Y', 'Z'], required) baz help
-  -h, --help     show this help message and exit
-""",
+            {_OPTIONS_TITLE}:
+            --baz {{X,Y,Z}}  (Literal['X', 'Y', 'Z'], required) baz help
+            -h, --help     show this help message and exit
+            """,
         ),
     ],
 )