Coder Social home page Coder Social logo

omni-us / jsonargparse Goto Github PK

View Code? Open in Web Editor NEW
314.0 4.0 43.0 9.33 MB

Implement minimal boilerplate CLIs derived from type hints and parse from command line, config files and environment variables

Home Page: https://jsonargparse.readthedocs.io

License: MIT License

Python 100.00%
python json yaml jsonnet argparse-alternative dataclasses type-hints argparse cli configuration-files

jsonargparse's Introduction

https://readthedocs.org/projects/jsonargparse/badge/?version=stable https://sonarcloud.io/api/project_badges/measure?project=omni-us_jsonargparse&metric=alert_status

jsonargparse

Docs: https://jsonargparse.readthedocs.io/ | Source: https://github.com/omni-us/jsonargparse/

jsonargparse is a library for creating command-line interfaces (CLIs) and making Python apps easily configurable. It is a well-maintained project with frequent releases, adhering to high standards of development: semantic versioning, deprecation periods, changelog, automated testing, and full test coverage.

Although jsonargparse might not be widely recognized yet, it already boasts a substantial user base. Most notably, it serves as the framework behind pytorch-lightning's LightningCLI.

Features

jsonargparse is user-friendly and encourages the development of clean, high-quality code. It encompasses numerous powerful features, some unique to jsonargparse, while also combining advantages found in similar packages:

Other notable features include:

  • Extensive type hint support: nested types (union, optional), containers (list, dict, etc.), user-defined generics, restricted types (regex, numbers), paths, URLs, types from stubs (*.pyi), future annotations (PEP 563), and backports (PEPs 604/585).
  • Keyword arguments introspection: resolving of parameters used via **kwargs.
  • Dependency injection: support types that expect a class instance and callables that return a class instance.
  • Structured configs: parse config files with more understandable non-flat hierarchies.
  • Config file formats: json, yaml, jsonnet and extendible to more formats.
  • Relative paths: within config files and parsing of config paths referenced inside other configs.
  • Argument linking: directing parsed values to multiple parameters, preventing unnecessary interpolation in configs.

Design principles

  • Non-intrusive/decoupled:

    There is no requirement for unrelated modifications throughout a codebase, maintaining the separation of concerns principle. In simpler terms, changes should make sense even without the CLI. No need to inherit from a special class, add decorators, or use CLI-specific type hints.

  • Minimal boilerplate:

    A recommended practice is to write code with function/class parameters having meaningful names, accurate type hints, and descriptive docstrings. Reuse these wherever they appear to automatically generate the CLI, following the don't repeat yourself principle. A notable advantage is that when parameters are added or types changed, the CLI will remain synchronized, avoiding the need to update the CLI's implementation.

  • Dependency injection:

    Using as type hint a class or a callable that instantiates a class, a practice known as dependency injection, is a sound design pattern for developing loosely coupled and highly configurable software. Such type hints should be supported with minimal restrictions.

Installation

You can install using pip as:

pip install jsonargparse

By default the only dependency that jsonargparse installs is PyYAML. However, several optional features can be enabled by specifying any of the following extras requires: signatures, jsonschema, jsonnet, urls, fsspec, ruyaml, omegaconf and argcomplete. There is also the all extras require to enable all optional features. Installing jsonargparse with extras require is as follows:

pip install "jsonargparse[signatures,urls]"  # Enable signatures and URLs features
pip install "jsonargparse[all]"              # Enable all optional features

jsonargparse's People

Contributors

0x404 avatar borda avatar bryant1410 avatar carmocca avatar diederus avatar erotemic avatar hadipash avatar ioangatop avatar kianmeng avatar lgtm-migrator avatar mauvilsa avatar mougams avatar mquillot avatar nkrishnaswami avatar pierregtch avatar pre-commit-ci[bot] avatar psirenny avatar solvik avatar speediedan avatar tshu-w avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

jsonargparse's Issues

Automatically add Optional type to kwargs with None default

I was having a look a this PR and decided to give it a try.

# In parsing_workout.py
class Dummy:
    def __init__(self, dummy: float = None):
        pass

Then execute

python parsing_workout.py --print_config > conf.yml
python parsing_workout.py --config conf.yml

I get the following error :

parsing_workout.py: error: Parser key "data.dummy": float() argument must be a string or a number, not 'NoneType'

Notably, this doesn't crash with IPython.

I think it would make sense to automatically add Optional to the type in those cases, WDYT?

Parsing a Union of int and str from config.json casts type incorrectly

When the types in a Union are a str and and int, a str in the config.json is parsed as a int.

Example config:

{ "int_or_str": "1" }

Example script:

from jsonargparse import ArgumentParser, ActionConfigFile
from typing import Union

if __name__ == "__main__":

    union = Union[str,int]
    parser = ArgumentParser()
    parser.add_argument("--cfg", action=ActionConfigFile)
    parser.add_argument("--int_or_str", type=union )
    args = parser.parse_args()
    print(args.int_or_str, type(args.int_or_str))

Output:

python test.py --cfg config.json
typing.Union[str, int]
1 <class 'int'>

Check types after overwriting

The problem

If the value is wrongly typed in the config file (for example if a non-optional positional arg was not specified with --print_config), and is being overwritten by a valid value, an error is raised nonetheless.

What I'd like

Check types after all the parsing has been done, to allow this use case.

Minimal example

# parsing_workout.py
import jsonargparse

class Model:
    def __init__(self, n: int):
        pass

def get_args():
    parser = jsonargparse.ArgumentParser(parse_as_dict=True, description="Trial")
    parser.add_argument(
        "--config", action=jsonargparse.ActionConfigFile, help="Configuration file"
    )
    parser.add_class_arguments(Model, "model")
    args = parser.parse_args(with_meta=False)
    return args

def main():
    args = get_args()
    print(args)

if __name__ == "__main__":
    main()

Way that works

python parsing_workout.py --model.n 2 --print_config > conf.yml
# This works
python parsing_workout.py --config conf.yml --model.n 3

