From 812a4fc3f3063741da0fa01cbbbf17dead66a664 Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Thu, 9 Jun 2022 09:16:53 -0400 Subject: T2719: prototype of an op mode command runner based on type hints and introspection --- python/vyos/opmode.py | 123 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 python/vyos/opmode.py (limited to 'python') diff --git a/python/vyos/opmode.py b/python/vyos/opmode.py new file mode 100644 index 000000000..c0812c7fd --- /dev/null +++ b/python/vyos/opmode.py @@ -0,0 +1,123 @@ +# Copyright 2022 VyOS maintainers and contributors +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see . + +import typing + + +def _is_op_mode_function_name(name): + from re import match + + if match(r"^(show|clear|reset|restart)", name): + return True + else: + return False + +def _is_show(name): + from re import match + + if match(r"^show", name): + return True + else: + return False + +def _get_op_mode_functions(module): + from inspect import getmembers, isfunction + + # Get all functions in that module + funcs = getmembers(module, isfunction) + + # getmembers returns (name, func) tuples + funcs = list(filter(lambda ft: _is_op_mode_function_name(ft[0]), funcs)) + + funcs_dict = {} + for (name, thunk) in funcs: + funcs_dict[name] = thunk + + return funcs_dict + +def _is_optional_type(t): + # Optional[t] is internally an alias for Union[t, NoneType] + # and there's no easy way to get union members it seems + if (type(t) == typing._UnionGenericAlias): + if (len(t.__args__) == 2): + if t.__args__[1] == type(None): + return True + + return False + +def _get_arg_type(t): + """ Returns the type itself if it's a primitive type, + or the "real" type of typing.Optional + + Doesn't work with anything else at the moment! + """ + if _is_optional_type(t): + t.__args__[0] + else: + return t + +def run(module): + from argparse import ArgumentParser + + functions = _get_op_mode_functions(module) + + parser = ArgumentParser() + subparsers = parser.add_subparsers(dest="subcommand") + + for function_name in functions: + subparser = subparsers.add_parser(function_name, help=functions[function_name].__doc__) + + type_hints = typing.get_type_hints(functions[function_name]) + for opt in type_hints: + th = type_hints[opt] + + # Show commands require an option to choose between raw JSON and human-readable + # formatted output. + # For interactive use, they default to formatted output. + if (function_name == "show") and (opt == "raw"): + subparser.add_argument(f"--raw", action='store_true') + elif _is_optional_type(th): + subparser.add_argument(f"--{opt}", type=_get_arg_type(th), default=None) + else: + subparser.add_argument(f"--{opt}", type=_get_arg_type(th), required=True) + + # Get options as a dict rather than a namespace, + # so that we can modify it and pack for passing to functions + args = vars(parser.parse_args()) + + func = functions[args["subcommand"]] + + # Remove the subcommand from the arguments, + # it would cause an extra argument error when we pass the dict to a function + del args["subcommand"] + + if "raw" not in args: + args["raw"] = False + + if function_name == "show": + # Show commands are slightly special: + # they may return human-formatted output + # or a raw dict that we need to serialize in JSON for printing + res = func(**args) + if not args["raw"]: + return res + else: + from json import dumps + return dumps(res, indent=4) + else: + # Other functions should not return anything, + # although they may print their own warnings or status messages + func(**args) + -- cgit v1.2.3 From f08b850f297422925b8e0a16d67accee6843b9e1 Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Wed, 15 Jun 2022 04:47:41 -0400 Subject: T2719: handle the case when script subcommand is not given --- python/vyos/opmode.py | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'python') diff --git a/python/vyos/opmode.py b/python/vyos/opmode.py index c0812c7fd..692e5fc98 100644 --- a/python/vyos/opmode.py +++ b/python/vyos/opmode.py @@ -13,6 +13,7 @@ # You should have received a copy of the GNU Lesser General Public # License along with this library. If not, see . +import sys import typing @@ -97,6 +98,11 @@ def run(module): # so that we can modify it and pack for passing to functions args = vars(parser.parse_args()) + if not args["subcommand"]: + print("Subcommand required!") + parser.print_usage() + sys.exit(1) + func = functions[args["subcommand"]] # Remove the subcommand from the arguments, -- cgit v1.2.3 From 3a9d9b4297c56bae42b1fc10a08cfcce58496483 Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Wed, 15 Jun 2022 06:38:09 -0400 Subject: T2719: correctly handle the raw argument for all show_* commands --- python/vyos/opmode.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) (limited to 'python') diff --git a/python/vyos/opmode.py b/python/vyos/opmode.py index 692e5fc98..10e8770d3 100644 --- a/python/vyos/opmode.py +++ b/python/vyos/opmode.py @@ -103,16 +103,20 @@ def run(module): parser.print_usage() sys.exit(1) - func = functions[args["subcommand"]] + function_name = args["subcommand"] + func = functions[function_name] # Remove the subcommand from the arguments, # it would cause an extra argument error when we pass the dict to a function del args["subcommand"] - if "raw" not in args: + # Show commands must always get the "raw" argument, + # but other commands (clear/reset/restart) should not, + # because they produce no output and it makes no sense for them. + if ("raw" not in args) and re.match(r"^show", function_name): args["raw"] = False - if function_name == "show": + if re.match(r"^show", function_name): # Show commands are slightly special: # they may return human-formatted output # or a raw dict that we need to serialize in JSON for printing -- cgit v1.2.3 From f0e0a2393b48359bc5f580bce9730225741c7c90 Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Thu, 16 Jun 2022 11:23:36 -0400 Subject: T2719: make re functions usage in vyos.opmode more consistent --- python/vyos/opmode.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) (limited to 'python') diff --git a/python/vyos/opmode.py b/python/vyos/opmode.py index 10e8770d3..270adc658 100644 --- a/python/vyos/opmode.py +++ b/python/vyos/opmode.py @@ -13,22 +13,19 @@ # You should have received a copy of the GNU Lesser General Public # License along with this library. If not, see . +import re import sys import typing def _is_op_mode_function_name(name): - from re import match - - if match(r"^(show|clear|reset|restart)", name): + if re.match(r"^(show|clear|reset|restart)", name): return True else: return False def _is_show(name): - from re import match - - if match(r"^show", name): + if re.match(r"^show", name): return True else: return False -- cgit v1.2.3 From 81f7df57eeb0714bda8d9103659e644de48d1dc9 Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Mon, 20 Jun 2022 10:26:25 -0400 Subject: T2719: use _is_show for detecting show functions --- python/vyos/opmode.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'python') diff --git a/python/vyos/opmode.py b/python/vyos/opmode.py index 270adc658..134f55017 100644 --- a/python/vyos/opmode.py +++ b/python/vyos/opmode.py @@ -84,7 +84,7 @@ def run(module): # Show commands require an option to choose between raw JSON and human-readable # formatted output. # For interactive use, they default to formatted output. - if (function_name == "show") and (opt == "raw"): + if _is_show(function_name) and (opt == "raw"): subparser.add_argument(f"--raw", action='store_true') elif _is_optional_type(th): subparser.add_argument(f"--{opt}", type=_get_arg_type(th), default=None) @@ -110,7 +110,7 @@ def run(module): # Show commands must always get the "raw" argument, # but other commands (clear/reset/restart) should not, # because they produce no output and it makes no sense for them. - if ("raw" not in args) and re.match(r"^show", function_name): + if ("raw" not in args) and _is_show(function_name): args["raw"] = False if re.match(r"^show", function_name): -- cgit v1.2.3 From b8e2a0650168ce4958dc360f857c816f02c6284f Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Tue, 5 Jul 2022 06:47:48 -0400 Subject: T2719: add general support for boolean options to generative op mode Since Python as of 3.9 doesn't give us an option to look up argument's default value by its name, this implementation requires that all boolean options must default to false. --- python/vyos/opmode.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) (limited to 'python') diff --git a/python/vyos/opmode.py b/python/vyos/opmode.py index 134f55017..4e68d6e03 100644 --- a/python/vyos/opmode.py +++ b/python/vyos/opmode.py @@ -81,13 +81,10 @@ def run(module): for opt in type_hints: th = type_hints[opt] - # Show commands require an option to choose between raw JSON and human-readable - # formatted output. - # For interactive use, they default to formatted output. - if _is_show(function_name) and (opt == "raw"): - subparser.add_argument(f"--raw", action='store_true') - elif _is_optional_type(th): + if _is_optional_type(th): subparser.add_argument(f"--{opt}", type=_get_arg_type(th), default=None) + elif _get_arg_type(th) == bool: + subparser.add_argument(f"--{opt}", action='store_true') else: subparser.add_argument(f"--{opt}", type=_get_arg_type(th), required=True) -- cgit v1.2.3 From 8d8c14b53408aec3011fbf33c201fda4926248bb Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Mon, 18 Jul 2022 12:34:29 -0500 Subject: T2719: patch for general support for boolean options Signed-off-by: Daniil Baturin --- python/vyos/opmode.py | 11 ++++++----- src/op_mode/version.py | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) (limited to 'python') diff --git a/python/vyos/opmode.py b/python/vyos/opmode.py index 4e68d6e03..906bd0dcb 100644 --- a/python/vyos/opmode.py +++ b/python/vyos/opmode.py @@ -62,7 +62,7 @@ def _get_arg_type(t): Doesn't work with anything else at the moment! """ if _is_optional_type(t): - t.__args__[0] + return t.__args__[0] else: return t @@ -81,12 +81,13 @@ def run(module): for opt in type_hints: th = type_hints[opt] - if _is_optional_type(th): - subparser.add_argument(f"--{opt}", type=_get_arg_type(th), default=None) - elif _get_arg_type(th) == bool: + if _get_arg_type(th) == bool: subparser.add_argument(f"--{opt}", action='store_true') else: - subparser.add_argument(f"--{opt}", type=_get_arg_type(th), required=True) + if _is_optional_type(th): + subparser.add_argument(f"--{opt}", type=_get_arg_type(th), default=None) + else: + subparser.add_argument(f"--{opt}", type=_get_arg_type(th), required=True) # Get options as a dict rather than a namespace, # so that we can modify it and pack for passing to functions diff --git a/src/op_mode/version.py b/src/op_mode/version.py index 90645dfbc..847bb197e 100755 --- a/src/op_mode/version.py +++ b/src/op_mode/version.py @@ -65,7 +65,7 @@ def _get_formatted_output(version_data): tmpl = Template(version_output_tmpl) return tmpl.render(version_data).strip() -def show(raw: bool, funny: bool): +def show(raw: bool, funny: typing.Optional[bool]): """ Display neighbor table contents """ version_data = _get_raw_data(funny=funny) -- cgit v1.2.3 From e64fcfd6014205184c21124f6570215ed908c233 Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Wed, 20 Jul 2022 11:30:00 -0400 Subject: T2719: fix a stray empty key in the CPU data dict --- python/vyos/cpu.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'python') diff --git a/python/vyos/cpu.py b/python/vyos/cpu.py index a0ef864be..488ae79fb 100644 --- a/python/vyos/cpu.py +++ b/python/vyos/cpu.py @@ -32,7 +32,8 @@ import re def _read_cpuinfo(): with open('/proc/cpuinfo', 'r') as f: - return f.readlines() + lines = f.read().strip() + return re.split(r'\n+', lines) def _split_line(l): l = l.strip() -- cgit v1.2.3 From 39fbe8eecf9f0a115ca0d54bbddc83e3f5fd0c8c Mon Sep 17 00:00:00 2001 From: Daniil Baturin Date: Wed, 20 Jul 2022 12:30:35 -0400 Subject: T2719: fix indentation in vyos.opmode --- python/vyos/opmode.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'python') diff --git a/python/vyos/opmode.py b/python/vyos/opmode.py index 906bd0dcb..0af4359c6 100644 --- a/python/vyos/opmode.py +++ b/python/vyos/opmode.py @@ -94,9 +94,9 @@ def run(module): args = vars(parser.parse_args()) if not args["subcommand"]: - print("Subcommand required!") - parser.print_usage() - sys.exit(1) + print("Subcommand required!") + parser.print_usage() + sys.exit(1) function_name = args["subcommand"] func = functions[function_name] -- cgit v1.2.3