Skip to content

Commit 797f004

Browse files
authored
Support implicit class_path when given a dict or single parameter (#505)
1 parent 8c26223 commit 797f004

File tree

7 files changed

+59
-11
lines changed

7 files changed

+59
-11
lines changed

CHANGELOG.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ Added
1919
^^^^^
2020
- Support for ``TypedDict`` (`#457
2121
<https://github.com/omni-us/jsonargparse/issues/457>`__).
22+
- Directly providing a dict with parameters or a single parameter to a subclass
23+
or callable with class return now implicitly tries using the base class as
24+
``class_path`` if not abstract.
2225

2326
Fixed
2427
^^^^^

DOCUMENTATION.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2013,6 +2013,16 @@ been imported before parsing. Abstract classes and private classes (module or
20132013
name starting with ``'_'``) are not considered. All the subclasses resolvable by
20142014
its name can be seen in the general help ``python tool.py --help``.
20152015

2016+
When the base class is not abstract, the ``class_path`` can be omitted, by
2017+
giving directly ``init_args``, for example:
2018+
2019+
.. code-block:: bash
2020+
2021+
python tool.py --calendar.firstweekday 2
2022+
2023+
would implicitly use ``calendar.Calendar`` as the class path.
2024+
2025+
20162026
Default values
20172027
--------------
20182028

jsonargparse/_core.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,10 @@ def _parse_common(
313313
_ActionPrintConfig.print_config_if_requested(self, cfg)
314314

315315
with parser_context(parent_parser=self):
316-
ActionLink.apply_parsing_links(self, cfg)
316+
try:
317+
ActionLink.apply_parsing_links(self, cfg)
318+
except Exception as ex:
319+
self.error(str(ex), ex)
317320

318321
if not skip_check and not lenient_check.get():
319322
self.check_config(cfg, skip_required=skip_required)

jsonargparse/_typehints.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -865,6 +865,11 @@ def adapt_typehints(
865865
else:
866866
raise ImportError(f"Unexpected import object {val_obj}")
867867
if isinstance(val, (dict, Namespace, NestedArg)):
868+
if prev_val is None:
869+
return_type = get_callable_return_type(typehint)
870+
if return_type and not inspect.isabstract(return_type):
871+
with suppress(ValueError):
872+
prev_val = Namespace(class_path=get_import_path(return_type))
868873
val = subclass_spec_as_namespace(val, prev_val)
869874
if not is_subclass_spec(val):
870875
raise ImportError(
@@ -920,6 +925,9 @@ def adapt_typehints(
920925
return val
921926

922927
val_input = val
928+
if prev_val is None and not inspect.isabstract(typehint):
929+
with suppress(ValueError):
930+
prev_val = Namespace(class_path=get_import_path(typehint))
923931
val = subclass_spec_as_namespace(val, prev_val)
924932
if not is_subclass_spec(val):
925933
raise_unexpected_value(

jsonargparse_tests/test_link_arguments.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from calendar import Calendar, TextCalendar
44
from dataclasses import dataclass
5+
from importlib.util import find_spec
56
from typing import Any, Callable, List, Mapping, Optional, Union
67

78
import pytest
@@ -51,7 +52,7 @@ def to_str(value):
5152
subcommands.add_subcommand("sub", subparser)
5253

5354
with subtests.test("parse_args"):
54-
with pytest.raises(ValueError) as ctx:
55+
with pytest.raises(ArgumentError) as ctx:
5556
parser.parse_args(["sub"])
5657
ctx.match("Call to compute_fn of link 'to_str.*failed: value is empty")
5758

@@ -111,7 +112,10 @@ def test_on_parse_compute_fn_subclass_spec(parser, subtests):
111112
parser.set_defaults(cal1=None)
112113
with pytest.raises(ArgumentError) as ctx:
113114
parser.parse_args(["--cal1.firstweekday=-"])
114-
ctx.match('Parser key "cal1"')
115+
if find_spec("typeshed_client"):
116+
ctx.match('Parser key "cal1"')
117+
else:
118+
ctx.match("Call to compute_fn of link")
115119

116120

117121
class ClassA:

jsonargparse_tests/test_subclasses.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -246,13 +246,6 @@ def test_subclass_init_args_without_class_path(parser):
246246
assert cfg.cal3.init_args == Namespace(firstweekday=5)
247247

248248

249-
def test_subclass_init_args_without_class_path_error(parser):
250-
parser.add_subclass_arguments(Calendar, "cal1")
251-
with pytest.raises(ArgumentError) as ctx:
252-
parser.parse_args(["--cal1.init_args.firstweekday=4"])
253-
ctx.match("class path given previously")
254-
255-
256249
def test_subclass_init_args_without_class_path_dict(parser):
257250
parser.add_argument("--cfg", action=ActionConfigFile)
258251
parser.add_argument("--cal", type=Calendar)
@@ -1500,6 +1493,23 @@ def test_subclass_help_not_subclass(parser):
15001493
ctx.match("is not a subclass of")
15011494

15021495

1496+
class Implicit:
1497+
def __init__(self, a: int = 1, b: str = ""):
1498+
pass
1499+
1500+
1501+
def test_subclass_implicit_class_path(parser):
1502+
parser.add_argument("--implicit", type=Implicit)
1503+
cfg = parser.parse_args(['--implicit={"a": 2, "b": "x"}'])
1504+
assert cfg.implicit.class_path == f"{__name__}.Implicit"
1505+
assert cfg.implicit.init_args == Namespace(a=2, b="x")
1506+
cfg = parser.parse_args(["--implicit.a=3"])
1507+
assert cfg.implicit.init_args == Namespace(a=3, b="")
1508+
with pytest.raises(ArgumentError) as ctx:
1509+
parser.parse_args(['--implicit={"c": null}'])
1510+
ctx.match('No action for key "c" to check its value')
1511+
1512+
15031513
# error messages tests
15041514

15051515

jsonargparse_tests/test_typehints.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -754,6 +754,16 @@ def test_callable_args_return_type_class(parser, subtests):
754754
assert f"{__name__}.{name}" in help_str
755755

756756

757+
def test_callable_return_type_class_implicit_class_path(parser):
758+
parser.add_argument("--optimizer", type=Callable[[List[float]], Optimizer])
759+
cfg = parser.parse_args(['--optimizer={"lr": 0.5}'])
760+
assert cfg.optimizer.class_path == f"{__name__}.Optimizer"
761+
assert cfg.optimizer.init_args == Namespace(lr=0.5, momentum=0.0)
762+
cfg = parser.parse_args(["--optimizer.momentum=0.2"])
763+
assert cfg.optimizer.class_path == f"{__name__}.Optimizer"
764+
assert cfg.optimizer.init_args == Namespace(lr=0.001, momentum=0.2)
765+
766+
757767
def test_callable_multiple_args_return_type_class(parser, subtests):
758768
parser.add_argument("--optimizer", type=Callable[[List[float], float], Optimizer], default=SGD)
759769

@@ -924,7 +934,7 @@ def __init__(
924934
self.activation = activation
925935

926936

927-
def test_callable_zero_args_return_type_class(parser): # , subtests):
937+
def test_callable_zero_args_return_type_class(parser):
928938
parser.add_class_arguments(Model, "model")
929939
cfg = parser.parse_args([])
930940
assert cfg.model.activation == Namespace(

0 commit comments

Comments
 (0)