Way that doesn't

python parsing_workout.py --print_config > conf.yml
# This doesn't work
python parsing_workout.py --config conf.yml --model.n 3

and this is the error message:

parsing_workout.py: error: Parser key "data.n_src": int() argument must be a string, a bytes-like object or a number, not 'NoneType'

Optional types doesn't work with custom actions

This does work:

class CustomAction(argparse.Action):
    def __call__(self, parser, args, values, option_string=None):
        print('Custom action!')
        setattr(args, self.dest, values)

parser = jsonargparse.ArgumentParser()
parser.add_argument('--path', type=str, action=CustomAction)

parser.parse_args(['--path=~/data'])   

But this doesn't:

class CustomAction(argparse.Action):
    def __call__(self, parser, args, values, option_string=None):
        print('Custom action!')
        setattr(args, self.dest, values)

parser = jsonargparse.ArgumentParser()
parser.add_argument('--path', type=Optional[str], action=CustomAction)

parser.parse_args(['--path=~/data'])   

Parsing dataclass from file doesn't work with required=True or default value

Hi! I am attempting to parse a config file into an namespace, such that the whole contents of the config YAML file is assigned to that namespace. I'm using a dataclass that matches the format of the YAML file.

I've found two ways to do this:

parser.add_argument('--server-config', type=ServerConfig)
# or
parser.add_dataclass_arguments(ServerConfig', 'server-config')

However, there doesn't seem to be a way to provide a default value or mark the argument as required. If I try to provide a default via default='server-config.yaml' I get a ValueError saying the string is not an instance of the class:

ValueError: Expected "default" argument to be an instance of "ServerConfig" or its kwargs dict, given server-config.yaml

And if I try to mark it as required then I always get an error saying the field is required but not included, even when I pass --server-config server-config.yaml on the command-line.

Is using the library in this way supported and if so, how might I provide a default value or make it required? Is the only way to provide a default config file and mark options as required to define everything explicitly, as opposed to using dataclasses?

Dataclass as CLI function argument

Hello again!

Is this supported by the CLI?

from dataclasses import dataclass
from jsonargparse import CLI

@dataclass
class MySQLConfig:
    host: str = "localhost"
    port: int = 3306

def my_app(cfg: MySQLConfig) -> None:
    print(f"{cfg.host}, {cfg.port}")

if __name__ == "__main__":
    CLI(my_app)

where python file.py would print localhost, 3306 and you could do something like python file.py --cfg.host="test" and it would print test, 3307

Thanks!

Error parsing nargs=+ positional argument from config file

This one is tricky, here is the snippet:

import jsonargparse

parser = jsonargparse.ArgumentParser()
parser.add_argument("--config", action=jsonargparse.ActionConfigFile)
parser.add_argument("my_list", type=str, nargs="+")
args = parser.parse_args()

Using the config generated by --print-config:
my_list: null
the error is:
error: Parser key "my_list": 'NoneType' object is not iterable

I believe the correct error should be:
TypeError: Key "my_list" is required but not included in config object.

Multiple YAML files

Looks great :)
Would it be possible to load from multiple YAML files ?
One YAML would be including the defaults, and the other ones only the diffs, in a cascading manner (according to order of loading).
Thanks.

'-' and '_' in nested parsers

This does work:

from jsonargparse import ArgumentParser, ActionParser
p1 = ArgumentParser()
p1.add_argument('--op1_like')

p2 = ArgumentParser()
p2.add_argument('--op2_like', action=ActionParser(parser=p1))

p3 = ArgumentParser()
p3.add_argument('--op3', action=ActionParser(parser=p2))

>>> p3.parse_args(['--op3.op2_like.op1_like=1'])

But this doesn't work:

from jsonargparse import ArgumentParser, ActionParser
p1 = ArgumentParser()
p1.add_argument('--op1-like')

p2 = ArgumentParser()
p2.add_argument('--op2-like', action=ActionParser(parser=p1))

p3 = ArgumentParser()
p3.add_argument('--op3', action=ActionParser(parser=p2))

>>> p3.parse_args(['--op3.op2-like.op1-like=1'])
# error: Unrecognized arguments: --op3.op2-like.op1-like=1

Empty / Null argument in config file should fallback to default value

I have a use-case where I want to populate from a config file, with empty values for some parameters. These parameters have a type but have None as default, so i want to be able to keep them in the config with their default value (which is empty/None).

Example

$ cat config.yaml
a:
import jsonargparse as argparse

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('--config', action=argparse.ActionConfigFile)
    parser.add_argument('--a', type=int, default=None)
    print(parser.parse_args())
$ python3 test.py --config config.yaml
[...]
jsonargparse.ParserError: Parser key "a": int() argument must be a string, a bytes-like object or a number, not 'NoneType'

I would expect the parser to skip empty values and use the default value. So for parameters which explicitly allow None values it should be possible to pass it.

Question

Should jsonargparse allow empty values for typed parameters and use the default value instead of throwing a parsing error?
Or is there already a way to achieve this with some options in jsonargparse?

Edit: I just tested this with type=str, where this doesn't occur, so this issue seems to be limited to int (and similar types).
Edit2: With type=str the parameter will have the value "None" as a string, so this applies to string also. See solution below to handle this case.

Arguments with Union and Iterable are not added by add_class_arguments

Hi,

when using Union together with Iterable for an argument this argument is not detected by add_class_arguments. See the following code example

from typing import Union, Iterable, List
from jsonargparse import ArgumentParser


class OtherClass(object):
    def __init__(self, name: str = "test"):
        self.name = name


class Class1(object):
    def __init__(
        self, other: Union[OtherClass, List[OtherClass], bool] = True
    ):
        self.other = other


class Class2(object):
    def __init__(
        self, other: Union[OtherClass, Iterable[OtherClass], bool] = True
    ):
        self.other = other


