From eba0c0ad8dd4eaf747da9d6de9ebe116a3e1d4fa Mon Sep 17 00:00:00 2001 From: Damien George Date: Fri, 5 Jun 2015 23:21:03 +0100 Subject: [PATCH] argparse: Initial minimal implementation of argparse. CPython's argparse needs around 160k heap just for the module, and has heaps of dependencies. This minimal version can run in 16k heap and has only sys and namedtuple dependencies (which are builtin to MicroPython). --- argparse/argparse.py | 176 ++++++++++++++++++++++++++++++++++++++ argparse/metadata.txt | 7 +- argparse/setup.py | 8 +- argparse/test_argparse.py | 33 +++++++ 4 files changed, 217 insertions(+), 7 deletions(-) create mode 100644 argparse/test_argparse.py diff --git a/argparse/argparse.py b/argparse/argparse.py index e69de29b..f89cc4ce 100644 --- a/argparse/argparse.py +++ b/argparse/argparse.py @@ -0,0 +1,176 @@ +""" +Minimal and functional version of CPython's argparse module. +""" + +import sys +from _collections import namedtuple + +class _ArgError(BaseException): + pass + +class _Arg: + def __init__(self, name, dest, action, nargs, const, default, help): + self.name = name + self.dest = dest + self.action = action + self.nargs = nargs + self.const = const + self.default = default + self.help = help + + def parse(self, args): + # parse args for this arg + if self.action == "store": + if self.nargs == None: + if args: + return args.pop(0) + else: + raise _ArgError("expecting value for %s" % self.name) + elif self.nargs == "?": + if args: + return args.pop(0) + else: + return self.default + else: + if self.nargs == "*": + n = -1 + elif self.nargs == "+": + if not args: + raise _ArgError("expecting value for %s" % self.name) + n = -1 + else: + n = int(self.nargs) + ret = [] + stop_at_opt = True + while args and n != 0: + if stop_at_opt and args[0].startswith("-") and args[0] != "-": + if args[0] == "--": + stop_at_opt = False + args.pop(0) + else: + break + else: + ret.append(args.pop(0)) + n -= 1 + if n > 0: + raise _ArgError("expecting value for %s" % self.name) + return ret + elif self.action == "store_const": + return self.const + else: + assert False + +class ArgumentParser: + def __init__(self, *, description): + self.description = description + self.opt = [] + self.pos = [] + + def add_argument(self, name, **kwargs): + action = kwargs.get("action", "store") + if action == "store_true": + action = "store_const" + const = True + default = kwargs.get("default", False) + elif action == "store_false": + action = "store_const" + const = False + default = kwargs.get("default", True) + else: + const = kwargs.get("const", None) + default = kwargs.get("default", None) + if name.startswith("-"): + list = self.opt + if name.startswith("--"): + dest = kwargs.get("dest", name[2:]) + else: + dest = kwargs.get("dest", name[1:]) + else: + list = self.pos + dest = kwargs.get("dest", name) + list.append( + _Arg(name, dest, action, kwargs.get("nargs", None), + const, default, kwargs.get("help", ""))) + + def usage(self, full): + # print short usage + print("usage: %s [-h]" % sys.argv[0], end="") + def render_arg(arg): + if arg.action == "store": + if arg.nargs is None: + return " %s" % arg.dest + if isinstance(arg.nargs, int): + return " %s(x%d)" % (arg.dest, arg.nargs) + else: + return " %s%s" % (arg.dest, arg.nargs) + else: + return "" + for opt in self.opt: + print(" [%s%s]" % (opt.name, render_arg(opt)), end="") + for pos in self.pos: + print(render_arg(pos), end="") + print() + + if not full: + return + + # print full information + print() + print(self.description) + if self.pos: + print("\npositional args:") + for pos in self.pos: + print(" %-16s%s" % (pos.name, pos.help)) + print("\noptional args:") + print(" -h, --help show this message and exit") + for opt in self.opt: + print(" %-16s%s" % (opt.name + render_arg(opt), opt.help)) + + def parse_args(self, args=None): + if args is None: + args = sys.argv[1:] + else: + args = args[:] + try: + return self._parse_args(args) + except _ArgError as e: + self.usage(False) + print("error:", e) + sys.exit(2) + + def _parse_args(self, args): + # add optional args with defaults + arg_dest = [] + arg_vals = [] + for opt in self.opt: + arg_dest.append(opt.dest) + arg_vals.append(opt.default) + + # parse all args + parsed_pos = False + while args or not parsed_pos: + if args and args[0].startswith("-") and args[0] != "-" and args[0] != "--": + # optional arg + a = args.pop(0) + if a in ("-h", "--help"): + self.usage(True) + sys.exit(0) + found = False + for i, opt in enumerate(self.opt): + if a == opt.name: + arg_vals[i] = opt.parse(args) + found = True + break + if not found: + raise _ArgError("unknown option %s" % a) + else: + # positional arg + if parsed_pos: + raise _ArgError("extra args: %s" % " ".join(args)) + for pos in self.pos: + arg_dest.append(pos.dest) + arg_vals.append(pos.parse(args)) + parsed_pos = True + + # build and return named tuple with arg values + return namedtuple("args", arg_dest)(*arg_vals) diff --git a/argparse/metadata.txt b/argparse/metadata.txt index 357bcc3b..2f1989c2 100644 --- a/argparse/metadata.txt +++ b/argparse/metadata.txt @@ -1,3 +1,4 @@ -srctype=dummy -type=module -version=0.0.0 +srctype = micropython-lib +type = module +version = 0.1 +author = Damien George diff --git a/argparse/setup.py b/argparse/setup.py index f8322b35..f74b2575 100644 --- a/argparse/setup.py +++ b/argparse/setup.py @@ -6,11 +6,11 @@ from setuptools import setup setup(name='micropython-argparse', - version='0.0.0', - description='Dummy argparse module for MicroPython', - long_description='This is a dummy implementation of a module for MicroPython standard library.\nIt contains zero or very little functionality, and primarily intended to\navoid import errors (using idea that even if an application imports a\nmodule, it may be not using it onevery code path, so may work at least\npartially). It is expected that more complete implementation of the module\nwill be provided later. Please help with the development if you are\ninterested in this module.', + version='0.1', + description='argparse module for MicroPython', + long_description="This is a module reimplemented specifically for MicroPython standard library,\nwith efficient and lean design in mind. Note that this module is likely work\nin progress and likely supports just a subset of CPython's corresponding\nmodule. Please help with the development if you are interested in this\nmodule.", url='https://github.com/micropython/micropython/issues/405', - author='MicroPython Developers', + author='Damien George', author_email='micro-python@googlegroups.com', maintainer='MicroPython Developers', maintainer_email='micro-python@googlegroups.com', diff --git a/argparse/test_argparse.py b/argparse/test_argparse.py new file mode 100644 index 00000000..b520cb6d --- /dev/null +++ b/argparse/test_argparse.py @@ -0,0 +1,33 @@ +import argparse + +parser = argparse.ArgumentParser(description="command line program") +parser.add_argument("a") +parser.add_argument("b") +parser.add_argument("c") +args = parser.parse_args(["1", "2", "3"]) +assert args.a == "1" and args.b == "2" and args.c == "3" + +parser = argparse.ArgumentParser() +parser.add_argument("-a", action="store_true") +parser.add_argument("-b", default=123) +args = parser.parse_args([]) +assert args.a == False and args.b == 123 +args = parser.parse_args(["-a"]) +assert args.a == True and args.b == 123 +args = parser.parse_args(["-b", "456"]) +assert args.a == False and args.b == 456 + +parser = argparse.ArgumentParser() +parser.add_argument("files", nargs="+") +args = parser.parse_args(["a"]) +assert args.files == ["a"] +args = parser.parse_args(["a", "b", "c"]) +assert args.files == ["a", "b", "c"] + +parser = argparse.ArgumentParser() +parser.add_argument("files1", nargs=2) +parser.add_argument("files2", nargs="*") +args = parser.parse_args(["a", "b"]) +assert args.files1 == ["a", "b"] and args.files2 == [] +args = parser.parse_args(["a", "b", "c"]) +assert args.files1 == ["a", "b"] and args.files2 == ["c"]