Skip to content

Commit

Permalink
Add globals
Browse files Browse the repository at this point in the history
  • Loading branch information
yuanx749 committed Nov 21, 2024
1 parent 6b9a20a commit b5e1ea6
Show file tree
Hide file tree
Showing 4 changed files with 185 additions and 3 deletions.
33 changes: 32 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/).

Expand Down Expand Up @@ -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:
Expand Down
5 changes: 3 additions & 2 deletions configargparser/__init__.py
Original file line number Diff line number Diff line change
@@ -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"]
95 changes: 95 additions & 0 deletions configargparser/gap.py
Original file line number Diff line number Diff line change
@@ -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
55 changes: 55 additions & 0 deletions tests/test_gap.py
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit b5e1ea6

Please sign in to comment.