if __name__ == "__main__":
    parser1 = ArgumentParser()
    parser2 = ArgumentParser()
    parser1.add_class_arguments(Class1, "Class1")
    parser2.add_class_arguments(Class2, "Class2")

    cfg1 = parser1.parse_args()
    cfg2 = parser2.parse_args()

    print(cfg1)
    print(cfg2)

with the following output

Namespace(Class1=Namespace(other=True))
Namespace()

I would expect this output

Namespace(Class1=Namespace(other=True))
Namespace(Class2=Namespace(other=True))

However, accordingt to the list in https://jsonargparse.readthedocs.io/en/stable/index.html?highlight=class_path#type-hints Union together with Iterable should be supported.

[Feature request] Add arguments description as comments in print_config output

Hello and thank you for the great library !

Feature request

The --print_config feature is useful but limited: this would be great if there was a way to print a config file in YAML format containing comments to describe the arguments. I'm aware that this kind of feature depends on the format used for the dump, because JSON does not support comments.

Suggestions are:

  • add an optional argument to the ArgumentParser.dump method to allow printing comments (only for YAML files). This will need a modification of the way the YAML is dumped
  • or add list of actions and groups to public API to allow custom implementation of this feature (as is, if I'm correct, there is no way to access Action objects except parser._actions which is not public)

Example script

import jsonargparse

parser = jsonargparse.ArgumentParser(parse_as_dict=True, description="Test print-config")

parser.add_argument("--argument1", default=1, help="Description argument 1")
parser.add_argument("--argument2", default='VAL2', help="Description argument 2")

group = parser.add_argument_group(title='Group title')
group.add_argument("--level.argument3", default=3, help="Description argument 3")
group.add_argument("--level.argument4", default=4, required=True, help="Description argument 4")

parser.parse_args()

Current behaviour

Using the --print_config option produce this output:

argument1: 1   
argument2: VAL2
level:
  argument3: 3 
  argument4: 4

Wanted behaviour

This would be great if I could produce this kind of output (using --print-config or ArgumentParser.dump method):

# optional arguments:
# ----------------------

# Description argument 1 (default: 1)
argument1: 1

# Description argument 2 (default: VAL2)
argument2: VAL2

# Group title
# ------------

level:
  # Description argument 3 (default: 3)
  argument3: 3 

  # Description argument 4 (required, default: 4)
  argument4: 4

The file here should still be a valid YAML file. Comments used here could be the same as for the --help method (with types, default values, ...).

Option to show full help message with ActionParser arguments.

I actively am using subparsers with jsonargparse.ActionParser. The problem is -- arguments for subparser's nodes don't appear in main parser help message.
I know it's outlined in README, but It's very confusing to see half of arguments available withscript.py --help.

Default arguments with default_config_files

I've stumbled upon an issue using default arguments and default_config_files

Here's the config

# cat /root/config.yaml
a: b

The code

import jsonargparse

p = jsonargparse.ArgumentParser(
    default_config_files=[
        '/root/config.yaml'
    ],
    prog='test',
)

p.add_argument('-c', '--config', action=jsonargparse.ActionConfigFile)
p.add_argument('-a', default='c')
print(p.parse_args())

And its execution

# python3 test.py 
namespace(__cwd__=['/root'], a='c', config=None)
# python3 test.py -c /root/config.yaml
namespace(__cwd__=['/root'], __path__=Path(path="/root/config.yaml", abs_path="/root/config.yaml", cwd="/root"), a='b', config=[Path(path="/root/config.yaml", abs_path="/root/config.yaml", cwd="/root")])

Required arguments?

Kudos for the project, it solves a lot of problem for me :)

I've tried to get my head around required arguments, but it seems that argparse takes over jsonargparse in the order.
Even though the variable is present in the file, it throws the error.

import jsonargparse

p = jsonargparse.ArgumentParser(
    default_config_files=[
        '~/config.yaml'
    ],
    prog='test',
)

p.add_argument('-c', '--config', action=jsonargparse.ActionConfigFile)
p.add_argument('-a', required=True)
p.add_argument('-b', required=False)
print(p.parse_args())
$ cat config.yaml
a: value1
b: value2

It gives:

Traceback (most recent call last):
  File "test.py", line 13, in <module>
    print(p.parse_args())
  File "/usr/local/lib/python3.7/site-packages/jsonargparse.py", line 199, in parse_args
    cfg = super().parse_args(args=args, namespace=namespace)
  File "/usr/local/Cellar/python/3.7.4/Frameworks/Python.framework/Versions/3.7/lib/python3.7/argparse.py", line 1749, in parse_args
    args, argv = self.parse_known_args(args, namespace)
  File "/usr/local/Cellar/python/3.7.4/Frameworks/Python.framework/Versions/3.7/lib/python3.7/argparse.py", line 1781, in parse_known_args
    namespace, args = self._parse_known_args(args, namespace)
  File "/usr/local/Cellar/python/3.7.4/Frameworks/Python.framework/Versions/3.7/lib/python3.7/argparse.py", line 2016, in _parse_known_args
    ', '.join(required_actions))
  File "/usr/local/lib/python3.7/site-packages/jsonargparse.py", line 478, in error
    raise ParserError(message)
jsonargparse.ParserError: the following arguments are required: -a

Also, it seems that default_config_files doesn't work:

$ python3 test.py
namespace(a=None, b=None, config=None)

Support Optional[Enum]

from dataclasses import dataclass
from enum import Enum
from typing import Optional
import jsonargparse

class MyEnum(Enum):
    A = "A"
    B = "B"

@dataclass
class Foo:
    value: Optional[MyEnum] = None

parser = jsonargparse.ArgumentParser()
parser.add_class_arguments(Foo)
args = parser.parse_args(["--value=A"], with_meta=False)
# error: Unrecognized arguments: --value=A
assert args.value is MyEnum.A

Works without Optional

Inconsistent behaviour for DefaultHelpFormatter subclass

Im trying to subclass jsonargparse.DefaultHelpFormatter but its format is different than the parents

