Skip to content

Commit

Permalink
Add support for nargs="+" and action="append"
Browse files Browse the repository at this point in the history
  • Loading branch information
kvnglb committed Nov 15, 2024
1 parent 1a9c1d4 commit 2c3d22c
Show file tree
Hide file tree
Showing 7 changed files with 146 additions and 14 deletions.
12 changes: 12 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,15 @@ The Python script that uses the NetArgumentParser must follow these rules:
4) The script using NetArgumentParser must not have arguments with leading underscore.
NetArgumentParser replaces leading underscores in receiving xml messages with dashes, e.g. `__x=5` will become `--x=5`. This is because `<nap><--x>5</--x></nap>` is invalid xml syntax but `<nap><__x>5</__x></nap>` is valid. So to pass the argument `--x=5` with xml, a substitution of the leading dashes is required. If the script really needs the argument `-_x=5`... Just don't, because sending `<nap><-_x>5</-_x></nap>` is not possible and `<nap><__x>5</__x></nap>` will result in `--x=5`.
5) Arguments with `nargs="+"` and/or `action="append"` are handled as shown in the table below. Examples can be found in [examples/nargs_append](examples/nargs_append).
|main \ nap|url parameters|json|xml|
|--|--|--|--|
|`-x 1 2 3`|`?-x=1 2 3`|`{"-x": "1 2 3"}`|`<nap><_x>1 2 3</_x></nap>`|
|`-x 1 -x 2 -x 3`|`?-x=1&-x=2&-x=3`|`{"-x": [1, 2, 3]}`|`<nap><_x>1</_x><_x>2</_x><_x>3</_x></nap>`|
|`-x 1 2 3 -x 11 22 33 -x 111 222 333`|`?-x=1 2 3&-x=11 22 33&-x=111 222 333`|`{"-x": ["1 2 3", "11 22 33", "111 222 333"]}`|`<nap><_x>1 2 3</_x><_x>11 22 33</_x><_x>111 222 333</_x></nap>`|
NOTE:
- Having the same tag multiple times in xml is fine, but having two identical keys in json will drop the first entry. So `{"a": 1, "a": 2}'` will result in `{'a': 2}`.
- Depending on the software that sends the HTTP request with the url parameters, the special characters may need to be replaced, e.g. whitespace with `%20`. So `?-x=1 2 3` will become `?-x=1%202%203`.
15 changes: 15 additions & 0 deletions docs/examples/nargs_append/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from netargparse import NetArgumentParser


def main(args):
if args._cmd == "main":
print(vars(args))
else:
return vars(args)


nap = NetArgumentParser()
nap.parser.add_argument("-x", type=int, required=True, nargs="+")
nap.parser.add_argument("-y", type=int, required=True, action="append")
nap.parser.add_argument("-z", type=int, required=True, nargs="+", action="append")
nap(main)
19 changes: 19 additions & 0 deletions docs/examples/nargs_append/send.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import argparse
import socket


parser = argparse.ArgumentParser()
parser.add_argument("-p", "--port", type=int, required=True)
parser.add_argument("--xml", action="store_true")
args = parser.parse_args()


s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("127.0.0.1", args.port))

if args.xml:
s.sendall(b"<nap><_x>1 2 3</_x><_x>11 22 33</_x><_y>1</_y><_y>11</_y><_z>1 2 3</_z><_z>11 22 33</_z></nap>")
else:
s.sendall(b'{"-x": ["1 2 3", "11 22 33"], "-y": [1, 11], "-z": ["1 2 3", "11 22 33"]}')

