Skip to content

Commit d3f201a

Browse files
authored
Deprecate ArgumentParser.add_instantiator in favor of a global add_instantiator function (#899)
1 parent cd29e75 commit d3f201a

14 files changed

Lines changed: 206 additions & 113 deletions

CHANGELOG.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ Deprecated
2424
- ``instantiate_classes`` is deprecated and will be removed in v5.0.0. Instead
2525
use ``instantiate`` (`#896
2626
<https://github.com/omni-us/jsonargparse/pull/896>`__).
27+
- ``ArgumentParser.add_instantiator`` is deprecated and will be removed in
28+
v5.0.0. Use the global function ``jsonargparse.add_instantiator`` instead
29+
(`#899 <https://github.com/omni-us/jsonargparse/pull/899>`__).
2730

2831

2932
v4.48.0 (2026-04-10)

jsonargparse/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from ._deprecated import * # noqa: F403
1818
from ._formatters import * # noqa: F403
1919
from ._from_config import * # noqa: F403
20+
from ._instantiation import * # noqa: F403
2021
from ._jsonnet import * # noqa: F403
2122
from ._jsonschema import * # noqa: F403
2223
from ._link_arguments import * # noqa: F403
@@ -47,6 +48,7 @@
4748
_deprecated,
4849
_formatters,
4950
_from_config,
51+
_instantiation,
5052
_jsonnet,
5153
_jsonschema,
5254
_link_arguments,
@@ -69,6 +71,7 @@
6971
__all__ += _namespace.__all__
7072
__all__ += _formatters.__all__
7173
__all__ += _common.__all__
74+
__all__ += _instantiation.__all__
7275
__all__ += _loaders_dumpers.__all__
7376
__all__ += _util.__all__
7477
__all__ += _deprecated.__all__

jsonargparse/_common.py

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -378,34 +378,6 @@ def subclass_type_behavior(
378378
)
379379

380380

381-
def default_class_instantiator(class_type: type[ClassType], *args, **kwargs) -> ClassType:
382-
return class_type(*args, **kwargs)
383-
384-
385-
class ClassInstantiator:
386-
def __init__(self, instantiators: InstantiatorsDictType) -> None:
387-
self.instantiators = instantiators
388-
389-
def __call__(self, class_type: type[ClassType], *args, **kwargs) -> ClassType:
390-
for (cls, subclasses), instantiator in self.instantiators.items():
391-
if class_type is cls or (subclasses and is_subclass(class_type, cls)):
392-
param_names = set(inspect.signature(instantiator).parameters)
393-
if "applied_instantiation_links" in param_names:
394-
applied_links = applied_instantiation_links.get() or set()
395-
kwargs["applied_instantiation_links"] = {
396-
action.target[0]: action.applied_value for action in applied_links
397-
}
398-
return instantiator(class_type, *args, **kwargs)
399-
return default_class_instantiator(class_type, *args, **kwargs)
400-
401-
402-
def get_class_instantiator() -> InstantiatorCallable:
403-
instantiators = class_instantiators.get()
404-
if not instantiators:
405-
return default_class_instantiator
406-
return ClassInstantiator(instantiators)
407-
408-
409381
# logging
410382

411383
logging_levels = {"CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"}

jsonargparse/_core.py

Lines changed: 3 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,7 @@
2626
previous_config,
2727
)
2828
from ._common import (
29-
ClassType,
30-
InstantiatorCallable,
31-
InstantiatorsDictType,
3229
LoggerProperty,
33-
class_instantiators,
3430
debug_mode_active,
3531
get_optionals_as_positionals_actions,
3632
is_subclasses_disabled,
@@ -45,6 +41,7 @@
4541
)
4642
from ._deprecated import ParserDeprecations, deprecated_skip_check, deprecated_yaml_comments
4743
from ._formatters import DefaultHelpFormatter, get_env_var
44+
from ._instantiation import get_class_instantiators
4845
from ._jsonnet import ActionJsonnet
4946
from ._jsonschema import ActionJsonSchema
5047
from ._link_arguments import ActionLink, ArgumentLinking
@@ -237,7 +234,6 @@ class ArgumentParser(ParserDeprecations, ActionsContainer, ArgumentLinking, Logg
237234
groups: Optional[dict[str, ArgumentGroup]] = None
238235
_group_class: type[ArgumentGroup]
239236
_subcommands_action: Optional[ActionSubCommands] = None
240-
_instantiators: Optional[InstantiatorsDictType] = None
241237

242238
def __init__(
243239
self,
@@ -1204,56 +1200,6 @@ def check_values(cfg):
12041200
if not skip_required and not lenient_check.get():
12051201
check_required(cfg, self, prefix)
12061202

1207-
def add_instantiator(
1208-
self,
1209-
instantiator: InstantiatorCallable,
1210-
class_type: type[ClassType],
1211-
subclasses: bool = True,
1212-
prepend: bool = False,
1213-
) -> None:
1214-
"""Adds a custom instantiator for a class type. Used by ``instantiate``.
1215-
1216-
Instantiator functions are expected to have as signature ``(class_type:
1217-
Type[ClassType], *args, **kwargs) -> ClassType``.
1218-
1219-
For reference, the default instantiator is ``return class_type(*args,
1220-
**kwargs)``.
1221-
1222-
In some use cases, the instantiator function might need access to values
1223-
applied by instantiation links. For this, the instantiator function can
1224-
have an additional keyword parameter ``applied_instantiation_links:
1225-
dict``. This parameter will be populated with a dictionary having as
1226-
keys the targets of the instantiation links and corresponding values
1227-
that were applied.
1228-
1229-
Args:
1230-
instantiator: Function that instantiates a class.
1231-
class_type: The class type to instantiate.
1232-
subclasses: Whether to instantiate subclasses of ``class_type``.
1233-
prepend: Whether to prepend the instantiator to the existing instantiators.
1234-
"""
1235-
if self._instantiators is None:
1236-
self._instantiators = {}
1237-
key = (class_type, subclasses)
1238-
instantiators = {k: v for k, v in self._instantiators.items() if k != key}
1239-
if prepend:
1240-
self._instantiators = {key: instantiator, **instantiators}
1241-
else:
1242-
instantiators[key] = instantiator
1243-
self._instantiators = instantiators
1244-
1245-
def _get_instantiators(self):
1246-
instantiators = self._instantiators or {}
1247-
if hasattr(self, "parent_parser"):
1248-
parent_instantiators = self.parent_parser._get_instantiators()
1249-
instantiators = instantiators.copy()
1250-
instantiators.update({k: v for k, v in parent_instantiators.items() if k not in instantiators})
1251-
context_instantiators = class_instantiators.get()
1252-
if context_instantiators:
1253-
instantiators = instantiators.copy()
1254-
instantiators.update({k: v for k, v in context_instantiators.items() if k not in instantiators})
1255-
return instantiators
1256-
12571203
def instantiate(
12581204
self,
12591205
cfg: Namespace,
@@ -1323,14 +1269,14 @@ def instantiate(
13231269
with parser_context(
13241270
parent_parser=self,
13251271
nested_links=ActionLink.get_nested_links(self, component),
1326-
class_instantiators=self._get_instantiators(),
1272+
class_instantiators=get_class_instantiators(self),
13271273
applied_instantiation_links=cfg.get("__applied_instantiation_links__"),
13281274
):
13291275
parent[key] = component.instantiate_classes(value)
13301276
else:
13311277
with parser_context(
13321278
load_value_mode=self.parser_mode,
1333-
class_instantiators=self._get_instantiators(),
1279+
class_instantiators=get_class_instantiators(self),
13341280
applied_instantiation_links=cfg.get("__applied_instantiation_links__"),
13351281
):
13361282
component.instantiate_class(component, cfg)

jsonargparse/_deprecated.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@
1111
from types import ModuleType
1212
from typing import Any, Callable, Dict, Optional, Set, Union, overload
1313

14-
from ._common import Action, null_logger
14+
from ._common import Action, InstantiatorsDictType, null_logger
1515
from ._common import LoggerProperty as InternalLoggerProperty
16+
from ._instantiation import _register_instantiator
1617
from ._namespace import Namespace
1718
from ._type_checking import ArgumentParser, ruamelCommentedMap
1819

@@ -566,6 +567,8 @@ def deprecation_warning_error_handler(stacklevel):
566567
class ParserDeprecations:
567568
"""Helper class for ArgumentParser deprecations. Will be removed in v5.0.0."""
568569

570+
_instantiators: Optional[InstantiatorsDictType] = None
571+
569572
def __init__(self, *args, error_handler=False, default_meta=None, **kwargs):
570573
super().__init__(*args, **kwargs)
571574
self.error_handler = error_handler
@@ -654,6 +657,30 @@ def add_dataclass_arguments(self, *args, **kwargs):
654657
def check_config(self, *args, **kwargs):
655658
return self.validate(*args, **kwargs)
656659

660+
@deprecated("""
661+
``ArgumentParser.add_instantiator`` was deprecated in v4.49.0 and will be
662+
removed in v5.0.0. Use the global function ``jsonargparse.add_instantiator``
663+
instead.
664+
""")
665+
def add_instantiator(
666+
self,
667+
instantiator,
668+
class_type,
669+
subclasses: bool = True,
670+
prepend: bool = False,
671+
) -> None:
672+
if self._instantiators is None:
673+
self._instantiators = {}
674+
_register_instantiator(self._instantiators, instantiator, class_type, subclasses=subclasses, prepend=prepend)
675+
676+
def _get_parser_instantiators(self) -> InstantiatorsDictType:
677+
instantiators = self._instantiators or {}
678+
if hasattr(self, "parent_parser"):
679+
parent_instantiators = self.parent_parser._get_parser_instantiators()
680+
instantiators = instantiators.copy()
681+
instantiators.update({k: v for k, v in parent_instantiators.items() if k not in instantiators})
682+
return instantiators
683+
657684

658685
def deprecated_skip_check(component, kwargs: dict, skip_validation: bool) -> bool:
659686
skip_check = kwargs.pop("skip_check", None)

jsonargparse/_instantiation.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import inspect
2+
3+
from ._common import (
4+
ClassType,
5+
InstantiatorCallable,
6+
InstantiatorsDictType,
7+
applied_instantiation_links,
8+
class_instantiators,
9+
is_subclass,
10+
)
11+
12+
__all__ = ["add_instantiator"]
13+
14+
_global_class_instantiators: InstantiatorsDictType = {}
15+
16+
17+
def add_instantiator(
18+
instantiator: InstantiatorCallable,
19+
class_type: type[ClassType],
20+
subclasses: bool = True,
21+
prepend: bool = False,
22+
) -> None:
23+
"""Adds a custom instantiator for a class type. Used by ``ArgumentParser.instantiate``.
24+
25+
Instantiator functions are expected to have as signature ``(class_type:
26+
Type[ClassType], *args, **kwargs) -> ClassType``.
27+
28+
For reference, the default instantiator is ``return class_type(*args,
29+
**kwargs)``.
30+
31+
In some use cases, the instantiator function might need access to values
32+
applied by instantiation links. For this, the instantiator function can
33+
have an additional keyword parameter ``applied_instantiation_links:
34+
dict``. This parameter will be populated with a dictionary having as
35+
keys the targets of the instantiation links and corresponding values
36+
that were applied.
37+
38+
Args:
39+
instantiator: Function that instantiates a class.
40+
class_type: The class type to instantiate.
41+
subclasses: Whether to instantiate subclasses of ``class_type``.
42+
prepend: Whether to prepend the instantiator to the existing instantiators.
43+
"""
44+
_register_instantiator(
45+
_global_class_instantiators, instantiator, class_type, subclasses=subclasses, prepend=prepend
46+
)
47+
48+
49+
def _register_instantiator(
50+
registry: InstantiatorsDictType,
51+
instantiator: InstantiatorCallable,
52+
class_type: type[ClassType],
53+
subclasses: bool = True,
54+
prepend: bool = False,
55+
) -> None:
56+
"""Registers an instantiator in the given registry dict (in-place)."""
57+
key = (class_type, subclasses)
58+
items = {k: v for k, v in registry.items() if k != key}
59+
if prepend:
60+
registry.clear()
61+
registry.update({key: instantiator, **items})
62+
else:
63+
items[key] = instantiator
64+
registry.clear()
65+
registry.update(items)
66+
67+
68+
def _get_global_class_instantiators() -> InstantiatorsDictType:
69+
"""Returns the global instantiators registry."""
70+
return _global_class_instantiators
71+
72+
73+
def default_class_instantiator(class_type: type[ClassType], *args, **kwargs) -> ClassType:
74+
return class_type(*args, **kwargs)
75+
76+
77+
class ClassInstantiator:
78+
def __init__(self, instantiators: InstantiatorsDictType) -> None:
79+
self.instantiators = instantiators
80+
81+
def __call__(self, class_type: type[ClassType], *args, **kwargs) -> ClassType:
82+
for (cls, subclasses), instantiator in self.instantiators.items():
83+
if class_type is cls or (subclasses and is_subclass(class_type, cls)):
84+
param_names = set(inspect.signature(instantiator).parameters)
85+
if "applied_instantiation_links" in param_names:
86+
applied_links = applied_instantiation_links.get() or set()
87+
kwargs["applied_instantiation_links"] = {
88+
action.target[0]: action.applied_value for action in applied_links
89+
}
90+
return instantiator(class_type, *args, **kwargs)
91+
return default_class_instantiator(class_type, *args, **kwargs)
92+
93+
94+
def get_class_instantiator() -> InstantiatorCallable:
95+
instantiators = class_instantiators.get()
96+
if not instantiators:
97+
return default_class_instantiator
98+
return ClassInstantiator(instantiators)
99+
100+
101+
def get_class_instantiators(parser) -> InstantiatorsDictType:
102+
"""Gathers all instantiators applicable to the given parser."""
103+
instantiators = parser._get_parser_instantiators()
104+
context_instantiators = class_instantiators.get()
105+
if context_instantiators:
106+
instantiators = instantiators.copy()
107+
instantiators.update({k: v for k, v in context_instantiators.items() if k not in instantiators})
108+
global_instantiators = _get_global_class_instantiators()
109+
if global_instantiators:
110+
instantiators = instantiators.copy()
111+
instantiators.update({k: v for k, v in global_instantiators.items() if k not in instantiators})
112+
return instantiators

jsonargparse/_signatures.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@
99
from ._actions import _ActionConfigLoad
1010
from ._common import (
1111
LoggerProperty,
12-
get_class_instantiator,
1312
get_generic_origin,
1413
get_unaliased_type,
1514
is_final_class,
1615
is_subclass,
1716
is_subclasses_disabled,
1817
)
18+
from ._instantiation import get_class_instantiator
1919
from ._namespace import Namespace
2020
from ._optionals import attrs_support, get_doc_short_description, is_attrs_class, is_pydantic_model
2121
from ._parameter_resolvers import ParamData, get_parameter_origins, get_signature_parameters

jsonargparse/_typehints.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@
4646
remove_actions,
4747
)
4848
from ._common import (
49-
get_class_instantiator,
5049
get_unaliased_type,
5150
is_generic_class,
5251
is_instance,
@@ -58,6 +57,7 @@
5857
parser_context,
5958
validating_defaults,
6059
)
60+
from ._instantiation import get_class_instantiator
6161
from ._loaders_dumpers import (
6262
basic_json_or_yaml_load,
6363
get_loader_exceptions,

jsonargparse_tests/conftest.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,15 @@ def subsubparser() -> ArgumentParser:
140140
return ArgumentParser(exit_on_error=False)
141141

142142

143+
@pytest.fixture
144+
def clear_instantiators():
145+
from jsonargparse._instantiation import _global_class_instantiators
146+
147+
_global_class_instantiators.clear()
148+
yield
149+
_global_class_instantiators.clear()
150+
151+
143152
@pytest.fixture
144153
def example_parser() -> ArgumentParser:
145154
parser = ArgumentParser(prog="app", exit_on_error=False)

0 commit comments

Comments
 (0)