Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ Deprecated
- ``instantiate_classes`` is deprecated and will be removed in v5.0.0. Instead
use ``instantiate`` (`#896
<https://github.com/omni-us/jsonargparse/pull/896>`__).
- ``ArgumentParser.add_instantiator`` is deprecated and will be removed in
v5.0.0. Use the global function ``jsonargparse.add_instantiator`` instead
(`#899 <https://github.com/omni-us/jsonargparse/pull/899>`__).


v4.48.0 (2026-04-10)
Expand Down
3 changes: 3 additions & 0 deletions jsonargparse/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from ._deprecated import * # noqa: F403
from ._formatters import * # noqa: F403
from ._from_config import * # noqa: F403
from ._instantiation import * # noqa: F403
from ._jsonnet import * # noqa: F403
from ._jsonschema import * # noqa: F403
from ._link_arguments import * # noqa: F403
Expand Down Expand Up @@ -47,6 +48,7 @@
_deprecated,
_formatters,
_from_config,
_instantiation,
_jsonnet,
_jsonschema,
_link_arguments,
Expand All @@ -69,6 +71,7 @@
__all__ += _namespace.__all__
__all__ += _formatters.__all__
__all__ += _common.__all__
__all__ += _instantiation.__all__
__all__ += _loaders_dumpers.__all__
__all__ += _util.__all__
__all__ += _deprecated.__all__
Expand Down
28 changes: 0 additions & 28 deletions jsonargparse/_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -378,34 +378,6 @@ def subclass_type_behavior(
)


def default_class_instantiator(class_type: type[ClassType], *args, **kwargs) -> ClassType:
return class_type(*args, **kwargs)


class ClassInstantiator:
def __init__(self, instantiators: InstantiatorsDictType) -> None:
self.instantiators = instantiators

def __call__(self, class_type: type[ClassType], *args, **kwargs) -> ClassType:
for (cls, subclasses), instantiator in self.instantiators.items():
if class_type is cls or (subclasses and is_subclass(class_type, cls)):
param_names = set(inspect.signature(instantiator).parameters)
if "applied_instantiation_links" in param_names:
applied_links = applied_instantiation_links.get() or set()
kwargs["applied_instantiation_links"] = {
action.target[0]: action.applied_value for action in applied_links
}
return instantiator(class_type, *args, **kwargs)
return default_class_instantiator(class_type, *args, **kwargs)


def get_class_instantiator() -> InstantiatorCallable:
instantiators = class_instantiators.get()
if not instantiators:
return default_class_instantiator
return ClassInstantiator(instantiators)


# logging