import argparse
import jsonargparse

class MyFormatter(jsonargparse.DefaultHelpFormatter): ...

parser = jsonargparse.ArgumentParser(formatter_class=MyFormatter)
parser.add_argument('--foo', type=float, default=0.123)
parser.print_help()
"""
optional arguments:
  -h, --help            show this help message and exit
  --print-config        print configuration and exit
  ARG:   --foo FOO
  ENV:   FOO
"""

parser = jsonargparse.ArgumentParser(formatter_class=jsonargparse.DefaultHelpFormatter)
parser.add_argument('--foo', type=float, default=0.123)
parser.print_help()
"""
optional arguments:
  -h, --help      show this help message and exit
  --print-config  print configuration and exit
  --foo FOO
"""

Notice how the second case does not include the ARG and ENV fields.

I believe the fix is to replace:

if self.formatter_class == DefaultHelpFormatter:

for:

if issubclass(self.formatter_class, DefaultHelpFormatter)

Thanks!

Bug parsing Optional[str]

This snippet does not work when Optional is used:

from pathlib import Path
from typing import Optional
import jsonargparse

# Works if `Optional` is removed
def foo(filepath: Optional[str] = None): ...

parser = jsonargparse.ArgumentParser()
parser.add_function_arguments(foo)

# Write something to the file
f = Path("/tmp/kk")
f.write_text("this is a test")

args = parser.parse_args([f"--filepath={f}"], with_meta=False)

print(args)  # Namespace(filepath='this is a test')
# filepath becomes the file contents instead of "tmp/kk"
assert args.filepath == str(f)  # AssertionError

Somehow, the file contents are parsed instead of the filepath

Missing class description when docstring is a f-string

To reproduce

from dataclasses import dataclass

import jsonargparse


@dataclass
class Foo:
    """
    Foo docstring

    Args:
        foo: foo arg
    """
    foo: str = "foo"


@dataclass
class Bar:
    f"""
    {"Bar"} docstring

    Args:
        bar: bar arg
    """
    bar: str = "bar"


parser = jsonargparse.ArgumentParser()
parser.add_class_arguments(Foo, "foo")
parser.add_class_arguments(Bar, "bar")
parser.print_help()

Output

usage: scratch_2.py [-h] [--print-config] [--foo.foo FOO] [--bar.bar BAR]

optional arguments:
  -h, --help      show this help message and exit
  --print-config  print configuration and exit

Foo docstring:
  --foo.foo FOO   foo arg (default: foo)

Bar(bar: str = 'bar'):
  --bar.bar BAR

Expected

usage: scratch_2.py [-h] [--print-config] [--foo.foo FOO] [--bar.bar BAR]

optional arguments:
  -h, --help      show this help message and exit
  --print-config  print configuration and exit

Foo docstring:
  --foo.foo FOO   foo arg (default: foo)

Bar docstring:
  --bar.bar BAR   bar arg (default: bar)

Validate class arguments

Hi!

In my code, I validate the inputs to the parser with the add_argument type, e.g.

import argparse


class PositiveNumber:
    def __init__(self, type):
        self.type = type

    def __call__(self, v):
        v = self.type(v)
        if v < 0:
            raise argparse.ArgumentTypeError("Must be positive")
        return v

parser = argparse.ArgumentParser()
parser.add_argument("--learning_rate", type=PositiveNumber(float))
args = parser.parse_args(["--learning_rate=-1"])
"""
usage: scratch_2.py [-h] [--learning_rate LEARNING_RATE]
scratch_2.py: error: argument --learning_rate: Must be positive
"""

However, it is not currently possible with add_class_arguments:

import argparse
from dataclasses import dataclass

import jsonargparse


class PositiveNumber:
    def __init__(self, type):
        self.type = type

    def __call__(self, v):
        v = self.type(v)
        if v < 0:
            raise argparse.ArgumentTypeError("Must be positive")
        return v

@dataclass
class Optimizer:
    """
    Optimizer options

    Args:
        learning_rate: Learning rate (must be > 0)
    """
    learning_rate: PositiveNumber(float) = 0.001

# AssertionError is not raised. __call__ is not called
# expectedly, python does not validate
x = Optimizer(learning_rate=-1)

parser = jsonargparse.ArgumentParser()
parser.add_class_arguments(Optimizer, "optimizer")
args = parser.parse_args(["--optimizer.learning_rate=-1"])
# I believe this is a bug, fails to parse at all
# works with learning_rate: float = 0.001
"""
usage: scratch_1.py [-h] [--print-config]
scratch_1.py: error: Unrecognized arguments: --optimizer.learning_rate=-1
"""

Would it be possible for jsonargparse to run __call__ on the types of each argument?

Support dataclasses.field as the default value

field is necessary for dataclasses with mutable fields. However, jsonargparse fails to validate them:

import dataclasses
from typing import List
import jsonargparse

@dataclasses.dataclass
class Foo:
    # the default value cannot be a mutable
    # object so a field has to be used
    bar: List[str] = dataclasses.field(default_factory=lambda: ["foo"])

# this works
Foo()
# also with a value
Foo(bar=["foo", "bar"])

parser = jsonargparse.ArgumentParser()
parser.add_class_arguments(Foo)

# this also works
args = parser.parse_args(["--bar=[foo,bar]"], with_meta=False)
print(args) # Namespace(bar=['foo', 'bar'])

# but not without a value
parser.parse_args()
"""
Traceback (most recent call last):
  File "/home/python3.8/site-packages/jsonargparse/core.py", line 867, in check_config
    check_values(cfg)
  File "/home/python3.8/site-packages/jsonargparse/core.py", line 855, in check_values
    self._check_value_key(action, val, kbase, ccfg)
  File "/home/python3.8/site-packages/jsonargparse/core.py", line 1014, in _check_value_key
    value = action._check_type(value, cfg=cfg)  # type: ignore
  File "/home/python3.8/site-packages/jsonargparse/jsonschema.py", line 117, in _check_type
    raise TypeError('Parser key "'+self.dest+'"'+elem+': '+str(ex))
TypeError: Parser key "bar": <factory> is not of type 'array'

Failed validating 'type' in schema:
    {'items': {'type': 'string'}, 'type': 'array'}

On instance:
    <factory>
"""

