@@ -1035,96 +1035,9 @@ A second option is a class that once instantiated becomes callable:
10351035 >>> init.callable(5 )
10361036 8
10371037
1038- The third option is only applicable when the type is a callable that has a class
1039- as return type or a ``Union `` including a class. This is useful to support
1040- dependency injection for classes that require a parameter that is only available
1041- after injection. The parser supports this automatically by providing a function
1042- that receives this parameter and returns the instance of the class. Take for
1043- example the classes:
1044-
1045- .. testcode :: callable
1046-
1047- class Optimizer:
1048- def __init__(self, params: Iterable):
1049- self.params = params
1050-
1051-
1052- class SGD(Optimizer):
1053- def __init__(self, params: Iterable, lr: float):
1054- super().__init__(params)
1055- self.lr = lr
1056-
1057- .. testcode :: callable
1058- :hide:
1059-
1060- doctest_mock_class_in_main(SGD)
1061-
1062- A possible parser and callable behavior would be:
1063-
1064- .. doctest :: callable
1065-
1066- >>> value = {
1067- ... " class_path" : " SGD" ,
1068- ... " init_args" : {
1069- ... " lr" : 0.01 ,
1070- ... },
1071- ... }
1072-
1073- >>> parser.add_argument(" --optimizer" , type = Callable[[Iterable], Optimizer]) # doctest: +IGNORE_RESULT
1074- >>> cfg = parser.parse_args([" --optimizer" , str (value)])
1075- >>> cfg.optimizer
1076- Namespace(class_path='__main__.SGD', init_args=Namespace(lr=0.01))
1077- >>> init = parser.instantiate_classes(cfg)
1078- >>> optimizer = init.optimizer([1 , 2 , 3 ])
1079- >>> isinstance (optimizer, SGD )
1080- True
1081- >>> optimizer.params, optimizer.lr
1082- ([1, 2, 3], 0.01)
1083-
1084- Multiple arguments available after injection are also supported and can be
1085- specified the same way with a ``Callable `` type hint. For example, for two
1086- ``Iterable `` arguments, you can use the following syntax: ``Callable[[Iterable,
1087- Iterable], Type] ``. Please be aware that the arguments are passed as positional
1088- arguments, this means that the injected function would be called like
1089- ``function(value1, value2) ``. Similarly, for a callable that accepts zero
1090- arguments, the syntax would be ``Callable[[], Type] ``.
1091-
1092- .. note ::
1093-
1094- When the ``Callable `` has a class return type, it is possible to specify the
1095- ``class_path `` giving only its name if imported before parsing, as explained
1096- in :ref: `sub-classes-command-line `.
1097-
1098- If the same type above is used as type hint of a parameter of another class, a
1099- default can be set using a lambda, for example:
1100-
1101- .. testcode :: callable
1102-
1103- class Model:
1104- def __init__(
1105- self,
1106- optimizer: Callable[[Iterable], Optimizer] = lambda p: SGD(p, lr=0.05),
1107- ):
1108- self.optimizer = optimizer
1109-
1110- Then a parser and behavior could be:
1111-
1112- .. code-block ::
1113-
1114- >>> parser.add_class_arguments(Model, 'model')
1115- >>> cfg = parser.get_defaults()
1116- >>> cfg.model.optimizer
1117- Namespace(class_path='__main__.SGD', init_args=Namespace(lr=0.05))
1118- >>> init = parser.instantiate_classes(cfg)
1119- >>> optimizer = init.model.optimizer([1, 2, 3])
1120- >>> optimizer.params, optimizer.lr
1121- ([1, 2, 3], 0.05)
1122-
1123- See :ref: `ast-resolver ` for limitations of lambda defaults in signatures.
1124- Providing a lambda default to :py:meth: `.ActionsContainer.add_argument ` does not
1125- work since there is no AST resolving. In this case, a dict with ``class_path ``
1126- and ``init_args `` can be used as default.
1127-
1038+ The third option is only applicable when the type is a callable that returns
1039+ class instances. This is a form of :ref: `dependency-injection `, so this third
1040+ case is explained in section :ref: `instance-factories `.
11281041
11291042.. _registering-types :
11301043
@@ -1967,28 +1880,55 @@ the stubs. In these cases in the parser help the default is shown as
19671880``Unknown<stubs-resolver> `` and not included in
19681881:py:meth: `.ArgumentParser.get_defaults ` or the output of ``--print_config ``.
19691882
1883+
1884+ .. _dependency-injection :
1885+
1886+ Dependency injection
1887+ ====================
1888+
1889+ Dependency injection is a software design pattern that separates the
1890+ instantiation details of objects from their usage, resulting in more loosely
1891+ coupled programs, see the `wikipedia article
1892+ <https://en.wikipedia.org/wiki/Dependency_injection> `__. Because of its
1893+ benefits, support for dependency injection has been a design goal of
1894+ jsonargparse.
1895+
1896+ In python, dependency injection is achieved by:
1897+
1898+ - Using as type hint a class, such that the parameter accepts an instance of
1899+ this class or any subclass, e.g. ``module: ModuleBaseClass ``.
1900+ - Using as type hint a callable that returns an instance of a class, such that
1901+ the parameter accepts a function for instantiation. This could be either
1902+ using ``Callable ``, e.g. ``module: Callable[[int], ModuleBaseClass] ``, or a
1903+ protocol, e.g. ``module: ModuleFactoryProtocol ``.
1904+
19701905.. _sub-classes :
19711906
19721907Class type and sub-classes
1973- ==========================
1974-
1975- It is possible to use an arbitrary class as a type such that the argument
1976- accepts an instance of this class or any derived subclass. This practice is
1977- known as `dependency injection
1978- <https://en.wikipedia.org/wiki/Dependency_injection> `__. In the config file a
1979- class is represented by a dictionary with a ``class_path `` entry indicating the
1980- dot notation expression to import the class, and optionally some ``init_args ``
1981- that would be used to instantiate it. When parsing, it will be checked that the
1982- class can be imported, that it is a subclass of the given type and that
1983- ``init_args `` values correspond to valid arguments to instantiate it. After
1984- parsing, the config object will include the ``class_path `` and ``init_args ``
1985- entries. To get a config object with all sub-classes instantiated, the
1986- :py:meth: `.ArgumentParser.instantiate_classes ` method is used. The ``skip ``
1987- parameter of the signature methods can also be used to exclude arguments within
1988- subclasses. This is done by giving its relative destination key, i.e. as
1989- ``param.init_args.subparam ``.
1990-
1991- A simple example would be having some config file ``config.yaml `` as:
1908+ --------------------------
1909+
1910+ When a class is used as a type hint, jsonargparse expects in config files a
1911+ dictionary with a ``class_path `` entry indicating the dot notation expression to
1912+ import the class, and optionally some ``init_args `` that would be used to
1913+ instantiate it. When parsing, it will be checked that the class can be imported,
1914+ that it is a subclass of the given type and that ``init_args `` values correspond
1915+ to valid arguments to instantiate it. After parsing, the config object will
1916+ include the ``class_path `` and ``init_args `` entries. To get a config object
1917+ with all nested sub-classes instantiated, the
1918+ :py:meth: `.ArgumentParser.instantiate_classes ` method is used.
1919+
1920+ Additional to using a class as type hint in signatures, for low level
1921+ construction of parsers, there are also the methods
1922+ :py:meth: `.SignatureArguments.add_class_arguments ` and
1923+ :py:meth: `.SignatureArguments.add_subclass_arguments `. These methods accept a
1924+ ``skip `` argument that can be used to exclude parameters within subclasses. This
1925+ is done by giving its relative destination key, i.e. as
1926+ ``param.init_args.subparam ``. An individual argument can also be added having as
1927+ type a class, i.e. ``parser.add_argument("--module", type=ModuleBase) ``.
1928+
1929+ A simple example with a top-level class to instantiate, with a parameter that
1930+ expects an injected class instance, would be having some config file
1931+ ``config.yaml `` as:
19921932
19931933.. code-block :: yaml
19941934
@@ -2030,20 +1970,31 @@ Then in python:
20301970 {'class_path': 'calendar.Calendar', 'init_args': {'firstweekday': 1}}
20311971
20321972 >>> cfg = parser.instantiate_classes(cfg)
1973+ >>> isinstance (cfg.myclass, MyClass)
1974+ True
1975+ >>> isinstance (cfg.myclass.calendar, Calendar)
1976+ True
20331977 >>> cfg.myclass.calendar.getfirstweekday()
20341978 1
20351979
20361980In this example the ``class_path `` points to the same class used for the type.
20371981But a subclass of ``Calendar `` with an extended set of init parameters would
20381982also work.
20391983
2040- An individual argument can also be added having as type a class, i.e.
2041- ``parser.add_argument('--calendar', type=Calendar) ``. There is also another
2042- method :py:meth: `.SignatureArguments.add_subclass_arguments ` which does the same
2043- as ``add_argument ``, but has some added benefits: 1) the argument is added in a
2044- new group automatically; 2) the argument values can be given in an independent
2045- config file by specifying a path to it; and 3) by default sets a useful
2046- ``metavar `` and ``help `` strings.
1984+ If the previous example were changed to use
1985+ :py:meth: `.SignatureArguments.add_subclass_arguments ` instead of
1986+ :py:meth: `.SignatureArguments.add_class_arguments `, then subclasses ``MyClass ``
1987+ would also be accepted. In this case the config would be like:
1988+
1989+ .. code-block :: yaml
1990+
1991+ myclass :
1992+ class_path : my_module.MyClass
1993+ init_args :
1994+ calendar :
1995+ class_path : calendar.TextCalendar
1996+ init_args :
1997+ firstweekday : 1
20471998
20481999 .. note ::
20492000
@@ -2057,14 +2008,149 @@ config file by specifying a path to it; and 3) by default sets a useful
20572008 type a class. The accepted ``init_args `` would be the parameters of that
20582009 function.
20592010
2011+ .. _instance-factories :
2012+
2013+ Instance factories
2014+ ------------------
2015+
2016+ As explained at the beginning of section :ref: `dependency-injection `, callables
2017+ that return instances of classes, referred to as instance factories, represent
2018+ an alternative approach to dependency injection. This is useful to support
2019+ dependency injection of classes that require parameters that are only available
2020+ after injection. For this case, when
2021+ :py:meth: `.ArgumentParser.instantiate_classes ` is run, a partial function is
2022+ provided, which might accept parameters and returns the instance of the class.
2023+ Two options are possible, either using ``Callable `` or ``Protocol ``. First to
2024+ illustrate the ``Callable `` option, take for example the classes:
2025+
2026+ .. testcode :: callable
2027+
2028+ class Optimizer:
2029+ def __init__(self, params: Iterable):
2030+ self.params = params
2031+
2032+
2033+ class SGD(Optimizer):
2034+ def __init__(self, params: Iterable, lr: float):
2035+ super().__init__(params)
2036+ self.lr = lr
2037+
2038+ .. testcode :: callable
2039+ :hide:
2040+
2041+ doctest_mock_class_in_main(SGD)
2042+
2043+ A possible parser and callable behavior would be:
2044+
2045+ .. doctest :: callable
2046+
2047+ >>> value = {
2048+ ... " class_path" : " SGD" ,
2049+ ... " init_args" : {
2050+ ... " lr" : 0.01 ,
2051+ ... },
2052+ ... }
2053+
2054+ >>> parser.add_argument(" --optimizer" , type = Callable[[Iterable], Optimizer]) # doctest: +IGNORE_RESULT
2055+ >>> cfg = parser.parse_args([" --optimizer" , str (value)])
2056+ >>> cfg.optimizer
2057+ Namespace(class_path='__main__.SGD', init_args=Namespace(lr=0.01))
2058+ >>> init = parser.instantiate_classes(cfg)
2059+ >>> optimizer = init.optimizer([1 , 2 , 3 ])
2060+ >>> isinstance (optimizer, SGD )
2061+ True
2062+ >>> optimizer.params, optimizer.lr
2063+ ([1, 2, 3], 0.01)
2064+
2065+ .. note ::
2066+
2067+ When the ``Callable `` has a class return type, it is possible to specify the
2068+ ``class_path `` giving only its name if imported before parsing, as explained
2069+ in :ref: `sub-classes-command-line `.
2070+
2071+ If the same type above is used as type hint of a parameter of another class, a
2072+ default can be set using a lambda, for example:
2073+
2074+ .. testcode :: callable
2075+
2076+ class Model:
2077+ def __init__(
2078+ self,
2079+ optimizer: Callable[[Iterable], Optimizer] = lambda p: SGD(p, lr=0.05),
2080+ ):
2081+ self.optimizer = optimizer
2082+
2083+ Then a parser and behavior could be:
2084+
2085+ .. code-block ::
2086+
2087+ >>> parser.add_class_arguments(Model, 'model')
2088+ >>> cfg = parser.get_defaults()
2089+ >>> cfg.model.optimizer
2090+ Namespace(class_path='__main__.SGD', init_args=Namespace(lr=0.05))
2091+ >>> init = parser.instantiate_classes(cfg)
2092+ >>> optimizer = init.model.optimizer([1, 2, 3])
2093+ >>> optimizer.params, optimizer.lr
2094+ ([1, 2, 3], 0.05)
2095+
2096+ See :ref: `ast-resolver ` for limitations of lambda defaults in signatures.
2097+ Providing a lambda default to :py:meth: `.ActionsContainer.add_argument ` does not
2098+ work since there is no AST resolving. In this case, a dict with ``class_path ``
2099+ and ``init_args `` can be used as default.
2100+
2101+ Multiple arguments required after injection is also supported and can be
2102+ specified the same way with a ``Callable ``. For example, for two
2103+ ``Iterable `` arguments, you can use the syntax: ``Callable[[Iterable,
2104+ Iterable], Type] ``. Similarly, for a callable that accepts zero
2105+ arguments, the syntax would be ``Callable[[], Type] ``.
2106+
2107+ Note the big limitation that ``Callable `` has. It is only possible to specify
2108+ positional and unnamed parameters. To overcome this limitation, the second
2109+ option, a callable ``Protocol `` can be used instead. Building up from the same
2110+ example, an ``OptimizerFactory `` protocol can be defined as:
2111+
2112+ .. testcode :: callable
2113+
2114+ class OptimizerFactory(Protocol):
2115+ def __call__(self, params: Iterable) -> Optimizer: ...
2116+
2117+ Then a parser and protocol behavior would be:
2118+
2119+ .. testcode :: callable
2120+ :hide:
2121+
2122+ parser = ArgumentParser()
2123+
2124+ .. doctest :: callable
2125+
2126+ >>> value = {
2127+ ... " class_path" : " SGD" ,
2128+ ... " init_args" : {
2129+ ... " lr" : 0.02 ,
2130+ ... },
2131+ ... }
2132+
2133+ >>> parser.add_argument(" --optimizer" , type = OptimizerFactory) # doctest: +IGNORE_RESULT
2134+ >>> cfg = parser.parse_args([" --optimizer" , str (value)])
2135+ >>> cfg.optimizer
2136+ Namespace(class_path='__main__.SGD', init_args=Namespace(lr=0.02))
2137+ >>> init = parser.instantiate_classes(cfg)
2138+ >>> optimizer = init.optimizer(params = [6 , 5 ])
2139+ >>> optimizer.params, optimizer.lr
2140+ ([6, 5], 0.02)
2141+
2142+ The key difference with respect to the ``Callable `` is being able to call
2143+ ``init.optimizer() `` with keyword arguments ``params=[6, 5] ``.
2144+
20602145.. _sub-classes-command-line :
20612146
20622147Command line
20632148------------
20642149
2065- The help of the parser does not show details for a type class since this depends
2066- on the subclass. To get details for a particular subclass there is a specific
2067- help option that receives the import path. Take for example a parser defined as:
2150+ The help of the parser does not show accepted parameters of a class since this
2151+ depends on the chosen subclass. To get details for a particular subclass there
2152+ is a help option that receives the import path. Take for example a parser
2153+ defined as:
20682154
20692155.. testcode ::
20702156
@@ -2163,6 +2249,12 @@ example above, this would be:
21632249Like this, the parsed default will be a dict with ``class_path `` and
21642250``init_args ``, again avoiding the risk of mutability.
21652251
2252+ The use of :func: `.lazy_instance ` is somewhat discouraged. A function that
2253+ delays the initialization of instances, and works for all possible cases out
2254+ there, is challenging. The current implementation is known to have some
2255+ problems. Instead of using :func: `.lazy_instance `, you could consider switching
2256+ to :ref: `instance-factories `.
2257+
21662258.. note ::
21672259
21682260 In python there can be some classes or functions for which it is not
0 commit comments