print("received:", s.recv(256).decode("utf-8"))
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "netargparse"
version = "0.1.2"
version = "0.1.3"
description = "Enhance ArgumentParser with a TCP-based API for argument handling."
authors = ["Kevin Golob <151143873+kvnglb@users.noreply.github.com>"]
readme = "README.md"
Expand Down
19 changes: 16 additions & 3 deletions src/netargparse/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,14 @@ def dict_to_argstring(d: dict) -> str:
dash = "-" * (len(key) - len(k))
k = dash + k
if value:
lst.append(f"{k} {value}")
if type(value) is list:
for val in value:
if val:
lst.append(f"{k} {val}")
else:
lst.append(k)
else:
lst.append(f"{k} {value}")
else:
lst.append(k)
return " ".join(lst)
Expand Down Expand Up @@ -121,9 +128,15 @@ def _to_dict(xml: t.Union[bytes, str]) -> dict:
if root.tag != "nap":
raise Exception("Root must be named `nap`. Message must be in `<nap>...</nap>`.")

ret = {}
ret = {} # type: t.Dict[t.Any, t.Any]
for child in root:
ret[child.tag] = child.text
if child.tag in ret:
if type(ret[child.tag]) is not list:
ret[child.tag] = [ret[child.tag], child.text]
else:
ret[child.tag].append(child.text)
else:
ret[child.tag] = child.text
return ret

