Source code for finesse.script.adapter

"""Kat script to Finesse object adapters.

Adapters provide various useful information to the kat script compiler about Finesse
objects, and vice versa for the kat script generator.
"""

import abc
import inspect
import logging

LOGGER = logging.getLogger(__name__)


def _remove_signature_parameters(signature, remove_first=True, remove_extra=None):
    """Remove any parameter whose name appears in `remove` from `signature`.

    There is also the option to remove the first parameter, which is usually "self" (in
    the case of methods) or some parameter that receives the container (e.g. a
    :class:`.Model` in the case of commands).
    """
    if not remove_extra:
        remove_extra = []
    parameters = []
    for i, (name, parameter) in enumerate(signature.parameters.items()):
        if i == 0 and remove_first:
            continue
        if name in remove_extra:
            continue
        parameters.append(parameter)
    return signature.replace(parameters=parameters)


[docs]class BaseAdapter(metaclass=abc.ABCMeta): """Adapter defining a kat script instruction and how it maps to/from a type. This encapsulates the required information to take a kat script instruction and generate a corresponding Python object (e.g. a :class:`.Laser` from a `laser l1 ...` instruction) and to dump that Python object back to kat script. This is an abstract class that should be subclassed. Parameters ---------- aliases : str or sequence The instruction alias(es). The first is considered the full name and is dumped in full archival mode. The last is considered the short form name and used in default dump mode. Any other specified aliases are only supported for parsing. setter : type The Python setter type for this instruction. This is used to build Python objects for elements, analyses, etc., and to set values in the model for commands. getter : type or callable, optional The Python getter type for this instruction. This is used to retrieve the object's current parameter values to generate its kat script representation. If `getter` is not specified, it is assumed that this instruction should not be regenerated in kat script. This may be the same as `setter` or alternatively a class that inherts :class:`.GetterProxy`. In the latter case the object should implement a `__call__` method that accepts the containing object and returns either a single (args, kwargs) tuple or a sequence thereof, depending on `singular`. singular : bool Whether the instruction can only be specified once per script. In such a case, if `getter` is a `GetterProxy`, it should only define a single `(args, kwargs)` tuple instead of a sequence thereof. build_last : bool, optional Whether to build the Python object last, regardless of dependencies. This is useful for elements with implicit dependencies (see e.g. the cavity adapter). Be careful using this flag because statements for other adapters that depend on statements for adapters with this flag will be built first. Defaults to False. Raises ------ ValueError If `aliases` sequence has less than 1 entry. """
[docs] def __init__( self, aliases, setter, getter=None, singular=False, build_last=False, ): if isinstance(aliases, str): aliases = [aliases] if len(aliases) < 1: raise ValueError("At least one alias must be specified in 'aliases'") self.aliases = aliases self.setter = setter self.getter = getter self.singular = singular self.build_last = build_last
@property def full_name(self): return self.aliases[0] @property def short_name(self): return self.aliases[-1]
[docs] @abc.abstractmethod def apply(self, model, *args, **kwargs): raise NotImplementedError
[docs] def get(self, container): """Get ordered mapping of argument names to values from `container`. Parameters ---------- container : object The container to retrieve argument names and values for. Returns ------- :class:`list` Positional argument values. :class:`dict` Mapping of keyword argument names to values. Raises ------ RuntimeError If this adapter's :meth:`.dump_signature` contains parameters that are not present in `container`. """ if not self.getter: LOGGER.debug(f"{self!r} has no kat script representation") return if isinstance(self.getter, GetterProxy): params = self.getter(container) if params is None: LOGGER.debug( f"skipping serialisation of empty {self!r} parameters (returned " f"from a GetterProxy)" ) return return params else: kwargs = {} for arg, param in self.dump_signature().parameters.items(): name = param.name try: # Try to get the attribute value. value = getattr(container, name) except AttributeError as e: raise RuntimeError( f"The dump signature (getter) for '{self!r}' defines parameter " f"'{name}' that is not a property or attribute of " f"'{container!r}'. Either define " f"'{container.__class__.__name__}.{name}' as a property or " f"attribute or set {self.full_name}'s `getter` to a " f"`GetterProxy` object (see BaseAdapter docstring)." ) from e kwargs[name] = value # FIXME: in the future, this should support being able to return parameters in # different modes, like positional or keyword-only. return [], kwargs
@property def docobj_type(self): """Type to use to get the Python object's docstring. :getter: Returns the type to be used to retrieve the Python object's docstring. Notes ----- This assumes that classes contain the documentation for the `__init__` method, in line with the Numpydoc convention. """ signature_type = self.setter if isinstance(signature_type, property): # Use the setter's signature. signature_type = signature_type.fset return signature_type
[docs] def doc_signature(self, exclude_name=False): """The signature of the documentation object. This is an ordered mapping of the Python object's supported parameters to :py:class:`inspect.Parameter` objects. Parameters ---------- exclude_name : bool, optional Exclude the `name` parameter. This is useful for code that already handles the name separately. Defaults to `False`. Returns ------- :py:class:`inspect.Signature` The documentation object's pseudo-signature. """ return _remove_signature_parameters( inspect.signature(self.docobj_type), remove_first=True, remove_extra=["name"] if exclude_name else [], )
@property def call_signature_type(self): """Type to use to get the Python object's call signature. :getter: Returns the type to be used to retrieve the Python object's call signature. """ signature_type = self.setter if inspect.isclass(signature_type): # Use the init method. signature_type = signature_type.__init__ elif isinstance(signature_type, property): # Use the setter's signature. signature_type = signature_type.fset return signature_type
[docs] def call_signature(self, exclude_name=False): """The call signature of the corresponding Python object constructor. This is an ordered mapping of the Python object's supported parameters to :py:class:`inspect.Parameter` objects. Parameters ---------- exclude_name : bool, optional Exclude the `name` parameter. This is useful for code that already handles the name separately. Defaults to `False`. Returns ------- :py:class:`inspect.Signature` The call object's pseudo-signature. """ return _remove_signature_parameters( inspect.signature(self.call_signature_type), remove_first=True, remove_extra=["name"] if exclude_name else [], )
@property def dump_signature_type(self): signature_type = self.getter if isinstance(signature_type, GetterProxy): # You're doing it wrong. raise TypeError( "Adapter has a getter proxy set, so no dump signature type exists. " "Change the code calling this method to first check the getter type." ) if inspect.isclass(signature_type): # Use the init method. signature_type = signature_type.__init__ elif isinstance(signature_type, property): # Use the getter's signature. signature_type = signature_type.fget return signature_type
[docs] def dump_signature(self, exclude_name=False): """The Python object constructor call signature as available to the generator. This is used when generating kat script for the corresponding Python object and may exclude or include parameters found or not found in the real Python object constructor, e.g. to allow a kat script command to use different arguments to that of the Python API. Parameters ---------- exclude_name : bool, optional Exclude the `name` parameter. This is useful for code that already handles the name separately. Defaults to `False`. Returns ------- :py:class:`inspect.Signature` The dump object's pseudo-signature. """ return _remove_signature_parameters( inspect.signature(self.dump_signature_type), remove_first=True, remove_extra=["name"] if exclude_name else [], )
def __repr__(self): return f"<{self.__class__.__name__}.{self.full_name} @ {hex(id(self))}>"
[docs]class ModelObject(BaseAdapter, metaclass=abc.ABCMeta): """Mixin for adapters that control Python objects directly (e.g. elements) as opposed to attributes (e.g. commands)."""
[docs] def compile(self, args, kwargs): return self.setter(*args, **kwargs)
[docs]class ElementAdapter(ModelObject, BaseAdapter): """Adapter for elements."""
[docs] def apply(self, model, item): model.add(item)
[docs] def get(self, item): # Split the name argument onto its own. args, kwargs = super().get(item) element_name = kwargs.pop("name") return element_name, args, kwargs
[docs]class CommandAdapter(BaseAdapter): """Adapter for commands. Commands set properties of a :class:`.Model`. Command getters should return a *sequence* of args and kwargs representing potentially multiple commands to dump. """
[docs] def apply(self, model, allargs): # The compiler passes this method an (args, kwargs) tuple when building # commands. args, kwargs = allargs return self.setter(model, *args, **kwargs)
[docs]class AnalysisAdapter(ModelObject, BaseAdapter): """Adapter for analyses."""
[docs] def apply(self, model, item): model.analysis = item
[docs]class GetterProxy(metaclass=abc.ABCMeta): """An object that when called returns the parameters and values that should be dumped to represent an object in kat script. Inheriting classes should define `__call__`, returning an ordered mapping of parameters to values. """ @abc.abstractmethod def __call__(self, item): raise NotImplementedError