If we avoid validation by adding Any, no exception is raised but it does not contain the correct default value:

import dataclasses
from typing import List, Union, Any
import jsonargparse

@dataclasses.dataclass
class Foo:
    # Any added to avoid validating it
    bar: Union[List[str], Any] = dataclasses.field(default_factory=lambda: ["foo"])

parser = jsonargparse.ArgumentParser()
parser.add_class_arguments(Foo)
args = parser.parse_args(with_meta=False)
print(args)  # Namespace(bar=<factory>)
print(type(args.bar))  # <class 'dataclasses._HAS_DEFAULT_FACTORY_CLASS'>

Any idea on how to support this? Or a valid workaround?

Sorry for giving you so much work haha. Im currently working on updating PyLaia to use jsonargparse so trying to report back all the rough edges I find along the way!

Feature request: Separate config in the same CLI

I forgot to mention in the other issues, but I've been looking at the source code a bit and playing with it and it seems great, so thanks! Making it similar to argparse was a great choice and will definitely weight in a lot for our adoption of jsonargparse.

One of the current possible way is to use --print_config, overwrite what we want and use --config to run the CLI.

In Asteroid (an audio source separation library), we support several datasets and several architectures. If we'd like to run the exact same architecture with the same trainer on all the datasets, could we think about composing config files so that a sub config file for an architecture could be reusable in several places?
Is this already possible? Maybe I didn't dig enough.

You might want to have a look at this PR in Asteroid which describes a bit what we'd use it for.

Example

It might look like that, where the --config is still necessary because the CLI would dump experiment_config.yml somewhere for reproducibility, and we could re-instantiate the run directly using --config.

import jsonargparse

class Model:
    def __init__(self, n: int):
        pass

class Data:
    def __init__(self, n: int = 8):
        pass

def get_args():
    parser = jsonargparse.ArgumentParser(parse_as_dict=True, description="Trial")
    parser.add_argument(
        "--config", action=jsonargparse.ActionConfigFile, help="Configuration file"
    )
    parser.add_argument(
        "--data.config", action=jsonargparse.ActionConfigFile, help="Configuration file"
    )
    parser.add_argument(
        "--model.config", action=jsonargparse.ActionConfigFile, help="Configuration file"
    )
    parser.add_class_arguments(Model, "model")
    parser.add_class_arguments(Data, "data")
    args = parser.parse_args(with_meta=False)
    return args

def main():
    args = get_args()
    print(args)

if __name__ == "__main__":
    main()

This is probably the last issue I'll raise tonight ๐Ÿ˜‰

Both environment variables and config file does not work

A variable in my config.yaml file is not parsed when having default_env=True:

I define my parser:
parser = ArgumentParser(env_prefix='CV', default_env=True, version=__version__)

and add the config file action to the argument:

parser.add_argument('--config', action=ActionConfigFile, help='Path to a yaml configuration file.')

And then my required argument is not set, even though it's present in the config file:
jsonargparse.ParserError: Config checking failed :: Key "outputpath" is required but its value is None.

Logging format KeyError bug

Last commit broke the following:

import jsonargparse


def config(fmt: str = "[%(asctime)s %(levelname)s %(name)s] %(message)s"):
    """
    Args:
        fmt: Logging format
    """
    pass

parser = jsonargparse.ArgumentParser()
parser.add_function_arguments(config)
parser.print_help()

KeyError: 'asctime'

ActionConfigFile fails when parse_as_dict=True

import jsonargparse

parser = jsonargparse.ArgumentParser(parse_as_dict=True)
parser.add_argument("--config", action=jsonargparse.ActionConfigFile)
args = parser.parse_args(["--config={}"])
# error: vars() argument must have __dict__ attribute

[Feature Request] YAML(json) variables

In ansible, variables can be defined in yaml file.
I think it is very useful. Maybe you can consider it as a feature.

Here are examples.
https://docs.ansible.com/ansible/latest/reference_appendices/YAMLSyntax.html

Further, Ansible uses "{{ var }}" for variables. If a value after a colon starts with a "{", YAML will think it is a dictionary, so you must quote it, like so:

foo: "{{ variable }}"

If your value starts with a quote the entire value must be quoted, not just part of it. Here are some additional examples of how to properly quote things:

foo: "{{ variable }}/additional/string/literal"
foo2: "{{ variable }}\\backslashes\\are\\also\\special\\characters"
foo3: "even if it's just a string literal it must all be quoted"

Moreover, you can include variables from other files
https://docs.ansible.com/ansible/latest/modules/include_vars_module.html

unexpected behavior difference between parse_args and parse_known_args?

I appreciate the flexibility jsonargparse offers us. But I think I've found some unexpected behaviors.

When adding an argument whos details you don't want to specify, and leaving it to be what ever is loaded from the yaml config file, for example, a dict, then if you parse with parse_args(), you would get the expected behavior. But if you pass an unkown argument , it would raise an Error, because unknown args are not allowed.

However, if you parse with parse_known_args, unkwon args are allowed, but you would not get the above behavior.

import jsonargparse

parser = jsonargparse.ArgumentParser()
parser.add_argument("--run", help="run configs") # leave it to be specified by the config file
parser.add_argument("--config", action=jsonargparse.ActionConfigFile, help="config file")

args = parser.parse_args() # change it to args, _ = parser.parse_known_args()
print(args)
run:
  speed: 30
  loop: 40
# parse_args
# namespace(__cwd__=['/workspace/projects/Parakeet/playground'], config=[Path(path="example.yaml", abs_path="/workspace/projects/Parakeet/playground/example.yaml", cwd="/workspace/projects/Parakeet/playground")], 
run=namespace(loop=40, speed=30))

