diff --git a/README.md b/README.md index 89a11c5..4a84a1d 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ A package to help automatically create command-line interface from configuration or code. -It contains two modules CAP🧢(`ConfigArgumentParser`) and TAP🚰(`TypeArgumentParser`). +It contains three modules CAP🧢(`ConfigArgumentParser`), TAP🚰(`TypeArgumentParser`), and GAP🕳️(`GlobalArgumentParser`). Read the documentation [here](http://config-argument-parser.readthedocs.io/). @@ -215,6 +215,37 @@ $ python example.py -b -f 1 Args(a_string='abc', a_float=1.0, a_boolean=True, an_integer=0) ``` +### Case 5: create CLI from global variables (without comments) + +This requires less code than case 3, but the comments are not parsed, as the script `example.py` below: + +```Python +a_string = "abc" +a_float = 1.23 +a_boolean = False +an_integer = 0 + +import configargparser + +parser = configargparser.GlobalArgumentParser() +parser.parse_globals(shorts="sfb") + +print(a_string) +print(a_float) +print(a_boolean) +print(an_integer) +``` + +Use it as in case 1. For example, `python example.py -b -f 1`: + +```console +$ python example.py -b -f 1 +abc +1.0 +True +0 +``` + ## Installation Install from PyPI: diff --git a/configargparser/__init__.py b/configargparser/__init__.py index 8784656..27457a2 100644 --- a/configargparser/__init__.py +++ b/configargparser/__init__.py @@ -1,8 +1,9 @@ """A package to help automatically create command-line interface from configuration or code.""" -__version__ = "1.4.0" +__version__ = "1.5.0" from .cap import ConfigArgumentParser +from .gap import GlobalArgumentParser from .tap import TypeArgumentParser -__all__ = ["ConfigArgumentParser", "TypeArgumentParser"] +__all__ = ["ConfigArgumentParser", "TypeArgumentParser", "GlobalArgumentParser"] diff --git a/configargparser/gap.py b/configargparser/gap.py new file mode 100644 index 0000000..498b66f --- /dev/null +++ b/configargparser/gap.py @@ -0,0 +1,95 @@ +"""A module for building command-line interface from globals.""" + +import argparse +import inspect + + +class GlobalArgumentParser: + """Parser parsing and updating global variables. + + Attributes: + parser: An `~argparse.ArgumentParser`. + defaults: A `dict` contains the default arguments. + args: A `dict` contains the parsed arguments. + """ + + def __init__(self): + """Initialize GlobalArgumentParser.""" + self._init_parser() + self.defaults = {} + self.args = {} + self.help = {} + self._globals = {} + + def _init_parser(self): + self.parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + + def _read_globals(self, stack=2): + """Read and parse the attributes of global variables. + + Convert attributes to :attr:`defaults`. + """ + self._globals = dict(inspect.getmembers(inspect.stack()[stack][0]))["f_globals"] + self.defaults = { + k: v + for k, v in self._globals.items() + if not k.startswith("_") + and not inspect.ismodule(v) + and not inspect.isclass(v) + and not inspect.isfunction(v) + and not isinstance(v, GlobalArgumentParser) + } + self.help = {k: str(k) for k in self.defaults} + + def _add_arguments(self, shorts=""): + """Add arguments to parser according to the default. + + Args: + shorts: A sequence of short option letters for the leading options. + """ + boolean_to_action = {True: "store_false", False: "store_true"} + for i, (option, value) in enumerate(self.defaults.items()): + flags = [f"--{option.replace('_', '-')}"] + if i < len(shorts): + flags.insert(0, f"-{shorts[i]}") + if isinstance(value, bool): + self.parser.add_argument( + *flags, + action=boolean_to_action[value], + help=self.help[option], + ) + else: + self.parser.add_argument( + *flags, default=value, type=type(value), help=self.help[option] + ) + + def _parse_args(self, args=None): + """Convert argument strings to dictionary :attr:`args`. + + Return a `dict` containing arguments. + """ + namespace = self.parser.parse_args(args) + self.args = vars(namespace) + return self.args + + def _change_globals(self): + """Update global variables.""" + self._globals.update(self.args) + + def parse_globals(self, args=None, *, shorts=""): + """Parse arguments and update global variables. + + Args: + args: A list of strings to parse. The default is taken from `sys.argv`. + shorts: A sequence of short option letters for the leading options. + + Returns: + A `dict` containing updated arguments. + """ + self._read_globals() + self._add_arguments(shorts=shorts) + self._parse_args(args=args) + self._change_globals() + return self.args diff --git a/tests/test_gap.py b/tests/test_gap.py new file mode 100644 index 0000000..bf76946 --- /dev/null +++ b/tests/test_gap.py @@ -0,0 +1,55 @@ +from configargparser import gap + +a_string = "abc" +a_float = 1.23 +a_boolean = False +an_integer = 0 + + +class TestConfigArgumentParser: + def setup_method(self): + self.args = { + "a_string": a_string, + "a_float": a_float, + "a_boolean": a_boolean, + "an_integer": an_integer, + } + self.parser = gap.GlobalArgumentParser() + self.parser._read_globals(stack=1) + + def teardown_method(self): + self.args = { + "a_string": a_string, + "a_float": a_float, + "a_boolean": a_boolean, + "an_integer": an_integer, + } + self.parser = gap.GlobalArgumentParser() + + def test_read_globals(self): + assert self.parser.defaults == self.args + + def test_parse_args_default(self): + self.parser._add_arguments() + self.parser._parse_args([]) + assert self.parser.args == self.args + + def test_parse_args_separate(self): + self.parser._add_arguments() + self.parser._parse_args("--a-float 1".split()) + assert self.parser.args["a_float"] == 1.0 + self.parser._parse_args(["--a-boolean"]) + assert self.parser.args["a_boolean"] + + def test_parse_args_short(self): + self.parser._add_arguments(shorts="sfb") + self.parser._parse_args("-b -f 1".split()) + assert self.parser.args["a_float"] == 1.0 + assert self.parser.args["a_boolean"] + + def test_update_globals(self): + self.parser.parse_globals("-b -f 1".split(), shorts="sfb") + assert a_string == "abc" + assert a_float == 1.0 + assert a_boolean + assert an_integer == 0