--- /dev/null
+# Interface Design Goals
+* Make a nice interface that is built on and compliant with argparse
+* No dependencies outside stdlib
+
+## Core Ideas
+* Use a command decorator to turn a function into a command.
+* Leverage the function signature to autopopulate command info.
+
+Example:
+``` python
+@command(aliases=['alias1', alias2], help="help message")
+def function(arg1: type1 = default1, arg2: type2 = default 2, ...)
+ definition
+```
+
+## MainCLI Overview
+* We want to support, but not mandate subparsers.
+* The `MainCLI` class provides the top level parser and automatically registers
+commands and subparsers.
+* By default, `@command` adds the function to the `MainCLI`, but users can also
+designate a function for use by a subparser.
+* Commands are parsed and dispatched by one of two means:
+ * initializing `MainCLI`, invoking `MainCLI().parse()` to get the command,
+ then invoking `Command.run()`.
+ * using a convenience `dispatch()` method that carries out the above steps for you.
+
+### Parser Construction: Designating a function for a subparser
+* Unless you need nesting; avoid mentioning an explicit parser.
+* If you need subparsing, add `subparser=cmd_parser` to the `@command` decorator.
+
+Example:
+``` python
+parser=ArgParser(...)
+render_subparser=parser.add_subparser()
+@command(subparser=render_subparser, ...)
+def function(arg1: type1 = default1, arg2: type2 = default 2, ...)
+ definition
+```
+
+Ruled out: Invoke the command decorator as part of a subparser
+
+Example:
+``` python
+parser=MyArgParser(...)
+render_subparser = parser.add_subparser(...)
+@render_subparser.command(...)
+def function(arg1: type1 = default1, arg2: type2 = default 2, ...)
+ definition
+```
+
+We are ruling this out because it requires extending `argparse.ArgParser`
+which violates our "comply with `argparse`" guideline.
+
+### Parser Construction: Adding a subparser to the library entry point
+
+Add a `MainCLI` class that automatically registers declared commands.
+
+Example:
+``` python
+cli = MainCLI()
+for cmd in commands:
+ cli.register_subparser(cmd)
+```
+
+Create helper functions that operate on `ArgumentParser`
+
+Example:
+``` python
+parser = ArgumentParser()
+subparser = make_subparser(cmd)
+register_subparser(parser, subparser)
+```
+
+### Parser Dispatch: Parsing and Dispatching
+* Invoke `MainCLI` methods.
+``` python
+if __name__ == "__main__":
+ cli = MainCLI()
+ cmd = cli.parse()
+ cmd.run()
+```
+
+* Invoke convenience wrapper
+``` python
+if __name__ == "__main__":
+ dispatch_cli()
+```
+
+# Possible Enhancements
+
+## (?) Distribute a CLI testing library
+* Shell out?
+* Mock sys.argv?
+* Don't bother?
+
+## 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)
+
+``` python
+@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
+This dealas with two things:
+* How to manually specify command help?
+* How to automatically generate some kind of help?
+
+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
+
+## (?) Autoimport flexibility
+Enhancement to MainCLI - add a param that enables users to define
+the command search space.jK:W
\ No newline at end of file
--- /dev/null
+# Findings from experiments
+
+## About Parsers
+* Subparsers are of same type as parser (`argparse.ArgumentParser`)
+* Corollary: You can nest subparsers indefinitely
+
+## About Decorators
+* You can define @instance.method wrappers
+* This might be useful to do lower level argparse stuff, like setting help info.
+
+Example:
+With `Command.wrap` defined as:
+``` python
+# Part of class Command:
+def wrap(self, fn: Callable, *args, **kwargs):
+ return str(fn(*args, **kwargs)).upper()
+```
+
+You can enhance the function decorate the functino `new_name` :
+``` python
+@test.wrap
+def new_name():
+ return "kyle"
+```
+
+Invoke it as follows:
+``` python
+if __name__ == "__main__":
+ print(new_name) # "KYLE"
+```
+
+# About MainCLI
+Purpose:
+1. Search for other commands to register with it
+2. Serve as main parser - Build the parser
+3. Parse and dispatch to the command
+
+Some additional notes:
+* Commands are *instances* of Cmd in this framework;
+they are not subclasses
+* Idiomatically, to get instances, register each instance
+with the class at initialization.
+* To get instances, they must be initialized (at runtime),
+meaning that Cmd() (or the decorator) must have been run
+* Each function that defines a command must be imported
+*prior* to being able to be registered by MainCLI()
+* For now, let's define a module registering standard.
+(Enhancement) Look into a look-before-import mechanism (pluggy?)
\ No newline at end of file
-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
+## Stories
+1. Create `Command` object and `@command` decorator.
+ * What data does `Command` require? (Look at argparse requirements)
+ * What methods does `Command` require?
+ * get_parser(fn: Callable)->argparse.ArgParser
+ * get_types(fn: Callable)->dict[type]
+ * get_defaults(fn: Callable)->dict
+ * init(fn: Callable, ?)->Self
+ * run()->Any:
+ * What additional info doese `@command` need?
+ * Tests:
+ * Check each builder method happy and sad paths.
+ * Check `Command.run()` output.
+ * Maybe shell out at least once.
+2. Add subparser arg to `@command` decorator (and `Command`?).
+3. Add `MainCLI()` (and dispatch convenience method.)
+ * Add `Command` registering here.
+4. Look into enhancements
+from proto.prototype import Command, command
+
+__all__ = ['Command', 'command']
\ No newline at end of file
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
+# NOTE: Currently, Command has no way to specify fn arguments.
+# NOTE: Args for a command are going to come from CLI.
+# Which means there will be a parser.parse_arguments() call.
+# NOTE: To test a Command whose function takes arguments, we
+# will need to mock parser.parse_arguments.
+class Command:
def __init__(self, fn: Callable):
self.name = fn.__name__
- self.run = fn
- self.types = get_types(fn)
- self.defaults = get_defaults(fn)
+ # self.types = get_types(fn)
+ # self.defaults = get_defaults(fn)
+ # (Property?) self.run = fn
# self.parser = get_parser(fn)
- # self.doc = Turn docstring into parser help
@property
def parser(self)-> argparse.ArgumentParser:
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
+ return Command(fn)
+
+#class MainCLI():
+#
+# # Signature?
+# def __init__(self):
+# # self.parser = # default parser
+# # self.commands = get_commands()
+# pass
+#
+# def parse(self)->Command:
+# pass
\ No newline at end of file
+import pytest
+from proto import command, Command
+from typing import Protocol
+
+
+class Stringable(Protocol):
+
+ def __str__(self)->str:
+ ...
+
+def echo(arg: Stringable)->str:
+ return "Command" + str(arg)
+
+@command
+def echo_decorated(arg: Stringable)->str:
+ return "@command" + str(arg)
+
+def test_attributes():
+ cmd = Command(echo)
+ assert cmd.name == "echo"
+
+def test_attributes_2():
+ cmd = echo_decorated
+ assert cmd.name == "echo_decorated"
\ No newline at end of file