Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] tapify with subparsers #140

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ pytest
- [Function](#function)
- [Class](#class)
- [Dataclass](#dataclass)
- [Pydantic](#pydantic)
+ [tapify help](#tapify-help)
+ [Command line vs explicit arguments](#command-line-vs-explicit-arguments)
+ [Known args](#known-args)
Expand Down
3 changes: 2 additions & 1 deletion tap/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
from argparse import ArgumentError, ArgumentTypeError
from tap._version import __version__
from tap.tap import Tap
from tap.tapify import tapify, to_tap_class
from tap.tapify import tapify, tapify_with_subparsers, to_tap_class

__all__ = [
"ArgumentError",
"ArgumentTypeError",
"Tap",
"tapify",
"tapify_with_subparsers",
"to_tap_class",
"__version__",
]
52 changes: 52 additions & 0 deletions tap/tapify.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"""

import dataclasses
from functools import partial
import inspect
from typing import Any, Callable, Dict, List, Optional, Sequence, Type, TypeVar, Union

Expand Down Expand Up @@ -355,3 +356,54 @@ def tapify(

# Initialize the class or run the function with the parsed arguments
return class_or_function(*class_or_function_args, **class_or_function_kwargs)


def tapify_with_subparsers(class_: Type):
# Create a Tap class with subparsers defined by the class_'s methods
docstring = _docstring(class_)
param_to_description = {param.arg_name: param.description for param in docstring.params}
args_data = _tap_data(class_, param_to_description, func_kwargs={}).args_data

subparser_dest = "_tap_subparser_dest"

class TapWithSubparsers(_tap_class(args_data)):
def configure(self): # TODO: understand why overriding _configure is wrong
self.add_subparsers(
help="sub-command help", # TODO: prolly should be user-inputted instead
required=True, # If not required just use tapify
dest=subparser_dest, # Need to know which subparser (i.e., which method) is being hit by the CLI
)
for method_name in dir(class_):
method = getattr(class_, method_name)
if method_name.startswith("_") or not callable(method):
# TODO: maybe the user can input their own function (method_name: str -> bool) for deciding whether
# or not a method_name should be included as a subparser or not.
continue
subparser_tap = to_tap_class(partial(method, None))
# TODO: the partial part is a stupid fix for getting rid of self. Need to also handle static and class
# methods
self.add_subparser(
method_name,
subparser_tap,
help=f"{method_name} help", # TODO: think about how to set
description=f"{method_name} description", # TODO: think about how to set
)

# Parse the user's command
cli_args = TapWithSubparsers().parse_args()

# TODO: think about how to avoid name collisions b/t the init and method args / avoid loading everything into as_dict

# Create the class_ object
# TODO: maybe figure out how to not do this step so that the input class_ can be a module or any collection of things
# where calling dir on it gives a bunch of functions
args_for_init = {arg_data.name for arg_data in args_data}
# TODO: handle args and kwargs like we did for tapify
init_kwargs = {name: value for name, value in cli_args.as_dict().items() if name in args_for_init}
object_ = class_(**init_kwargs)

# Call the method
method = getattr(object_, getattr(cli_args, subparser_dest))
# TODO: handle args and kwargs like we did for tapify
method_kwargs = {name: value for name, value in cli_args.as_dict().items() if name not in args_for_init}
return method(**method_kwargs) # TODO: also return the object?
9 changes: 0 additions & 9 deletions tests/test_to_tap_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,9 +302,6 @@ def replace_whitespace(string: str) -> str:
assert replace_whitespace(message) == replace_whitespace(message_expected)


# Test sublcasser_simple


@pytest.mark.parametrize(
"args_string_and_arg_to_expected_value",
[
Expand Down Expand Up @@ -350,9 +347,6 @@ def test_subclasser_simple_help_message(class_or_function_: Any):
_test_subclasser_message(subclasser_simple, class_or_function_, help_message_expected, description=description)


# Test subclasser_complex


@pytest.mark.parametrize(
"args_string_and_arg_to_expected_value",
[
Expand Down Expand Up @@ -428,9 +422,6 @@ def test_subclasser_complex_help_message(class_or_function_: Any):
_test_subclasser_message(subclasser_complex, class_or_function_, help_message_expected, description=description)


# Test subclasser_subparser


@pytest.mark.parametrize(
"args_string_and_arg_to_expected_value",
[
Expand Down