# parse_known_args
# Namespace(__cwd__=['/workspace/projects/Parakeet/playground'], config=[Path(path="example.yaml", abs_path="/workspace/projects/Parakeet/playground/example.yaml", cwd="/workspace/projects/Parakeet/playground")], 
run=None, **{'run.loop': 40, 'run.speed': 30})

Defaults are missing in the namespace of the sub subcommand

Hi,

I've been trying to design cli with the several levels of subcommands. I want something like that:

app subcommand1 subcommand2 --arg1=foo

The problem is that when I add an argument with the default value into the subcommand2, this argument is not getting populated in the subcommand2 namespace.

Here is my code to verify that:

parser = ArgumentParser()
subcommand = ArgumentParser()
# this is a default argument for subcommand
subcommand.add_argument('--sub-foo', default="bar")

subcommand_subcommand = ArgumentParser()
# this is a default argument for subcommand2. This one will not be populated!
subcommand_subcommand.add_argument('--sub-sub-foo', default="bar")

root_subcommands = parser.add_subcommands()
root_subcommands.add_subcommand('subcommand1', subcommand)

sub_subcommands = subcommand.add_subcommands()
sub_subcommands.add_subcommand("subcommand2", subcommand_subcommand)


args = parser.parse_args(['subcommand1', 'subcommand2'])
print(args)

As the result I'm getting that:

Namespace(__cwd__=['....'], subcommand='subcommand1', subcommand1=Namespace(sub_foo='bar', subcommand='subcommand2'))

You can see here that the subcommand2 Namespace is missing in the result. For the subcommand1 namespace you see that the default argument is present (sub_foo='bar').

Is that a bug, or am I doing something wrong here?

Best regards,
Oleksii

AttributeError using --print-config and Enum

When I create an Enum field:

from dataclasses import dataclass
from enum import Enum
import jsonargparse

class MyEnum(Enum):
    A = "A"
    B = "B"

@dataclass
class Foo:
    # THIS WORKS
    value: MyEnum = MyEnum.A

    # THIS DOES NOT
    value: MyEnum = "A"
    # File "jsonargparse/core.py", line 620, in cleanup_actions
    # cfg[action.dest] = cfg[action.dest].name
    # AttributeError: 'str' object has no attribute 'name'

parser = jsonargparse.ArgumentParser()
parser.add_class_arguments(Foo)
args = parser.parse_args(with_meta=False)
print(args)

--print-config fails if the default value is not an Enum. Is this a bug or not supported?

The reason I set the default to a str instead of Enum is because the help message is tidier:

# with str
--value {A,B}   my value (default: "A")

# with Enum
--value {A,B}   my value (default: MyEnum.A)

In the second case, an user might think he has to pass --value=MyEnum.A which wouldn't work.
Maybe the correct fix is not supporting str but making the help message use the simpler form.

TypeError parsing Enums from ActionConfig

from enum import Enum
from jsonargparse import ArgumentParser, ActionConfigFile

class MyEnum(str, Enum):
    A = "A"
    B = "B"

parser = ArgumentParser()
parser.add_argument('--config', action=ActionConfigFile)
parser.add_argument("--enum", type=MyEnum, default=MyEnum.A)

# BUG: doesnt work if the yaml value is the same as the default
"""
$ cat cfg.yaml
enum: A
"""
parser.parse_args(["--config=cfg.yaml"])
# TypeError: Parser key "enum": value MyEnum.A not in MyEnum.

# this works
"""
$ cat cfg.yaml
enum: B
"""
parser.parse_args(["--config=cfg.yaml"])

Almost finished with the QA testing ๐Ÿคฃ

Arguments defined as booleans (ActionYesNo) cannot be set with environment variables

The code to reproduce is:

from jsonargparse import ArgumentParser, ActionYesNo
p = ArgumentParser(default_env=True, env_prefix='APP')
p.add_argument('--op', action=ActionYesNo, default=False)
print(p.parse_env({'APP_OP': 'true'}))

When running this gives as error:

usage: issue.py [-h] [--print_config [skip_null]] [--op]
issue.py: error: Value not boolean: ['true'].

Not able to mimic the behavior of argparse

I am playing with this library for my project, but encountering some issue with a basic behavior with type specification in add_argument.

While the following snippet

import argparse
this = argparse.ArgumentParser()
this.add_argument('--ints', nargs='+', type=int)
this.parse_args(args=['--ints', '3', '4', '5'])

gives Namespace(ints=[3, 4, 5]), the same snippet with jsonargparse, i.e.,

import jsonargparse
that = jsonargparse.ArgumentParser()
that.add_argument('--ints', nargs='+', type=int)
that.parse_args(args=['--ints', '3', '4', '5'])

raises an error: jsonargparse.ParserError: Parser key "temp": int() argument must be a string, a bytes-like object or a number, not 'list'. I believe this is not an expected behavior.

I took a quick look at parse_args implementation and found that it raises the error because the type conversion is done by calling parse_args of argparse in cfg = super().parse_args(args=args, namespace=namespace), while it tries to invoke actions again afterwards later in self.check_config(cfg_ns, skip_none=True).

Am I missing something? Otherwise, it would be great if this can be resolved :)

Confusing None vs null in the help message

Arguments whose default is None appear in the help message as (default: None) even though they have to be passed as --value=null. This can be confusing for users.

I can think of a few options:

  • Show (default: null) in the help message instead
  • Add support for value=None (null|none). Just as booleans do with (true|yes|false|no)
  • Just support value=None. Why was null chosen if it's not in the python language? is it a limitation of json/yaml?

What do you think?

Required for nested parsers

This does work:

from jsonargparse import ArgumentParser, ActionParser
p1 = ArgumentParser()
p1.add_argument('--op1', required=True)

>>> p1.parse_args(['--op1=1'])

But this doesn't work:

from jsonargparse import ArgumentParser, ActionParser
p1 = ArgumentParser()
p1.add_argument('--op1', required=True)

