--- /dev/null
+[build-system]
+requires = ["setuptools>=61.0"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "proto"
+version = "0.0.1"
+authors = [
+ { name="Kyle Bowman", email="kylebowman14@gmail.com" },
+]
+description = "TODO"
+readme = "README.md"
+requires-python = ">=3.11"
+classifiers = [
+ "Programming Language :: Python :: 3",
+ "License :: OSI Approved :: MIT License",
+ "Operating System :: OS Independent",
+]
+
+[project.scripts]
+proto = "proto:main"
+
+[project.urls]
+Homepage = "None"
+Issues = "None"
\ No newline at end of file
--- /dev/null
+iniconfig==2.0.0
+packaging==23.2
+pluggy==1.4.0
+# Editable Git install (proto==0.0.1) with either a deleted local remote or invalid URI:
+# 'git@10.5.1.242:/srv/git/proto.git'
+-e /home/kyle/projects/proto
+pytest==8.0.0
--- /dev/null
+NOTE: Subparser is of same type as parser => indefinite nesting
+NOTE: You can define @instance.method wrappers
+
+# Philosophy
+* Make a nice interface that is built on and compliant with argparse
+* No dependencies outside stdlib
+
+## REQURIED ##
+### Core Idea;
+* Idea: Use a command decorator to mark a function as a command
+
+``` python
+@command(aliases=['alias1', alias2], help="help message")
+def function(arg1: type1 = default1, arg2: type2 = default 2, ...)
+ definition
+```
+
+### Subparser Ideas:
+* Idea Zero: Unless you need nesting; avoid an explicit parser.
+* Idea One: Implement command decorator as part of parser object
+
+``` python
+parser=MyArgParser(...)
+render_subparser = parser.add_subparser(...)
+@render_subparser.command(...)
+def function(arg1: type1 = default1, arg2: type2 = default 2, ...)
+ definition
+```
+
+* Idea Two: Add to command decorator an optional subparser
+
+``` python
+ parser=ArgParser(...)
+ render_subparser=parser.add_subparser()
+ @command(subparser=render_subparser, ...)
+ def function(arg1: type1 = default1, arg2: type2 = default 2, ...)
+ definition
+
+### Compose main parser
+Idea One: Custom MainCLI that does everything
+ cli = MainCLI()
+ for cmd in commands:
+ cli.register_subparser(cmd)
+
+Idea Two: Helper functions that operate on ArgumentParse
+ parser = ArgumentParser()
+ subparser = make_subparser(cmd)
+ register_subparser(parser, subparser)
+
+Probably both - Idea Two so people can use existing ArgParsers and
+Idea one for convenience
+
+### Parse and Dispatch Ideas
+
+Idea One: Add a quirk: MainCLI(), but afterwards it works nicely.
+ cli = MainCLI()
+ cmd = cli.parse()
+ cmd.run()
+
+Idea Two: The above, but also wrap it all into one command.
+ dispatch_cli()
+
+Idea Three: Can/should I make an easy interface w/o introducing MainCLI()?
+ Call into Cmd itself?
+
+###### OPTIONAL ########
+### Validation Ideas:
+1. Define a predicate data type: Callable[Any, bool]
+2. Define a validate decorator for Cmd objects
+ * Allow for multiple validators and use any(list[bool])
+ * To what extent can/should I "Parse, don't validate"?
+3. (?) Define a library of useful arg validating predicates:
+ * Consider creating argument groups?
+ * specify X or specify Y and Z?
+ * zero_or_one_of(**args)
+ * one_or_more_of(**args)
+ * zero_or_more_of(**args)
+
+ @function.validate
+ def is_valid(arg1, arg2):
+ return True if X else False
+
+### (?) Aggregate Commands
+ We need a way to fetch Cmds from their modules and register with CLI.
+ Like `instances = fetch_subclasses(Cmd)`
+ Make generic? fetch(subclasses, Cmd)
+
+### (?) Serializing Ideas
+ cmd.to_json() - packs command and args into json
+ add a to_json Cmd?
+ add by default to MainCLI() parser?
+
+ => Mirror with from_json
+
+### (?) Help documentation
+
+Idea Zero: Minimal support; use existing getters/setters
+
+Idea One: Define as dictionary and pass unpacked to @command.
+ dct = dict(help="prints a message to the screen")
+ @command(**dct)
+ def print(msg: Optional[str]=None):
+ return msg
+
+Idea Two: Define helper functions around idea Zero
+
+"""
\ No newline at end of file
--- /dev/null
+import argparse
+from typing import Optional, Callable
+from proto.utils import get_defaults, get_types
+
+# Purpose:
+# 1. Search for other commands to register with it
+ # (Enhancement) - param to define search space
+# 2. Serve as main parser - Build the parser
+# 3. Parse and dispatch to the command
+class MainCLI():
+ # NOTE: Commands are *instances* of Cmd in this framework;
+ # they are not subclasses
+ # NOTE: Idiomatically, to get instances, register each instance
+ # with the class at initialization.
+ # NOTE: To get instances, they must be initialized (at runtime),
+ # meaning that Cmd() (or the decorator) must have been run
+ # NOTE: Each function that defines a command must be imported
+ # *prior* to being able to be registered by MainCLI()
+ # NOTE: For now, let's define a module registering standard.
+ # (Enhancement) Look into a look-before-import mechanism (pluggy?)
+
+ #self.parser =
+ #self.commands = get_subclasses(Command)
+
+ # (Register parser and register w/ command dict if necessary)
+ #for cmd in self.commands:
+ # self.register_command(cmd)
+ pass
+
+class Cmd:
+ # ALT: self.__call__ = fn instead of self.run = fn
+ def __init__(self, fn: Callable):
+ self.name = fn.__name__
+ self.run = fn
+ self.types = get_types(fn)
+ self.defaults = get_defaults(fn)
+ # self.parser = get_parser(fn)
+ # self.doc = Turn docstring into parser help
+
+ @property
+ def parser(self)-> argparse.ArgumentParser:
+ pass
+
+ def run(self):
+ pass
+
+ # TODO: Modify this to modify lower level argparse stuff
+ def wrap(self, fn: Callable, *args, **kwargs):
+ return str(fn(*args, **kwargs)).upper()
+
+
+def command(fn: Callable, aliases: list = [], help: Optional[str] = None):
+ # fn.__name__
+ return Cmd(fn)
+
+@command
+def test(arg1: str = "hello"):
+ return arg1
+
+@test.wrap
+def new_name():
+ return "kyle"
+
+if __name__ == "__main__":
+ print(new_name)
\ No newline at end of file
--- /dev/null
+from typing import Callable
+import inspect
+
+def get_defaults(fn: Callable)->dict:
+ """ Returns a dictionary of parameter names and defaults. """
+ sig = inspect.signature(fn)
+ defaults = {}
+ for name, prm in sig.parameters.items():
+ # ASSUME: __init__ is defined with "self". It's a Pythonic standard to use self, but it's convention, not rule. Beware.
+ if name == "self":
+ continue
+ if prm.default == inspect._empty:
+ defaults[name] = None
+ else:
+ defaults[name] = prm.default
+ return defaults
+
+def get_types(fn: Callable)->dict[type]:
+ """ Returns a dictionary of parameter names and types. """
+ sig = inspect.signature(fn)
+ types = {}
+ for name, prm in sig.parameters.items():
+ # ASSUME: __init__ is defined with "self". It's a Pythonic standard to use self, but it's convention, not rule. Beware.
+ if name == "self":
+ continue
+ if prm.annotation == inspect._empty:
+ types[name] = None
+ else:
+ types[name] = prm.annotation
+ return types
\ No newline at end of file
--- /dev/null
+from typing import Optional
+import proto.utils
+
+##############
+# Begin Data # -- signature testing
+##############
+def fn(string, num)->str:
+ return f"String: {string} \n Integer: {num} \n"
+
+def fn_annotated(string: str, num: int)->str:
+ return f"String: {string} \n Integer: {num} \n"
+
+def fn_optional(string: Optional[str], num: Optional[int] = 42)->str:
+ return f"String: {string} \n Integer: {num} \n"
+
+def fn_default(string: str="spam", num: int = 42)->str:
+ return f"String: {string} \n Integer: {num} \n"
+
+###############
+# Begin Tests #
+###############
+def test_get_defaults():
+ defaults = proto.utils.get_defaults(fn_default)
+ assert defaults['string'] == "spam"
+ assert defaults['num'] == 42
+
+def test_get_defaults_unspecified():
+ defaults = proto.utils.get_defaults(fn)
+ assert defaults['string'] is None
+ assert defaults['num'] is None
+
+def test_get_types():
+ types = proto.utils.get_types(fn_annotated)
+ assert types['string'] == str
+ assert types['num'] == int
+
+def test_get_types_unspecified():
+ types = proto.utils.get_types(fn)
+ assert types['string'] is None
+ assert types['num'] is None
+
+def test_get_types_optional():
+ types = proto.utils.get_types(fn_optional)
+ assert types['string'] == Optional[str]
+ assert types['num'] == Optional[int]
+
+def test_validating_optional():
+ types = proto.utils.get_types(fn_optional)
+ defaults = proto.utils.get_defaults(fn_optional)
+ assert isinstance(defaults['string'],types['string'])
+ assert isinstance(defaults['num'],types['num'])