@staticmethod
Expand Down
8 changes: 3 additions & 5 deletions src/netargparse/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,17 +205,15 @@ def msg_handler(autoformat: bool, response: t.Union[dict, str],
@app.route("/")
def http_get() -> flask.wrappers.Response:
"""Route for json response."""
d = dict(flask.request.args)
q_get.put(d)
q_get.put(flask.request.args)
r = q_send.get() # type: tuple[t.Any, t.Any, t.Any]
return flask.Response(msg_handler(*r, MessageJson),
mimetype="application/json")

@app.route("/xml")
def http_get_xml() -> flask.wrappers.Response:
"""Route for xml response."""
d = dict(flask.request.args)
q_get.put(d)
q_get.put(flask.request.args)
r = q_send.get() # type: tuple[t.Any, t.Any, t.Any]
return flask.Response(msg_handler(*r, MessageXml),
mimetype="application/xml")
Expand All @@ -237,7 +235,7 @@ def get_msg(self) -> str:
"""
d = self.q_get.get()
return Message.dict_to_argstring(d)
return Message.dict_to_argstring(d.to_dict(flat=False))

def send_msg(self, autoformat: bool, response: t.Union[dict, str],
exception: str) -> None:
Expand Down
85 changes: 80 additions & 5 deletions tests/test_netargparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@ def main(args):
nap.parser.add_argument("--var_true", action="store_true")
nap(main, resp_delay=0.2, parse_args=["nap", "--port", "7001"])

def tcp_socket_autoformat_nargs_append():
def main(args):
return vars(args)

nap = NetArgumentParser()
nap.parser.add_argument("-x", type=int, nargs="+")
nap.parser.add_argument("-y", type=int, action="append")
nap.parser.add_argument("-z", type=int, nargs="+", action="append")
nap(main, resp_delay=0.2, parse_args=["nap", "--port", "7002"])

def http_no_autoformat():
def main(args):
if args.var_str == "damn":
Expand All @@ -43,7 +53,7 @@ def main(args):
nap.parser.add_argument("--var_str", type=str)
nap.parser.add_argument("--var_int", type=int)
nap.parser.add_argument("--var_true", action="store_true")
nap(main, False, 0.2, ["nap", "--port", "7002", "--http"])
nap(main, False, 0.2, ["nap", "--port", "7003", "--http"])

def http_autoformat():
def main(args):
Expand All @@ -55,7 +65,17 @@ def main(args):
nap.parser.add_argument("--var_str", type=str)
nap.parser.add_argument("--var_int", type=int)
nap.parser.add_argument("--var_true", action="store_true")
nap(main, resp_delay=0.2, parse_args=["nap", "--port", "7003", "--http"])
nap(main, resp_delay=0.2, parse_args=["nap", "--port", "7004", "--http"])

def http_autoformat_nargs_append():
def main(args):
return vars(args)

nap = NetArgumentParser()
nap.parser.add_argument("-x", type=int, nargs="+")
nap.parser.add_argument("-y", type=int, action="append")
nap.parser.add_argument("-z", type=int, nargs="+", action="append")
nap(main, resp_delay=0.2, parse_args=["nap", "--port", "7005", "--http"])


class TcpSocketRequest:
Expand Down Expand Up @@ -153,6 +173,21 @@ def test_plain_xml_a_func_exc(self):
self.assertEqual(ans, b"<nap><response></response><exception>division by zero</exception><finished>1</finished></nap>")
self.assertResponse(ans, "xml")

def test_plain_xml_a_narap_two_append(self):
ans = s_tcp_a_narap.txrx(b"<nap><_x>1 2 3</_x><_x>11 22 33</_x><_y>1</_y><_y>11</_y><_z>1 2 3</_z><_z>11 22 33</_z></nap>")
self.assertEqual(ans, b"<nap><response><x>[11, 22, 33]</x><y>[1, 11]</y><z>[[1, 2, 3], [11, 22, 33]]</z><_cmd>nap</_cmd></response><exception></exception><finished>1</finished></nap>")
self.assertResponse(ans, "xml")

def test_plain_xml_a_narap_three_append(self):
ans = s_tcp_a_narap.txrx(b"<nap><_x>1 2 3</_x><_x>11 22 33</_x><_x>111 222 333</_x><_y>1</_y><_y>11</_y><_y>111</_y><_z>1 2 3</_z><_z>11 22 33</_z><_z>111 222 333</_z></nap>")
self.assertEqual(ans, b"<nap><response><x>[111, 222, 333]</x><y>[1, 11, 111]</y><z>[[1, 2, 3], [11, 22, 33], [111, 222, 333]]</z><_cmd>nap</_cmd></response><exception></exception><finished>1</finished></nap>")
self.assertResponse(ans, "xml")

def test_plain_xml_invalid_a_narap(self):
ans = s_tcp_a_narap.txrx(b"<nap><_x>1 2 3</_x><_x>11 22 33</_x><_x>111 222 333</_x><_y>1 2</_y><_y>11 22</_y><_y>111 222</_y><_z>1 2 3</_z><_z>11 22 33</_z><_z>111 222 333</_z></nap>")
self.assertEqual(ans, b"<nap><response></response><exception>unrecognized arguments: 2 22 222</exception><finished>1</finished></nap>")
self.assertResponse(ans, "xml")

# Plain json, autoformat
def test_plain_json_a_valid_tx(self):
ans = s_tcp_a.txrx(b'{"--var_str": "value", "--var_int": "2"}')
Expand All @@ -174,6 +209,26 @@ def test_plain_json_a_func_exc(self):
self.assertEqual(ans, b'{"response": "", "exception": "division by zero", "finished": 1}')
self.assertResponse(ans, "json")

def test_plain_json_a_narap_two_append(self):
ans = s_tcp_a_narap.txrx(b'{"-x": ["1 2 3", "11 22 33"], "-y": [1, 11], "-z": ["1 2 3", "11 22 33"]}')
self.assertEqual(ans, b'{"response": {"x": [11, 22, 33], "y": [1, 11], "z": [[1, 2, 3], [11, 22, 33]], "_cmd": "nap"}, "exception": "", "finished": 1}')
self.assertResponse(ans, "json")

def test_plain_json_a_narap_three_append(self):
ans = s_tcp_a_narap.txrx(b'{"-x": ["1 2 3", "11 22 33", "111 222 333"], "-y": [1, 11, 111], "-z": ["1 2 3", "11 22 33", "111 222 333"]}')
self.assertEqual(ans, b'{"response": {"x": [111, 222, 333], "y": [1, 11, 111], "z": [[1, 2, 3], [11, 22, 33], [111, 222, 333]], "_cmd": "nap"}, "exception": "", "finished": 1}')
self.assertResponse(ans, "json")

def test_plain_json_a_narap_double_key(self):
ans = s_tcp_a_narap.txrx(b'{"-x": "1 2 3", "-x": "11 22 33", "-y": 1, "-y": 11, "-z": "11 22 33", "-z": "11 22 33"}')
self.assertEqual(ans, b'{"response": {"x": [11, 22, 33], "y": [11], "z": [[11, 22, 33]], "_cmd": "nap"}, "exception": "", "finished": 1}')
self.assertResponse(ans, "json")

def test_plain_json_invalid_a_narap(self):
ans = s_tcp_a_narap.txrx(b'{"-x": ["1 2 3", "11 22 33"], "-y": ["1 2", "11 22"], "-z": ["1 2 3", "11 22 33"]}')
self.assertEqual(ans, b'{"response": "", "exception": "unrecognized arguments: 2 22", "finished": 1}')
self.assertResponse(ans, "json")

# HTTP, json resp, no autoformat
def test_http_json_na_valid_tx(self):
ans = s_http_na.txrx("/?--var_str=value&--var_int=2")
Expand Down Expand Up @@ -237,6 +292,21 @@ def test_http_json_a_func_exc(self):
self.assertEqual(ans, '{"response": "", "exception": "division by zero", "finished": 1}')
self.assertResponse(ans, "json")

def test_http_json_a_narap_two_append(self):
ans = s_http_a_narap.txrx("/?-x=1 2 3&-x=11 22 33&-y=1&-y=11&-z=1 2 3&-z=11 22 33")
self.assertEqual(ans, '{"response": {"x": [11, 22, 33], "y": [1, 11], "z": [[1, 2, 3], [11, 22, 33]], "_cmd": "nap"}, "exception": "", "finished": 1}')
self.assertResponse(ans, "json")

def test_http_json_a_narap_three_append(self):
ans = s_http_a_narap.txrx("/?-x=1 2 3&-x=11 22 33&-x=111 222 333&-y=1&-y=11&-y=111&-z=1 2 3&-z=11 22 33&-z=111 222 333")
self.assertEqual(ans, '{"response": {"x": [111, 222, 333], "y": [1, 11, 111], "z": [[1, 2, 3], [11, 22, 33], [111, 222, 333]], "_cmd": "nap"}, "exception": "", "finished": 1}')
self.assertResponse(ans, "json")

def test_http_json_invalid_a_narap(self):
ans = s_http_a_narap.txrx("/?-x=1 2 3&-x=11 22 33&-y=1 2&-y=11 22&-z=1 2 3&-z=11 22 33")
self.assertEqual(ans, '{"response": "", "exception": "unrecognized arguments: 2 22", "finished": 1}')
self.assertResponse(ans, "json")

# HTTP, xml resp, autoformat
def test_http_xml_a_valid_tx(self):
ans = s_http_a.txrx("/xml?--var_str=value&--var_int=2")
Expand All @@ -260,7 +330,8 @@ def test_http_xml_a_func_exc(self):


if __name__ == "__main__":
for t in [tcp_socket_no_autoformat, tcp_socket_autoformat, http_no_autoformat, http_autoformat]:
for t in [tcp_socket_no_autoformat, tcp_socket_autoformat, tcp_socket_autoformat_nargs_append,
http_no_autoformat, http_autoformat, http_autoformat_nargs_append]:
Thread(target=t, daemon=True).start()

for i in range(10):
Expand All @@ -271,10 +342,14 @@ def test_http_xml_a_func_exc(self):
s_tcp_na = TcpSocketRequest(7000)
if not "s_tcp_a" in globals():
s_tcp_a = TcpSocketRequest(7001)
if not "s_tcp_a_narap" in globals():
s_tcp_a_narap = TcpSocketRequest(7002)
if not "s_http_na" in globals():
s_http_na = HttpRequest(7002)
s_http_na = HttpRequest(7003)
if not "s_http_a" in globals():
s_http_a = HttpRequest(7003)
s_http_a = HttpRequest(7004)
if not "s_http_a_narap" in globals():
s_http_a_narap = HttpRequest(7005)
break
except (ConnectionRefusedError, requests.exceptions.ConnectionError):
time.sleep(1)
Expand Down

0 comments on commit 2c3d22c

Please sign in to comment.