Skip to content

Commit

Permalink
Add RegistryField machinery for dynamic defaults.
Browse files Browse the repository at this point in the history
  • Loading branch information
TallJimbo committed Apr 18, 2023
1 parent 9c140cc commit e83267f
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 11 deletions.
1 change: 1 addition & 0 deletions doc/changes/DM-31924.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add a dynamic-default callback argument to `RegistryField`.
73 changes: 62 additions & 11 deletions python/lsst/pex/config/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ def __iter__(self):
def __contains__(self, key):
return key in self._dict

def makeField(self, doc, default=None, optional=False, multi=False):
def makeField(self, doc, default=None, optional=False, multi=False, on_none=None):
"""Create a `RegistryField` configuration field from this registry.
Parameters
Expand All @@ -194,13 +194,18 @@ def makeField(self, doc, default=None, optional=False, multi=False):
multi : `bool`, optional
A flag to allow multiple selections in the `RegistryField` if
`True`.
on_none: `Callable`, optional
A callable that should be invoked when ``apply`` is called but the
selected name or names is `None`. Will be passed the field
attribute proxy (`RegistryInstanceDict`) and then all positional
and keyword arguments passed to ``apply``.
Returns
-------
field : `lsst.pex.config.RegistryField`
`~lsst.pex.config.RegistryField` Configuration field.
"""
return RegistryField(doc, self, default, optional, multi)
return RegistryField(doc, self, default, optional, multi, on_none=on_none)


class RegistryAdaptor(collections.abc.Mapping):
Expand Down Expand Up @@ -262,24 +267,63 @@ def _getTargets(self):

targets = property(_getTargets)

def apply(self, *args, **kw):
"""Call the active target(s) with the active config as a keyword arg
def apply(self, *args, **kwargs):
"""Call the active target(s) with the active config as a keyword arg.
If this is a multi-selection field, return a list obtained by calling
each active target with its corresponding active config.
Parameters
----------
selection : `str` or `~collections.abc.Iterable` [ `str` ]
Name or names of targets, depending on whether ``multi=True``.
*args, **kwargs
Additional arguments will be passed on to the configurable
target(s).
Additional arguments will be passed on to the configurable target(s)
Returns
-------
result
If this is a single-selection field, the return value from calling
the target. If this is a multi-selection field, a list thereof.
"""
if self.active is None:
if self._field._on_none is not None:
return self._field._on_none(self, *args, **kwargs)
msg = "No selection has been made. Options: %s" % " ".join(self.types.registry.keys())
raise FieldValidationError(self._field, self._config, msg)
return self.apply_with(self._selection, *args, **kwargs)

def apply_with(self, selection, *args, **kwargs):
"""Call named target(s) with the corresponding config as a keyword
arg.
Parameters
----------
selection : `str` or `~collections.abc.Iterable` [ `str` ]
Name or names of targets, depending on whether ``multi=True``.
*args, **kwargs
Additional arguments will be passed on to the configurable
target(s).
Returns
-------
result
If this is a single-selection field, the return value from calling
the target. If this is a multi-selection field, a list thereof.
Notes
-----
This method ignores the current selection in the ``name`` or ``names``
attribute, which is usually not what you want. This method is most
useful in ``on_none`` callbacks provided at field construction, which
allow a context-dependent default to be used when no selection is
configured.
"""
if self._field.multi:
retvals = []
for c in self._selection:
retvals.append(self.types.registry[c](*args, config=self[c], **kw))
for c in selection:
retvals.append(self.types.registry[c](*args, config=self[c], **kwargs))

Check warning on line 323 in python/lsst/pex/config/registry.py

View check run for this annotation

Codecov / codecov/patch

python/lsst/pex/config/registry.py#L322-L323

Added lines #L322 - L323 were not covered by tests
return retvals
else:
return self.types.registry[self.name](*args, config=self[self.name], **kw)
return self.types.registry[selection](*args, config=self[selection], **kwargs)

def __setattr__(self, attr, value):
if attr == "registry":
Expand All @@ -305,6 +349,11 @@ class RegistryField(ConfigChoiceField):
multi : `bool`, optional
If `True`, the field allows multiple selections. The default is
`False`.
on_none: `Callable`, optional
A callable that should be invoked when ``apply`` is called but the
selected name or names is `None`. Will be passed the field attribute
proxy (`RegistryInstanceDict`) and then all positional and keyword
arguments passed to ``apply``.
See also
--------
Expand All @@ -323,9 +372,10 @@ class RegistryField(ConfigChoiceField):
"""Class used to hold configurable instances in the field.
"""

def __init__(self, doc, registry, default=None, optional=False, multi=False):
def __init__(self, doc, registry, default=None, optional=False, multi=False, on_none=None):
types = RegistryAdaptor(registry)
self.registry = registry
self._on_none = on_none
ConfigChoiceField.__init__(self, doc, types, default, optional, multi)

def __deepcopy__(self, memo):
Expand All @@ -340,6 +390,7 @@ def __deepcopy__(self, memo):
default=copy.deepcopy(self.default),
optional=self.optional,
multi=self.multi,
on_none=self._on_none,
)
other.source = self.source
return other
Expand Down
15 changes: 15 additions & 0 deletions tests/test_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,21 @@ def fail(name): # lambda doesn't like |=

self.assertRaises(pexConfig.FieldValidationError, fail, "bar")

def test_on_none(self):
"""Test the on_none callback argument to RegistryField."""

def on_none_callback(config_dict, *args, **kwargs):
return config_dict.apply_with("foo1", *args, **kwargs)

class C1(pexConfig.Config):
r = self.registry.makeField(
"registry field with callback default", default=None, optional=True, on_none=on_none_callback
)

c = C1()
self.assertIsNone(c.r.name)
self.assertIsInstance(c.r.apply(), self.fooAlg1Class)


if __name__ == "__main__":
unittest.main()

0 comments on commit e83267f

Please sign in to comment.