Skip to content

Commit

Permalink
Merge pull request #32 from speg03/feature/use-any-delimiter-for-name…
Browse files Browse the repository at this point in the history
…space

Use a different delimiter for namespace
  • Loading branch information
speg03 committed Nov 17, 2023
2 parents b3a4712 + f4890db commit 8ded4bc
Show file tree
Hide file tree
Showing 3 changed files with 54 additions and 18 deletions.
18 changes: 16 additions & 2 deletions README.md
Expand Up @@ -32,14 +32,14 @@ parser.add_argument("--banana.price", type=float)
args = parser.parse_args(
["--apple.n=2", "--apple.price=1.5", "--banana.n=3", "--banana.price=3.5"]
)
# => NestedNamespace(apple=NestedNamespace(n=2, price=1.5), banana=NestedNamespace(n=3, price=3.5))
# => _NestedNamespace(apple=_NestedNamespace(n=2, price=1.5), banana=_NestedNamespace(n=3, price=3.5))
```

Let's take out only the program argument apple.

```python
args.apple
# => NestedNamespace(n=2, price=1.5)
# => _NestedNamespace(n=2, price=1.5)
```

You can also get each value.
Expand All @@ -55,3 +55,17 @@ If you want a dictionary format, you can get it this way.
vars(args.apple)
# => {'n': 2, 'price': 1.5}
```

## Use a different delimiter for namespace

The default namespace delimiter is "." but can be any other character. In that case, specify the delimiter in the `NestedArgumentParser` constructor argument.

```python
import nestargs

parser = nestargs.NestedArgumentParser(delimiter="/")
parser.add_argument("--apple/n", type=int)

args = parser.parse_args(["--apple/n=1"])
# => _NestedNamespace(apple=_NestedNamespace(n=1))
```
41 changes: 25 additions & 16 deletions src/nestargs/parser.py
@@ -1,27 +1,36 @@
import argparse

DEFAULT_DELIMITER = "." # default delimiter for nested namespaces

class NestedNamespace(argparse.Namespace):
DELIMITER = "."

def __setattr__(self, name, value):
try:
parent, name = name.split(self.DELIMITER, maxsplit=1)
except ValueError:
parent = None
def create_namespace(delimiter: str):
class _NestedNamespace(argparse.Namespace):
def __setattr__(self, name, value):
if delimiter in name:
parent, name = name.split(delimiter, maxsplit=1)
else:
parent = None

if parent:
if not hasattr(self, parent):
super().__setattr__(parent, NestedNamespace())
setattr(getattr(self, parent), name, value)
elif parent is not None:
raise ValueError("parent should not be empty: {}".format(name))
else:
super().__setattr__(name, value)
if parent:
if not hasattr(self, parent):
super().__setattr__(parent, self.__class__())
setattr(getattr(self, parent), name, value)
elif parent is not None:
raise ValueError("parent should not be empty: {}".format(name))
else:
super().__setattr__(name, value)

return _NestedNamespace()


class NestedArgumentParser(argparse.ArgumentParser):
"""ArgumentParser that supports nested namespaces."""

def __init__(self, delimiter: str = DEFAULT_DELIMITER, **kwargs):
self.delimiter = delimiter
super().__init__(**kwargs)

def parse_known_args(self, args=None, namespace=None):
if namespace is None:
namespace = NestedNamespace()
namespace = create_namespace(self.delimiter)
return super().parse_known_args(args=args, namespace=namespace)
13 changes: 13 additions & 0 deletions tests/test_parser.py
Expand Up @@ -16,6 +16,19 @@ def test_parse_args(self):
assert vars(args.some).keys() == {"a", "b", "c"}
assert vars(args.some.c).keys() == {"d"}

def test_parse_args_with_another_delimiter(self):
parser = nestargs.NestedArgumentParser(delimiter="/")
parser.add_argument("some/a")
parser.add_argument("some/b")
parser.add_argument("some/c/d")

args = parser.parse_args(["1", "2", "3"])
assert args.some.a == "1"
assert args.some.b == "2"
assert args.some.c.d == "3"
assert vars(args.some).keys() == {"a", "b", "c"}
assert vars(args.some.c).keys() == {"d"}

def test_parse_args_with_empty_parent(self):
parser = nestargs.NestedArgumentParser()
parser.add_argument(".empty")
Expand Down

0 comments on commit 8ded4bc

Please sign in to comment.