diff --git a/README.md b/README.md
index 583d37a..ebf380b 100644
--- a/README.md
+++ b/README.md
@@ -7,8 +7,8 @@ Easily parse arbitrary arguments from the command line without dependencies:
![example code](assets/code.png)
![example code](assets/run.png)
-![](https://img.shields.io/badge/coverage-97%25-success)
-![](https://img.shields.io/badge/version-0.1.4-informational)
+![](https://img.shields.io/badge/coverage-99%25-success)
+![](https://img.shields.io/badge/version-0.1.5-informational)
![](https://img.shields.io/badge/python-3.7%2B%20-orange)
```bash
@@ -25,6 +25,7 @@ pip install minydra
MinyDict •
Save config •
Prevent typos •
+ Use default configs •
Examples
@@ -336,6 +337,68 @@ KeyError: 'Cannot create a non-existing key in strict mode ({"log_leveel":INFO})
+### Using default configurations
+
+The `minydra.Parser` class takes a `defaults=` keyword argument. This can be:
+
+* a `str` or a `pathlib.Path` to a `json` `yaml` or `pickle` file that `minydra.MinyDict` can load (`from_X`)
+* a `dict` or a `minydra.MinyDict`
+
+When `defaults` is provided, the resulting `minydra.MinyDict` serves as a reference for the arguments parsed from the command-line:
+
+* arguments from the command-line have a higher priority but **must** be present in the `defaults` (`defaults.update(args, strict=True)` is used, see [strict mode](#strict-mode))
+* arguments not present in the command-line with fallback to values in `defaults`
+
+`defaults` can actually be a `list` and the update order is the same as the list's. For instance:
+
+```python
+In [1]: from minydra import Parser
+
+In [2]: Parser(defaults=["./examples/demo.json", "./examples/demo2.json"]).args.pretty_print();
+╭─────────────────────────────────╮
+│ log │
+│ │logger │
+│ │ │log_level : INFO │
+│ │ │logger_name : minydra │
+│ │outdir : /some/other/path │
+│ │project : demo │
+│ new_key : 3 │
+│ verbose : False │
+╰─────────────────────────────────╯
+```
+
+If you need to set defaults from the command-line, there's a special `@defaults` keyword you can use:
+
+
+```text
+$ python examples/decorator.py @defaults=./examples/demo.json
+╭──────────────────────────────────────╮
+│ @defaults : ./examples/demo.json │
+│ log │
+│ │logger │
+│ │ │log_level : DEBUG │
+│ │ │logger_name : minydra │
+│ │outdir : /some/path │
+│ │project : demo │
+│ verbose : False │
+╰──────────────────────────────────────╯
+
+$ python examples/decorator.py @defaults="['./examples/demo.json', './examples/demo2.json']"
+╭───────────────────────────────────────────────────────────────────╮
+│ @defaults : ['./examples/demo.json', './examples/demo2.json'] │
+│ log │
+│ │logger │
+│ │ │log_level : INFO │
+│ │ │logger_name : minydra │
+│ │outdir : /some/other/path │
+│ │project : demo │
+│ new_key : 3 │
+│ verbose : False │
+╰───────────────────────────────────────────────────────────────────╯
+```
+
+
+
### `pretty_print`
Prints the `MinyDict` in a box, with dicts properly indented. A few arguments:
diff --git a/assets/carbon-config.json b/assets/carbon-config.json
index 1aa56be..ebd1bae 100644
--- a/assets/carbon-config.json
+++ b/assets/carbon-config.json
@@ -1 +1 @@
-{"paddingVertical":"57px","paddingHorizontal":"55px","backgroundImage":null,"backgroundImageSelection":null,"backgroundMode":"color","backgroundColor":"rgba(255,255,255,1)","dropShadow":true,"dropShadowOffsetY":"21px","dropShadowBlurRadius":"45px","theme":"material","windowTheme":"none","language":"text","fontFamily":"MonoLisa","fontSize":"13.5px","lineHeight":"150%","windowControls":true,"widthAdjustment":false,"lineNumbers":false,"firstLineNumber":1,"exportSize":"4x","watermark":false,"squaredImage":false,"hiddenCharacters":false,"name":"","width":912}
\ No newline at end of file
+{"paddingVertical":"57px","paddingHorizontal":"55px","backgroundImage":null,"backgroundImageSelection":null,"backgroundMode":"color","backgroundColor":"rgba(255,255,255,1)","dropShadow":true,"dropShadowOffsetY":"21px","dropShadowBlurRadius":"45px","theme":"material","windowTheme":"none","language":"text","fontFamily":"MonoLisa","fontSize":"13.5px","lineHeight":"150%","windowControls":true,"widthAdjustment":false,"lineNumbers":false,"firstLineNumber":1,"exportSize":"4x","watermark":false,"squaredImage":false,"hiddenCharacters":false,"name":"","width":912}
diff --git a/examples/Readme.md b/examples/Readme.md
index 8acddae..457cdec 100644
--- a/examples/Readme.md
+++ b/examples/Readme.md
@@ -297,6 +297,72 @@ In [6]: d.update({"b": {"e": 2}}).pretty_print() # default is strict=False, allo
Out[6]: {'a': 10, 'b': {'c': 0, 'e': 2}}
```
+## Using default configurations
+
+Code: [**`defaults.py`**](defaults.py)
+
+```python
+from minydra import resolved_args
+
+if __name__ == "__main__":
+
+ args = resolved_args(defaults="demo.yaml")
+ args.pretty_print()
+```
+
+```text
+$ python examples/defaults.py
+╭──────────────────────────────╮
+│ log │
+│ │logger │
+│ │ │log_level : DEBUG │
+│ │ │logger_name : minydra │
+│ │outdir : /some/path │
+│ │project : demo │
+│ verbose : False │
+╰──────────────────────────────╯
+
+$ python examples/defaults.py @defaults=examples/demo2.json
+╭─────────────────────────────────────╮
+│ @defaults : examples/demo2.json │
+│ log │
+│ │logger │
+│ │ │log_level : INFO │
+│ │ │logger_name : minydra │
+│ │outdir : /some/other/path │
+│ new_key : 3 │
+│ verbose : False │
+╰─────────────────────────────────────╯
+
+$ python examples/defaults.py @defaults="['examples/demo.json', 'examples/demo2.json']"
+╭───────────────────────────────────────────────────────────────╮
+│ @defaults : ['examples/demo.json', 'examples/demo2.json'] │
+│ log │
+│ │logger │
+│ │ │log_level : INFO │
+│ │ │logger_name : minydra │
+│ │outdir : /some/other/path │
+│ │project : demo │
+│ new_key : 3 │
+│ verbose : False │
+╰───────────────────────────────────────────────────────────────╯
+```
+
+```python
+In [1]: from minydra import Parser
+
+In [2]: Parser(defaults=["./examples/demo.json", "./examples/demo2.json"]).args.pretty_print();
+╭─────────────────────────────────╮
+│ log │
+│ │logger │
+│ │ │log_level : INFO │
+│ │ │logger_name : minydra │
+│ │outdir : /some/other/path │
+│ │project : demo │
+│ new_key : 3 │
+│ verbose : False │
+╰─────────────────────────────────╯
+```
## Protected attributes
`MinyDict`'s methods (including the `dict` class's) are protected, they are read-only and you cannot therefore set _attributes_ with there names, like `args.get = 2`. If you do need to have a `get` argument, you can access it through _items_: `args["get"] = 2`.
diff --git a/examples/defaults.py b/examples/defaults.py
new file mode 100644
index 0000000..b69466f
--- /dev/null
+++ b/examples/defaults.py
@@ -0,0 +1,8 @@
+from pathlib import Path
+
+from minydra import resolved_args
+
+if __name__ == "__main__":
+
+ args = resolved_args(defaults=Path(__file__).parent / "demo.yaml")
+ args.pretty_print()
diff --git a/examples/demo.json b/examples/demo.json
index 1408972..a397eed 100644
--- a/examples/demo.json
+++ b/examples/demo.json
@@ -8,4 +8,4 @@
}
},
"verbose": false
-}
\ No newline at end of file
+}
diff --git a/examples/demo2.json b/examples/demo2.json
new file mode 100644
index 0000000..aba4456
--- /dev/null
+++ b/examples/demo2.json
@@ -0,0 +1,11 @@
+{
+ "log": {
+ "outdir": "/some/other/path",
+ "logger": {
+ "log_level": "INFO",
+ "logger_name": "minydra"
+ }
+ },
+ "verbose": false,
+ "new_key": 3
+}
diff --git a/minydra/__init__.py b/minydra/__init__.py
index 9461ca2..375c53a 100644
--- a/minydra/__init__.py
+++ b/minydra/__init__.py
@@ -1,7 +1,7 @@
from .dict import MinyDict # noqa: F401
from .parser import Parser
-__version__ = "0.1.4"
+__version__ = "0.1.5"
def parse_args(
@@ -10,6 +10,7 @@ def parse_args(
warn_overwrites=True,
parse_env=True,
warn_env=True,
+ defaults=None,
):
def decorator(function):
def wrapper(*args, **kwargs):
@@ -19,6 +20,7 @@ def wrapper(*args, **kwargs):
warn_overwrites=warn_overwrites,
parse_env=parse_env,
warn_env=warn_env,
+ defaults=defaults,
)
result = function(parser.args)
return result
@@ -34,6 +36,7 @@ def resolved_args(
warn_overwrites=True,
parse_env=True,
warn_env=True,
+ defaults=None,
):
return Parser(
verbose=verbose,
@@ -41,4 +44,5 @@ def resolved_args(
warn_overwrites=warn_overwrites,
parse_env=parse_env,
warn_env=warn_env,
+ defaults=defaults,
).args.resolve()
diff --git a/minydra/parser.py b/minydra/parser.py
index 40689ed..2b6166a 100644
--- a/minydra/parser.py
+++ b/minydra/parser.py
@@ -1,11 +1,13 @@
import ast
import os
+import pathlib
import re
import sys
-from typing import Any, List, Optional
+from typing import Any, List, Optional, Union
from minydra.dict import MinyDict
from minydra.exceptions import MinydraWrongArgumentException
+from minydra.utils import resolve_path
class Parser:
@@ -32,6 +34,7 @@ def __init__(
warn_overwrites=True,
parse_env=True,
warn_env=True,
+ defaults=None,
) -> None:
"""
Create a Minydra Parser to parse arbitrary commandline argument as:
@@ -51,6 +54,9 @@ def __init__(
as key or value in the command line. Defaults to True.
warn_env (bool, optional): Wether to print a warning in case an environment
variable is parsed but no value is found. Defaults to True.
+ defaults (Union[str, dict, MinyDict], optional): The set of allowed keys as
+ a (Miny)dict or a path to a file that `minydra.MinyDict` will be able to
+ load (as `json`, `pickle` or `yaml`)
"""
super().__init__()
@@ -64,6 +70,48 @@ def __init__(
self._print("sys.argv:", self._argv)
self._parse_args()
+ if defaults is not None or self.args["@defaults"]:
+ default = self.load_defaults(self.args["@defaults"] or defaults)
+ args = self.args.deepcopy().resolve()
+ args_defaults = args["@defaults"]
+ if args["@defaults"]:
+ del args["@defaults"]
+ self.args = default.update(args, strict=True)
+ if args_defaults:
+ self.args["@defaults"] = args_defaults
+
+ @staticmethod
+ def load_defaults(default: Union[str, dict, MinyDict]):
+ """
+ Set the default keys.
+
+ Args:
+ allow (Union[str, dict, MinyDict]): The set of allowed keys as a
+ (Miny)dict or a path to a file that `minydra.MinyDict` will be able to
+ load (as `json`, `pickle` or `yaml`)
+ """
+ if isinstance(default, (str, pathlib.Path)):
+ default = resolve_path(default)
+ assert default.exists()
+ assert default.is_file()
+ if default.suffix not in {".json", ".yaml", ".yml", ".pickle", ".pkl"}:
+ raise ValueError(f"{str(default)} is not a valid file extension.")
+ if default.suffix in {".yaml", ".yml"}:
+ default = MinyDict.from_yaml(default)
+ elif default.suffix in {".pickle", ".pkl"}:
+ default = MinyDict.from_pickle(default)
+ else:
+ default = MinyDict.from_json(default)
+ elif isinstance(default, dict):
+ default = MinyDict(default).resolve()
+ elif isinstance(default, list):
+ defaults = [Parser.load_defaults(d) for d in default]
+ default = MinyDict()
+ for d in defaults:
+ default.update(d, strict=False)
+
+ assert isinstance(default, MinyDict)
+ return default
def _print(self, *args, **kwargs):
if self.verbose > 0:
@@ -177,7 +225,6 @@ def _force_type(value: str, type_str: str) -> Any:
return float(value)
if type_str == "str":
return str(value)
- return value
@staticmethod
def _infer_arg_type(arg: Any, type_str: Optional[str] = None) -> Any:
diff --git a/requirements-test.txt b/requirements-test.txt
index 4eb528b..89f8c7f 100644
--- a/requirements-test.txt
+++ b/requirements-test.txt
@@ -4,4 +4,4 @@ isort
pre-commit
pytest
pytest-cov
-tox
\ No newline at end of file
+tox
diff --git a/tests/test_minydra.py b/tests/test_minydra.py
index 4142b5e..c637d15 100644
--- a/tests/test_minydra.py
+++ b/tests/test_minydra.py
@@ -1,7 +1,12 @@
+import json
import sys
+from pathlib import Path
from unittest.mock import patch
+import pytest
+
import minydra
+from minydra.dict import MinyDict
def test_resolved_args():
@@ -21,3 +26,59 @@ def main(args):
assert args.foo == "bar"
main()
+
+
+def test_defaults():
+
+ examples = Path(__file__).resolve().parent.parent / "examples"
+ d1 = examples / "demo.json"
+ d2 = examples / "demo2.json"
+ y1 = examples / "demo.yaml"
+
+ p = MinyDict({"a": "2", "c": 3, "d": {"e": {"f": 4, "g": 5}}})
+ pkl = p.to_pickle(Path(__file__).resolve().parent / "test.pkl")
+
+ with patch.object(sys, "argv", [""]):
+ args = minydra.resolved_args(defaults=p)
+ assert args == p
+
+ with patch.object(sys, "argv", ["", "d.e.f=2"]):
+ args = minydra.resolved_args(defaults=p)
+ assert args.d.e.f == 2
+
+ with patch.object(sys, "argv", ["", f"@defaults={str(d1)}"]):
+ args = minydra.resolved_args()
+ del args["@defaults"]
+ assert args.to_dict() == json.loads(d1.read_text())
+
+ with patch.object(sys, "argv", ["", f"@defaults={str(y1)}"]):
+ args = minydra.resolved_args()
+ del args["@defaults"]
+ assert args.to_dict() == MinyDict.from_yaml(y1)
+
+ with patch.object(sys, "argv", ["", f"@defaults={str(pkl)}"]):
+ args = minydra.resolved_args()
+ del args["@defaults"]
+ assert args.to_dict() == MinyDict.from_pickle(pkl)
+ Path(pkl).unlink()
+
+ with pytest.raises(ValueError):
+ with patch.object(
+ sys, "argv", ["", f"@defaults={str(d1).replace('.json', '.py')}"]
+ ):
+ args = minydra.resolved_args()
+
+ with pytest.raises(KeyError):
+ with patch.object(sys, "argv", ["", f"@defaults={str(d1)}", "new_key=3"]):
+ args = minydra.resolved_args()
+ del args["@defaults"]
+ assert args.to_dict() == json.loads(d1.read_text())
+
+ double_defaults = f"['{str(d1)}', '{str(d2)}']"
+ with patch.object(sys, "argv", ["", f"@defaults={double_defaults}", "new_key=3"]):
+ args = minydra.resolved_args()
+ d1d = MinyDict.from_json(d1)
+ d2d = MinyDict.from_json(d2)
+ d1d = d1d.update(d2d)
+ del args["@defaults"]
+ assert args == d1d
diff --git a/tests/test_parser_static.py b/tests/test_parser_static.py
index 8a1c6d2..0564a14 100644
--- a/tests/test_parser_static.py
+++ b/tests/test_parser_static.py
@@ -91,42 +91,42 @@ def test_set_env(monkeypatch, capfd):
assert "Detected variable $MINY_VAR_DOES_NOT_EXIST" in out
-def test_sanitize_args_int():
- assert Parser.sanitize_args({"a": "3"}) == {"a": 3}
+def test_parse_arg_types_int():
+ assert Parser.parse_arg_types({"a": "3"}) == {"a": 3}
-def test_sanitize_args_float():
- assert Parser.sanitize_args({"a": "3.1"}) == {"a": 3.1}
- assert Parser.sanitize_args({"a": "1e-4"}) == {"a": 0.0001}
+def test_parse_arg_types_float():
+ assert Parser.parse_arg_types({"a": "3.1"}) == {"a": 3.1}
+ assert Parser.parse_arg_types({"a": "1e-4"}) == {"a": 0.0001}
-def test_sanitize_args_bool():
- assert Parser.sanitize_args({"a": "true"}) == {"a": True}
- assert Parser.sanitize_args({"a": "True"}) == {"a": True}
- assert Parser.sanitize_args({"a": "false"}) == {"a": False}
- assert Parser.sanitize_args({"a": "False"}) == {"a": False}
+def test_parse_arg_types_bool():
+ assert Parser.parse_arg_types({"a": "true"}) == {"a": True}
+ assert Parser.parse_arg_types({"a": "True"}) == {"a": True}
+ assert Parser.parse_arg_types({"a": "false"}) == {"a": False}
+ assert Parser.parse_arg_types({"a": "False"}) == {"a": False}
-def test_sanitize_args_list():
- assert Parser.sanitize_args({"a": "[]"}) == {"a": []}
- assert Parser.sanitize_args({"a": "[1, 2]"}) == {"a": [1, 2]}
- assert Parser.sanitize_args({"a": "[1, 2, 'b']"}) == {"a": [1, 2, "b"]}
- assert Parser.sanitize_args({"a": "[1, 2, 'b']", "c": 4}) == {
+def test_parse_arg_types_list():
+ assert Parser.parse_arg_types({"a": "[]"}) == {"a": []}
+ assert Parser.parse_arg_types({"a": "[1, 2]"}) == {"a": [1, 2]}
+ assert Parser.parse_arg_types({"a": "[1, 2, 'b']"}) == {"a": [1, 2, "b"]}
+ assert Parser.parse_arg_types({"a": "[1, 2, 'b']", "c": 4}) == {
"a": [1, 2, "b"],
"c": 4,
}
- assert Parser.sanitize_args({"a": "[1, [2, 3]]"}) == {"a": [1, [2, 3]]}
- assert Parser.sanitize_args({"a": "[1, [2, 1e-3]]"}) == {"a": [1, [2, 0.001]]}
- assert Parser.sanitize_args({"a": "['false']"}) == {"a": [False]}
- assert Parser.sanitize_args({"a": "[False]"}) == {"a": [False]}
+ assert Parser.parse_arg_types({"a": "[1, [2, 3]]"}) == {"a": [1, [2, 3]]}
+ assert Parser.parse_arg_types({"a": "[1, [2, 1e-3]]"}) == {"a": [1, [2, 0.001]]}
+ assert Parser.parse_arg_types({"a": "['false']"}) == {"a": [False]}
+ assert Parser.parse_arg_types({"a": "[False]"}) == {"a": [False]}
-def test_sanitize_args_dict(monkeypatch):
- assert Parser.sanitize_args({"a": "{}"}) == {"a": {}}
- assert Parser.sanitize_args({"a": "{1: 2}"}) == {"a": {1: 2}}
- assert Parser.sanitize_args({"a": "{1: 2e-3}"}) == {"a": {1: 0.002}}
- assert Parser.sanitize_args({"a": "{1: False}"}) == {"a": {1: False}}
- assert Parser.sanitize_args({"a": "{1: [1, 3]}"}) == {"a": {1: [1, 3]}}
+def test_parse_arg_types_dict(monkeypatch):
+ assert Parser.parse_arg_types({"a": "{}"}) == {"a": {}}
+ assert Parser.parse_arg_types({"a": "{1: 2}"}) == {"a": {1: 2}}
+ assert Parser.parse_arg_types({"a": "{1: 2e-3}"}) == {"a": {1: 0.002}}
+ assert Parser.parse_arg_types({"a": "{1: False}"}) == {"a": {1: False}}
+ assert Parser.parse_arg_types({"a": "{1: [1, 3]}"}) == {"a": {1: [1, 3]}}
monkeypatch.setenv("USER", "TestingUser")
- assert Parser.sanitize_args({"a": "{1: '$USER'}"}) == {"a": {1: "TestingUser"}}
+ assert Parser.parse_arg_types({"a": "{1: '$USER'}"}) == {"a": {1: "TestingUser"}}