logging_levels = {"CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"}
Expand Down
60 changes: 3 additions & 57 deletions jsonargparse/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,7 @@
previous_config,
)
from ._common import (
ClassType,
InstantiatorCallable,
InstantiatorsDictType,
LoggerProperty,
class_instantiators,
debug_mode_active,
get_optionals_as_positionals_actions,
is_subclasses_disabled,
Expand All @@ -45,6 +41,7 @@
)
from ._deprecated import ParserDeprecations, deprecated_skip_check, deprecated_yaml_comments
from ._formatters import DefaultHelpFormatter, get_env_var
from ._instantiation import get_class_instantiators
from ._jsonnet import ActionJsonnet
from ._jsonschema import ActionJsonSchema
from ._link_arguments import ActionLink, ArgumentLinking
Expand Down Expand Up @@ -237,7 +234,6 @@ class ArgumentParser(ParserDeprecations, ActionsContainer, ArgumentLinking, Logg
groups: Optional[dict[str, ArgumentGroup]] = None
_group_class: type[ArgumentGroup]
_subcommands_action: Optional[ActionSubCommands] = None
_instantiators: Optional[InstantiatorsDictType] = None

def __init__(
self,
Expand Down Expand Up @@ -1204,56 +1200,6 @@ def check_values(cfg):
if not skip_required and not lenient_check.get():
check_required(cfg, self, prefix)

def add_instantiator(
self,
instantiator: InstantiatorCallable,
class_type: type[ClassType],
subclasses: bool = True,
prepend: bool = False,
) -> None:
"""Adds a custom instantiator for a class type. Used by ``instantiate``.

Instantiator functions are expected to have as signature ``(class_type:
Type[ClassType], *args, **kwargs) -> ClassType``.

For reference, the default instantiator is ``return class_type(*args,
**kwargs)``.

In some use cases, the instantiator function might need access to values
applied by instantiation links. For this, the instantiator function can
have an additional keyword parameter ``applied_instantiation_links:
dict``. This parameter will be populated with a dictionary having as
keys the targets of the instantiation links and corresponding values
that were applied.

Args:
instantiator: Function that instantiates a class.
class_type: The class type to instantiate.
subclasses: Whether to instantiate subclasses of ``class_type``.
prepend: Whether to prepend the instantiator to the existing instantiators.
"""
if self._instantiators is None:
self._instantiators = {}
key = (class_type, subclasses)
instantiators = {k: v for k, v in self._instantiators.items() if k != key}
if prepend:
self._instantiators = {key: instantiator, **instantiators}
else:
instantiators[key] = instantiator
self._instantiators = instantiators

def _get_instantiators(self):
instantiators = self._instantiators or {}
if hasattr(self, "parent_parser"):
parent_instantiators = self.parent_parser._get_instantiators()
instantiators = instantiators.copy()
instantiators.update({k: v for k, v in parent_instantiators.items() if k not in instantiators})
context_instantiators = class_instantiators.get()
if context_instantiators:
instantiators = instantiators.copy()
instantiators.update({k: v for k, v in context_instantiators.items() if k not in instantiators})
return instantiators

def instantiate(
self,
cfg: Namespace,
Expand Down Expand Up @@ -1323,14 +1269,14 @@ def instantiate(
with parser_context(
parent_parser=self,
nested_links=ActionLink.get_nested_links(self, component),
class_instantiators=self._get_instantiators(),
class_instantiators=get_class_instantiators(self),
applied_instantiation_links=cfg.get("__applied_instantiation_links__"),
):
parent[key] = component.instantiate_classes(value)
else:
with parser_context(
load_value_mode=self.parser_mode,
class_instantiators=self._get_instantiators(),
class_instantiators=get_class_instantiators(self),
applied_instantiation_links=cfg.get("__applied_instantiation_links__"),
):
component.instantiate_class(component, cfg)
Expand Down
29 changes: 28 additions & 1 deletion jsonargparse/_deprecated.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@
from types import ModuleType
from typing import Any, Callable, Dict, Optional, Set, Union, overload

from ._common import Action, null_logger
from ._common import Action, InstantiatorsDictType, null_logger
from ._common import LoggerProperty as InternalLoggerProperty
from ._instantiation import _register_instantiator
from ._namespace import Namespace
from ._type_checking import ArgumentParser, ruamelCommentedMap

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

_instantiators: Optional[InstantiatorsDictType] = None

def __init__(self, *args, error_handler=False, default_meta=None, **kwargs):
super().__init__(*args, **kwargs)
self.error_handler = error_handler
Expand Down Expand Up @@ -654,6 +657,30 @@ def add_dataclass_arguments(self, *args, **kwargs):
def check_config(self, *args, **kwargs):
return self.validate(*args, **kwargs)

@deprecated("""
``ArgumentParser.add_instantiator`` was deprecated in v4.49.0 and will be
removed in v5.0.0. Use the global function ``jsonargparse.add_instantiator``
instead.
""")
def add_instantiator(
self,
instantiator,
class_type,
subclasses: bool = True,
prepend: bool = False,
) -> None:
if self._instantiators is None:
self._instantiators = {}
_register_instantiator(self._instantiators, instantiator, class_type, subclasses=subclasses, prepend=prepend)

def _get_parser_instantiators(self) -> InstantiatorsDictType:
instantiators = self._instantiators or {}
if hasattr(self, "parent_parser"):
parent_instantiators = self.parent_parser._get_parser_instantiators()
instantiators = instantiators.copy()
instantiators.update({k: v for k, v in parent_instantiators.items() if k not in instantiators})
return instantiators


def deprecated_skip_check(component, kwargs: dict, skip_validation: bool) -> bool:
skip_check = kwargs.pop("skip_check", None)
Expand Down
112 changes: 112 additions & 0 deletions jsonargparse/_instantiation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import inspect

from ._common import (
ClassType,
InstantiatorCallable,
InstantiatorsDictType,
applied_instantiation_links,
class_instantiators,
is_subclass,
)

__all__ = ["add_instantiator"]

_global_class_instantiators: InstantiatorsDictType = {}


def add_instantiator(
instantiator: InstantiatorCallable,
class_type: type[ClassType],
subclasses: bool = True,
prepend: bool = False,
) -> None:
"""Adds a custom instantiator for a class type. Used by ``ArgumentParser.instantiate``.

Instantiator functions are expected to have as signature ``(class_type:
Type[ClassType], *args, **kwargs) -> ClassType``.

For reference, the default instantiator is ``return class_type(*args,
**kwargs)``.

In some use cases, the instantiator function might need access to values
applied by instantiation links. For this, the instantiator function can
have an additional keyword parameter ``applied_instantiation_links:
dict``. This parameter will be populated with a dictionary having as
keys the targets of the instantiation links and corresponding values
that were applied.

Args:
instantiator: Function that instantiates a class.
class_type: The class type to instantiate.
subclasses: Whether to instantiate subclasses of ``class_type``.
prepend: Whether to prepend the instantiator to the existing instantiators.
"""
_register_instantiator(
_global_class_instantiators, instantiator, class_type, subclasses=subclasses, prepend=prepend
)


def _register_instantiator(
registry: InstantiatorsDictType,
instantiator: InstantiatorCallable,
class_type: type[ClassType],
subclasses: bool = True,
prepend: bool = False,
) -> None:
"""Registers an instantiator in the given registry dict (in-place)."""
key = (class_type, subclasses)
items = {k: v for k, v in registry.items() if k != key}
if prepend:
registry.clear()
registry.update({key: instantiator, **items})
else:
items[key] = instantiator
registry.clear()
registry.update(items)


def _get_global_class_instantiators() -> InstantiatorsDictType:
"""Returns the global instantiators registry."""
return _global_class_instantiators


def default_class_instantiator(class_type: type[ClassType], *args, **kwargs) -> ClassType:
return class_type(*args, **kwargs)


class ClassInstantiator:
def __init__(self, instantiators: InstantiatorsDictType) -> None:
self.instantiators = instantiators

def __call__(self, class_type: type[ClassType], *args, **kwargs) -> ClassType:
for (cls, subclasses), instantiator in self.instantiators.items():
if class_type is cls or (subclasses and is_subclass(class_type, cls)):
param_names = set(inspect.signature(instantiator).parameters)
if "applied_instantiation_links" in param_names:
applied_links = applied_instantiation_links.get() or set()
kwargs["applied_instantiation_links"] = {
action.target[0]: action.applied_value for action in applied_links
}
return instantiator(class_type, *args, **kwargs)
return default_class_instantiator(class_type, *args, **kwargs)


def get_class_instantiator() -> InstantiatorCallable:
instantiators = class_instantiators.get()
if not instantiators:
return default_class_instantiator
return ClassInstantiator(instantiators)


def get_class_instantiators(parser) -> InstantiatorsDictType:
"""Gathers all instantiators applicable to the given parser."""
instantiators = parser._get_parser_instantiators()
context_instantiators = class_instantiators.get()
if context_instantiators:
instantiators = instantiators.copy()
instantiators.update({k: v for k, v in context_instantiators.items() if k not in instantiators})
global_instantiators = _get_global_class_instantiators()
if global_instantiators:
instantiators = instantiators.copy()
instantiators.update({k: v for k, v in global_instantiators.items() if k not in instantiators})
return instantiators
2 changes: 1 addition & 1 deletion jsonargparse/_signatures.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@
from ._actions import _ActionConfigLoad
from ._common import (
LoggerProperty,
get_class_instantiator,
get_generic_origin,
get_unaliased_type,
is_final_class,
is_subclass,
is_subclasses_disabled,
)
from ._instantiation import get_class_instantiator
from ._namespace import Namespace
from ._optionals import attrs_support, get_doc_short_description, is_attrs_class, is_pydantic_model
from ._parameter_resolvers import ParamData, get_parameter_origins, get_signature_parameters
Expand Down
2 changes: 1 addition & 1 deletion jsonargparse/_typehints.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@
remove_actions,
)
from ._common import (
get_class_instantiator,
get_unaliased_type,
is_generic_class,
is_instance,
Expand All @@ -58,6 +57,7 @@
parser_context,
validating_defaults,
)
from ._instantiation import get_class_instantiator
from ._loaders_dumpers import (
basic_json_or_yaml_load,
get_loader_exceptions,
Expand Down
9 changes: 9 additions & 0 deletions jsonargparse_tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,15 @@ def subsubparser() -> ArgumentParser:
return ArgumentParser(exit_on_error=False)


@pytest.fixture
def clear_instantiators():
from jsonargparse._instantiation import _global_class_instantiators

_global_class_instantiators.clear()
yield
_global_class_instantiators.clear()


@pytest.fixture
def example_parser() -> ArgumentParser:
parser = ArgumentParser(prog="app", exit_on_error=False)
Expand Down
Loading
Loading