Skip to content

Commit eba0c0a

Browse files
committed
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).
1 parent 132ab95 commit eba0c0a

File tree

4 files changed

+217
-7
lines changed

4 files changed

+217
-7
lines changed

argparse/argparse.py

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
"""
2+
Minimal and functional version of CPython's argparse module.
3+
"""
4+
5+
import sys
6+
from _collections import namedtuple
7+
8+
class _ArgError(BaseException):
9+
pass
10+
11+
class _Arg:
12+
def __init__(self, name, dest, action, nargs, const, default, help):
13+
self.name = name
14+
self.dest = dest
15+
self.action = action
16+
self.nargs = nargs
17+
self.const = const
18+
self.default = default
19+
self.help = help
20+
21+
def parse(self, args):
22+
# parse args for this arg
23+
if self.action == "store":
24+
if self.nargs == None:
25+
if args:
26+
return args.pop(0)
27+
else:
28+
raise _ArgError("expecting value for %s" % self.name)
29+
elif self.nargs == "?":
30+
if args:
31+
return args.pop(0)
32+
else:
33+
return self.default
34+
else:
35+
if self.nargs == "*":
36+
n = -1
37+
elif self.nargs == "+":
38+
if not args:
39+
raise _ArgError("expecting value for %s" % self.name)
40+
n = -1
41+
else:
42+
n = int(self.nargs)
43+
ret = []
44+
stop_at_opt = True
45+
while args and n != 0:
46+
if stop_at_opt and args[0].startswith("-") and args[0] != "-":
47+
if args[0] == "--":
48+
stop_at_opt = False
49+
args.pop(0)
50+
else:
51+
break
52+
else:
53+
ret.append(args.pop(0))
54+
n -= 1
55+
if n > 0:
56+
raise _ArgError("expecting value for %s" % self.name)
57+
return ret
58+
elif self.action == "store_const":
59+
return self.const
60+
else:
61+
assert False
62+
63+
class ArgumentParser:
64+
def __init__(self, *, description):
65+
self.description = description
66+
self.opt = []
67+
self.pos = []
68+
69+
def add_argument(self, name, **kwargs):
70+
action = kwargs.get("action", "store")
71+
if action == "store_true":
72+
action = "store_const"
73+
const = True
74+
default = kwargs.get("default", False)
75+
elif action == "store_false":
76+
action = "store_const"
77+
const = False
78+
default = kwargs.get("default", True)
79+
else:
80+
const = kwargs.get("const", None)
81+
default = kwargs.get("default", None)
82+
if name.startswith("-"):
83+
list = self.opt
84+
if name.startswith("--"):
85+
dest = kwargs.get("dest", name[2:])
86+
else:
87+
dest = kwargs.get("dest", name[1:])
88+
else:
89+
list = self.pos
90+
dest = kwargs.get("dest", name)
91+
list.append(
92+
_Arg(name, dest, action, kwargs.get("nargs", None),
93+
const, default, kwargs.get("help", "")))
94+
95+
def usage(self, full):
96+
# print short usage
97+
print("usage: %s [-h]" % sys.argv[0], end="")
98+
def render_arg(arg):
99+
if arg.action == "store":
100+
if arg.nargs is None:
101+
return " %s" % arg.dest
102+
if isinstance(arg.nargs, int):
103+
return " %s(x%d)" % (arg.dest, arg.nargs)
104+
else:
105+
return " %s%s" % (arg.dest, arg.nargs)
106+
else:
107+
return ""
108+
for opt in self.opt:
109+
print(" [%s%s]" % (opt.name, render_arg(opt)), end="")
110+
for pos in self.pos:
111+
print(render_arg(pos), end="")
112+
print()
113+
114+
if not full:
115+
return
116+
117+
# print full information
118+
print()
119+
print(self.description)
120+
if self.pos:
121+
print("\npositional args:")
122+
for pos in self.pos:
123+
print(" %-16s%s" % (pos.name, pos.help))
124+
print("\noptional args:")
125+
print(" -h, --help show this message and exit")
126+
for opt in self.opt:
127+
print(" %-16s%s" % (opt.name + render_arg(opt), opt.help))
128+
129+
def parse_args(self, args=None):
130+
if args is None:
131+
args = sys.argv[1:]
132+
else:
133+
args = args[:]
134+
try:
135+
return self._parse_args(args)
136+
except _ArgError as e:
137+
self.usage(False)
138+
print("error:", e)
139+
sys.exit(2)
140+
141+
def _parse_args(self, args):
142+
# add optional args with defaults
143+
arg_dest = []
144+
arg_vals = []
145+
for opt in self.opt:
146+
arg_dest.append(opt.dest)
147+
arg_vals.append(opt.default)
148+
149+
# parse all args
150+
parsed_pos = False
151+
while args or not parsed_pos:
152+
if args and args[0].startswith("-") and args[0] != "-" and args[0] != "--":
153+
# optional arg
154+
a = args.pop(0)
155+
if a in ("-h", "--help"):
156+
self.usage(True)
157+
sys.exit(0)
158+
found = False
159+
for i, opt in enumerate(self.opt):
160+
if a == opt.name:
161+
arg_vals[i] = opt.parse(args)
162+
found = True
163+
break
164+
if not found:
165+
raise _ArgError("unknown option %s" % a)
166+
else:
167+
# positional arg
168+
if parsed_pos:
169+
raise _ArgError("extra args: %s" % " ".join(args))
170+
for pos in self.pos:
171+
arg_dest.append(pos.dest)
172+
arg_vals.append(pos.parse(args))
173+
parsed_pos = True
174+
175+
# build and return named tuple with arg values
176+
return namedtuple("args", arg_dest)(*arg_vals)