p2 = ArgumentParser()
p2.add_argument('--op2', action=ActionParser(parser=p1))

>>> p2.parse_args(['--op2.op1=1'])
# error: Configuration check failed :: Key "op1" is required but not included in config object or its value is None.

`--help` defaults values doesn't updated with default config files

Thanks for the full --help command.
Couple of small problems I have spotted so far:

  • Arguments order is sorted alphabetically (not with respect to time added). Probably related to #46.
  • Output contains python objects info, like ... object at 0x7f3670c6dc50.
  • Default values aren't update with having default config files.

Could a subclass type hint be a config file when using CLI?

I want to load a config file with the structure like:

input:
  class_path: src.input.KeyBoard
  init_args: input/keyboard.yaml

But it would not parse the file specified in init_args as a Dict. I also try to write it as

input:
  class_path: src.input.KeyBoard
  init_args:
    cfg: input/keyboard.yaml

and it still does not work. Is there any way to implement such function?

Bug parsing space for Optional type from config file

' ' is parsed as None

from typing import Optional
import jsonargparse

parser = jsonargparse.ArgumentParser()
parser.add_argument("--config", action=jsonargparse.ActionConfigFile)
parser.add_argument("--value", type=Optional[str], default=None)

# works as expected
args = parser.parse_args(["--value=' '"], with_meta=False)
print(args)  # Namespace(config=None, value=' ')
assert args.value == " "

# sanity check, this also works as expected
args = parser.parse_args(["--config={'value': 'test'}"], with_meta=False)
print(args)  # Namespace(config=[None], value='test')
assert args.value == "test"

# parses ' ' as None
args = parser.parse_args(["--config={'value': ' '}"], with_meta=False)
print(args)  # Namespace(config=[None], value=None)
assert args.value == " "  # AssertionError

This also made me notice that an empty space is passed differently for Optional[str] than str:

import jsonargparse

parser = jsonargparse.ArgumentParser()
parser.add_argument("--value", type=str, default=None)

# this works for str but doesn't for Optional[str]
args = parser.parse_args(["--value= "], with_meta=False)
print(args)  # Namespace(value=' ')
assert args.value == " "

# this works for Optional[str] but doesn't for str
args = parser.parse_args(["--value=' '"], with_meta=False)
print(args)  # Namespace(value="' '")
assert args.value == " "  # AssertionError

Required class arguments

This does work:

class Model:
    def __init__(self, a: int = 4, *, b: float = 0.3, c: str = 'kek'):
        super().__init__()

parser = jsonargparse.ArgumentParser()

model_parser = jsonargparse.ArgumentParser()
model_parser.add_class_arguments(Model)

action = jsonargparse.ActionParser(model_parser)
parser.add_argument('--model', action=action)

parser.parse_args(['--model.a=10'])

But this doesn't:

class Model:
    def __init__(self, a: int, *, b: float = 0.3, c: str = 'kek'):
        super().__init__()

parser = jsonargparse.ArgumentParser()

model_parser = jsonargparse.ArgumentParser()
model_parser.add_class_arguments(Model)

action = jsonargparse.ActionParser(model_parser)
parser.add_argument('--model', action=action)

>>> parser.parse_args(['--model.a=10'])
# error: Configuration check failed :: Key "a" is required but not included in config object or its value is None.

'jsonvalidator' is not defined

As you can see from path and stack trace - python3.9 and issue arises when invoking add_argument.

  File "/Users/kenny/semantic/semantic-convs/scae/args.py", line 56, in add_pcae_args
    pcae_args.add_argument(
  File "/Users/kenny/.virtualenvs/semantic-convs/lib/python3.9/site-packages/jsonargparse/core.py", line 86, in add_argument
    kwargs['action'] = ActionJsonSchema(annotation=kwargs.pop('type'), enable_path=enable_path)
  File "/Users/kenny/.virtualenvs/semantic-convs/lib/python3.9/site-packages/jsonargparse/jsonschema.py", line 94, in __init__
    jsonvalidator.check_schema(schema)
NameError: name 'jsonvalidator' is not defined

Can a sdist be provided on PyPi?

We would like to use this for a project at Facebook, but corporate policies require an sdist be made available. Would it be possible to publish an sdist to PyPi please?

Changing env variable format

I wonder if it's possible to change the [PREFIX_][LEV__]*OPT format, e.g. specify the environment variable on the argument explicitely? I prefer to do this explicitely for users who are not aware of the convention.

And maybe we can add something in the docs for when you don't want to use PREFIX or LEV

And can you overwrite the entire class with a dictionary?

Sorry for the messy issue with the many questions :D If you explain I can make a PR for the README ;)

CLI default config path

Can the CLI include an option CLI(config_path='config.yaml')

So config.yaml is loaded automatically?

Can I use Ellipses in types?

