Skip to content

Commit

Permalink
Support typing of ConfigurableAction(fields)
Browse files Browse the repository at this point in the history
Support supplying Field types through python class typing syntax.
Another minor fix includes setting an identity attribute on an
action if it is accessed via a Field.
  • Loading branch information
natelust committed Jun 23, 2022
1 parent c397daa commit db10279
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 27 deletions.
19 changes: 17 additions & 2 deletions python/lsst/pipe/tasks/configurableActions/_configurableAction.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,16 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations

__all__ = ["ConfigurableAction"]
__all__ = ["ConfigurableAction", "ActionTypeVar"]

from typing import Any
from typing import Any, TypeVar

from lsst.pex.config.config import Config


ActionTypeVar = TypeVar("ActionTypeVar", bound='ConfigurableAction')


class ConfigurableAction(Config):
"""A `ConfigurableAction` is an interface that extends a
`lsst.pex.config.Config` class to include a `__call__` method.
Expand All @@ -45,5 +48,17 @@ class ConfigurableAction(Config):
it will raise a `NotImplementedError` when called. Subclasses that
represent concrete actions must provide an override.
"""

identity: str | None = None
"""If a configurable action is assigned to a `ConfigurableActionField`, or a
`ConfigurableActionStructField` the name of the field will be bound to this
variable when it is retrieved.
"""

def __setattr__(self, attr, value, at=None, label="assignment"):
if attr == 'identity':
return object.__setattr__(self, attr, value)
return super().__setattr__(attr, value, at, label)

def __call__(self, *args, **kwargs) -> Any:
raise NotImplementedError("This method should be overloaded in subclasses")
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,16 @@

__all__ = ("ConfigurableActionField",)

from lsst.pex.config import ConfigField, FieldValidationError
from typing import Any

from lsst.pex.config import ConfigField, FieldValidationError, FieldTypeVar, Config
from lsst.pex.config.config import _typeStr, _joinNamePath
from lsst.pex.config.callStack import getCallStack

from . import ConfigurableAction