argparse/metadata.txt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1-
srctype=dummy
2-
type=module
3-
version=0.0.0
1+
srctype = micropython-lib
2+
type = module
3+
version = 0.1
4+
author = Damien George

argparse/setup.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66

77

88
setup(name='micropython-argparse',
9-
version='0.0.0',
10-
description='Dummy argparse module for MicroPython',
11-
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.',
9+
version='0.1',
10+
description='argparse module for MicroPython',
11+
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.",
1212
url='https://github.com/micropython/micropython/issues/405',
13-
author='MicroPython Developers',
13+
author='Damien George',
1414
author_email='micro-python@googlegroups.com',
1515
maintainer='MicroPython Developers',
1616
maintainer_email='micro-python@googlegroups.com',

argparse/test_argparse.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import argparse
2+
3+
parser = argparse.ArgumentParser(description="command line program")
4+
parser.add_argument("a")
5+
parser.add_argument("b")
6+
parser.add_argument("c")
7+
args = parser.parse_args(["1", "2", "3"])
8+
assert args.a == "1" and args.b == "2" and args.c == "3"
9+
10+
parser = argparse.ArgumentParser()
11+
parser.add_argument("-a", action="store_true")
12+
parser.add_argument("-b", default=123)
13+
args = parser.parse_args([])
14+
assert args.a == False and args.b == 123
15+
args = parser.parse_args(["-a"])
16+
assert args.a == True and args.b == 123
17+
args = parser.parse_args(["-b", "456"])
18+
assert args.a == False and args.b == 456
19+
20+
parser = argparse.ArgumentParser()
21+
parser.add_argument("files", nargs="+")
22+
args = parser.parse_args(["a"])
23+
assert args.files == ["a"]
24+
args = parser.parse_args(["a", "b", "c"])
25+
assert args.files == ["a", "b", "c"]
26+
27+
parser = argparse.ArgumentParser()
28+
parser.add_argument("files1", nargs=2)
29+
parser.add_argument("files2", nargs="*")
30+
args = parser.parse_args(["a", "b"])
31+
assert args.files1 == ["a", "b"] and args.files2 == []
32+
args = parser.parse_args(["a", "b", "c"])
33+
assert args.files1 == ["a", "b"] and args.files2 == ["c"]

0 commit comments

Comments
 (0)