Parameterizations are often expressed as a tuple of keys followed by a sequence of tuples of values in order to express a sequence of mapping of the keys to the values, eg I might write (('foo', 'bar'), ((1, 0.3), (5, 0.9)) to mean ({'foo': 1, 'bar': 0.3}, {'foo': 5, 'bar': 0.9}). I'm trying to do this using jsonargparse, but I am fairly ignorant with python's typing module and argument parsing in general.

This appears to be a valid type: Tuple[Tuple[str, str], Tuple[Tuple[int, float],...]] , as the Ellipsis operate on a 'single' type Tuple[int, float] in the Tuple. However, when I try the code (below) with either Ellipsis (commented out), or repeating two Tuple[int, float] elements, I get the same error (ValueError: not enough values to unpack (expected 3, got 2)). If I only have one nested level it works ok, eg Tuple[Tuple[str, str], Tuple[int, float], Tuple[int, float]] which is often ok, but as I understand it I can't use Ellipsis on a Tuple with elements of more than one type, and this flattened parameterization has two types, the keys and the values.

I really appreciate jsonargparse's ability to map dataclasses to argparser -- that's the functionality I need. I might well be overlooking something regarding argparse or python's typing module, or some of the other dependencies.

from jsonargparse import ArgumentParser, namespace_to_dict
from typing import Dict, Union, List, Tuple
from dataclasses import field, asdict, dataclass

@dataclass
class Schedule:
    #sched: Tuple[Tuple[str, str], Tuple[Tuple[int, float],...]]
    sched: Tuple[Tuple[str, str], Tuple[Tuple[int, float], Tuple[int, float]]]

parser = ArgumentParser()
parser.add_class_arguments(Schedule)

cmds = ["--sched", "[['foo', 'bar'], [[1, 2.02], [3, 3.09]]]"]
cfg = parser.parse_args(cmds)
print(cmds)
print(cfg)

myschedule = Schedule(**namespace_to_dict(cfg))
print(myschedule)
['--sched', "[['foo', 'bar'], [[1, 2.02], [3, 3.09]]]"]

---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-112-b905a1e6539a> in <module>
     22 cmds = ["--sched", "[['foo', 'bar'], [[1, 2.02], [3, 3.09]]]"]
     23 print(cmds)
---> 24 cfg = parser.parse_args(cmds)
     25 print(cfg)
     26 

~/anaconda3/envs/py39/lib/python3.9/site-packages/jsonargparse/core.py in parse_args(self, args, namespace, env, defaults, nested, with_meta, _skip_check)
    319         try:
    320             with _suppress_stderr():
--> 321                 cfg, unk = self.parse_known_args(args=args)
    322                 if unk:
    323                     self.error('Unrecognized arguments: %s' % ' '.join(unk))

~/anaconda3/envs/py39/lib/python3.9/site-packages/jsonargparse/core.py in parse_known_args(self, args, namespace)
    201 
    202         try:
--> 203             namespace, args = self._parse_known_args(args, namespace)
    204             if len(args) > 0:
    205                 for action in self._actions:

~/anaconda3/envs/py39/lib/python3.9/argparse.py in _parse_known_args(self, arg_strings, namespace)
   2058 
   2059             # consume the next optional and any arguments for it
-> 2060             start_index = consume_optional(start_index)
   2061 
   2062         # consume any positionals following the last Optional

~/anaconda3/envs/py39/lib/python3.9/argparse.py in consume_optional(start_index)
   1998             assert action_tuples
   1999             for action, args, option_string in action_tuples:
-> 2000                 take_action(action, args, option_string)
   2001             return stop
   2002 

~/anaconda3/envs/py39/lib/python3.9/argparse.py in take_action(action, argument_strings, option_string)
   1926             # (e.g. from a default)
   1927             if argument_values is not SUPPRESS:
-> 1928                 action(self, namespace, argument_values, option_string)
   1929 
   1930         # function to convert arg_strings into an optional action

~/anaconda3/envs/py39/lib/python3.9/site-packages/jsonargparse/jsonschema.py in __call__(self, *args, **kwargs)
    126                 kwargs['help'] = kwargs['help'] % json.dumps(self._validator.schema, sort_keys=True)
    127             return ActionJsonSchema(**kwargs)
--> 128         val = self._check_type(args[2])
    129         if not self._with_meta:
    130             val = strip_meta(val)

~/anaconda3/envs/py39/lib/python3.9/site-packages/jsonargparse/jsonschema.py in _check_type(self, value, cfg)
    141                 if isinstance(val, Namespace):
    142                     val = namespace_to_dict(val)
--> 143                 val = self._adapt_types(val, self._annotation, self._subschemas, reverse=True)
    144                 path_meta = val.pop('__path__') if isinstance(val, dict) and '__path__' in val else None
    145                 self._validator.validate(val)

~/anaconda3/envs/py39/lib/python3.9/site-packages/jsonargparse/jsonschema.py in _adapt_types(val, annotation, subschemas, reverse, instantiate_classes)
    243                 if n < len(subschemas) and subschemas[n] is not None:
    244                     for subschema in subschemas[n]:
--> 245                         val[n] = validate_adapt(v, subschema)
    246             if not reverse:
    247                 val = tuple(val) if annotation.__origin__ in {Tuple, tuple} else set(val)

~/anaconda3/envs/py39/lib/python3.9/site-packages/jsonargparse/jsonschema.py in validate_adapt(v, subschema)
    184         def validate_adapt(v, subschema):
    185             if subschema is not None:
--> 186                 subannotation, subvalidator, subsubschemas = subschema
    187                 if reverse:
    188                     v = ActionJsonSchema._adapt_types(v, subannotation, subsubschemas, reverse, instantiate_classes)

ValueError: not enough values to unpack (expected 3, got 2)

ActionParser docs

Hey,

Sorry for raising many issues but I'm just trying to find out if I'll find my use-case here.

I think there is an inconsistency in the ActionParser's docs, please correct me if I'm wrong.

Sometimes it is useful to take an already existing parser that is required standalone in some part of the code, and reuse it to parse an inner node of another more complex parser. For these cases an argument can be defined using the ActionParser class.

and then

An important detail to note is that the parsers that are given to ActionParser are internally modified. So they should be instantiated exclusively for the ActionParser and not used standalone.

Should I make a copy then?

See here

Parsing paths with nested ActionParsers

Parsing paths works when ActionPath arguments are added directly to a parser. When they are added indirectly by an ActionParser, parse_paths produces a string instead of a Path object for these arguments.

from jsonargparse import ArgumentParser, ActionPath, ActionParser

sub_parser = ArgumentParser()
sub_parser.add_argument('--info', action=ActionPath(mode='fr', skip_check=True))

parser = ArgumentParser()

# Option 1: produces a Path object
# parser.add_argument('--databases.info', action=ActionPath(mode='fr', skip_check=True))
# Option 2: produces a string
parser.add_argument('--databases', action=ActionParser(parser=sub_parser))

cfg = parser.parse_path('config.json')

config.json:

{
  "databases": {
    "info": "data/info.db"
  }
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.