class ConfigurableActionField(ConfigField):
class ConfigurableActionField(ConfigField[FieldTypeVar]):
"""`ConfigurableActionField` is a subclass of `~lsst.pex.config.Field` that
allows a single `ConfigurableAction` (or a subclass of thus) to be
assigned to it. The `ConfigurableAction` is then accessed through this
Expand All @@ -42,7 +44,13 @@ class ConfigurableActionField(ConfigField):
# classes
name: str

def __set__(self, instance, value, at=None, label="assignment"):
def __set__(
self,
instance: Config,
value: FieldTypeVar | type[FieldTypeVar],
at: Any = None,
label: str = "assignment"
) -> None:
if instance._frozen:
raise FieldValidationError(self, instance,
"Cannot modify a frozen Config")
Expand All @@ -63,6 +71,12 @@ def __set__(self, instance, value, at=None, label="assignment"):
history = instance._history.setdefault(self.name, [])
history.append(("config value set", at, label))

def __get__(self, instance, owner=None) -> Any:
result = super().__get__(instance, owner)
if instance is not None:
result.identity = self.name # type: ignore
return result

def save(self, outfile, instance):
# docstring inherited from parent
# This is different that the parent class in that this field must
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,16 @@
__all__ = ("ConfigurableActionStructField", "ConfigurableActionStruct")

from types import SimpleNamespace
from typing import Iterable, Mapping, Optional, TypeVar, Union, Type, Tuple, List, Any, Dict
from typing import (
Iterable, Mapping, Optional, TypeVar, Union, Type, Tuple, List, Any, Dict, Iterator, Generic)
from types import GenericAlias

from lsst.pex.config.config import Config, Field, FieldValidationError, _typeStr, _joinNamePath
from lsst.pex.config.comparison import compareConfigs, compareScalars, getComparisonName
from lsst.pex.config.callStack import StackFrame, getCallStack, getStackFrame
from lsst.pipe.base import Struct

from . import ConfigurableAction
from . import ConfigurableAction, ActionTypeVar

import weakref

Expand Down Expand Up @@ -87,7 +89,7 @@ def __get__(self, instance, objtype=None) -> None:
return None


class ConfigurableActionStruct:
class ConfigurableActionStruct(Generic[ActionTypeVar]):
"""A ConfigurableActionStruct is the storage backend class that supports
the ConfigurableActionStructField. This class should not be created
directly.
Expand Down Expand Up @@ -119,8 +121,8 @@ class ConfigurableActionStruct:
have been removed from the `ConfigurableActionStruct`
"""
# declare attributes that are set with __setattr__
_config: Config
_attrs: Dict[str, ConfigurableAction]
_config_: weakref.ref
_attrs: Dict[str, ActionTypeVar]
_field: ConfigurableActionStructField
_history: List[tuple]

Expand All @@ -145,8 +147,9 @@ def __init__(self, config: Config, field: ConfigurableActionStructField,
def _config(self) -> Config:
# Config Fields should never outlive their config class instance
# assert that as such here
assert(self._config_() is not None)
return self._config_()
value = self._config_()
assert(value is not None)
return value

@property
def history(self) -> List[tuple]:
Expand Down Expand Up @@ -178,13 +181,15 @@ def __setattr__(self, attr: str, value: Union[ConfigurableAction, Type[Configura
valueInst = type(value)(__name=name, __at=at, __label=label, **value._storage)
else:
valueInst = value(__name=name, __at=at, __label=label)
self._attrs[attr] = valueInst
self._attrs[attr] = valueInst # type: ignore
else:
super().__setattr__(attr, value)

def __getattr__(self, attr):
def __getattr__(self, attr) -> Any:
if attr in object.__getattribute__(self, '_attrs'):
return self._attrs[attr]
result = self._attrs[attr]
result.identity = attr
return result
else:
super().__getattribute__(attr)

Expand All @@ -194,17 +199,19 @@ def __delattr__(self, name):
else:
super().__delattr__(name)

def __iter__(self) -> Iterable[ConfigurableAction]:
return iter(self._attrs.values())
def __iter__(self) -> Iterator[ActionTypeVar]:
for name in self.fieldNames:
yield getattr(self, name)

def items(self) -> Iterable[Tuple[str, ConfigurableAction]]:
return iter(self._attrs.items())
def items(self) -> Iterable[Tuple[str, ActionTypeVar]]:
for name in self.fieldNames:
yield name, getattr(self, name)


T = TypeVar("T", bound="ConfigurableActionStructField")


class ConfigurableActionStructField(Field):
class ConfigurableActionStructField(Field[ActionTypeVar]):
r"""`ConfigurableActionStructField` is a `~lsst.pex.config.Field` subclass
that allows `ConfigurableAction`\ s to be organized in a
`~lsst.pex.config.Config` class in a manner similar to how a
Expand All @@ -230,8 +237,13 @@ def __init__(self, doc: str, default: Optional[Mapping[str, ConfigurableAction]]
self._setup(doc=doc, dtype=self.__class__, default=default, check=None,
optional=optional, source=source, deprecated=deprecated)

def __class_getitem__(cls, params):
return GenericAlias(cls, params)

def __set__(self, instance: Config,
value: Union[None, Mapping[str, ConfigurableAction],
SimpleNamespace,
Struct,
ConfigurableActionStruct,
ConfigurableActionStructField,
Type[ConfigurableActionStructField]],
Expand Down Expand Up @@ -271,14 +283,18 @@ def __set__(self, instance: Config,
"Can only assign things that are subclasses of Configurable Action")
instance._storage[self.name] = value

def __get__(self: T, instance: Config, owner: None = None, at: Iterable[StackFrame] = None,
label: str = "default"
) -> Union[None, T, ConfigurableActionStruct]:
def __get__(
self,
instance,
owner=None,
at=None,
label='default'
) -> ConfigurableActionStruct[ActionTypeVar]:
if instance is None or not isinstance(instance, Config):
return self
return self # type: ignore
else:
field: Optional[ConfigurableActionStruct] = instance._storage[self.name]
return field
field: Optional[ConfigurableActionStruct] = instance._storage[self.name] # type: ignore
return field # type: ignore

def rename(self, instance: Config):
actionStruct: ConfigurableActionStruct = self.__get__(instance)
Expand All @@ -288,7 +304,7 @@ def rename(self, instance: Config):
fullname = _joinNamePath(base_name, k)
v._rename(fullname)

def validate(self, instance):
def validate(self, instance: Config):
value = self.__get__(instance)
if value is not None:
for item in value:
Expand Down

0 comments on commit db10279

Please sign in to comment.