"""Interface between script and Finesse.
Adapters provide a programmatic way to retrieve useful information about KatScript
directives using their corresponding Python objects, and vice versa. This is primarily
used for the compiler and generator, but is also used to improve syntax suggestions and
error messages.
The adapter class hierarchy is intentionally very generic. By default, an ordinary
KatScript directive corresponding to an ordinary Finesse object is quite simple to
specify and should "just work". When special behaviour is required, e.g. in cases where
the KatScript directive has different arguments than the corresponding Python object
(e.g. `tem`) or where there is no corresponding Python object (e.g. `modes`), the
methods and members of the adapter can be overridden.
"""
import abc
import inspect
from functools import partial, cached_property
from enum import unique, Enum
import logging
from typing import get_type_hints, Any, Union, List
from dataclasses import dataclass
from ..parameter import Parameter, ParameterRef
from ..element import ModelElement
from ..env import TERMINAL_WIDTH, INDENT
LOGGER = logging.getLogger(__name__)
[docs]@unique
class ArgumentType(Enum):
"""Signature argument types.
While we just copy those defined by :mod:`inspect`, note that the definitions here
are more abstract than those of :mod:`inspect`: these refer to the different
flavours of script argument, which may or may not map directly to or from a Python
type's call signature.
"""
POS_ONLY = inspect.Parameter.POSITIONAL_ONLY
ANY = inspect.Parameter.POSITIONAL_OR_KEYWORD
KEYWORD_ONLY = inspect.Parameter.KEYWORD_ONLY
VAR_POS = inspect.Parameter.VAR_POSITIONAL
VAR_KEYWORD = inspect.Parameter.VAR_KEYWORD
_INSPECT_KIND_CONVERSIONS = {
inspect.Parameter.POSITIONAL_ONLY: ArgumentType.POS_ONLY,
inspect.Parameter.POSITIONAL_OR_KEYWORD: ArgumentType.ANY,
inspect.Parameter.KEYWORD_ONLY: ArgumentType.KEYWORD_ONLY,
inspect.Parameter.VAR_POSITIONAL: ArgumentType.VAR_POS,
inspect.Parameter.VAR_KEYWORD: ArgumentType.VAR_KEYWORD,
}
class _empty:
"""Marker object for empty values."""
# Sometimes `None` is an intended default value, so we define a special field to use for
# actually empty defaults.
# NOTE: this should *not* be set to the same as :attr:`inspect.Parameter.empty`, as
# doing so would interfere with the construction of dataclasses.
EMPTY_VALUE = _empty
[docs]@dataclass
class Argument:
"""A generic instruction argument.
This is similar but not identical to :class:`inspect.Parameter`. Arguments in the
Finesse sense are more general than :class:`inspect.Parameter` since they can refer
to KatScript arguments, and KatScript instructions may not necessarily define their
supported arguments via Python class signatures.
"""
name: str
kind: ArgumentType = ArgumentType.ANY
default: Any = EMPTY_VALUE
annotation: Any = EMPTY_VALUE
@property
def has_no_default(self):
return self.default is EMPTY_VALUE
[docs]@dataclass
class BoundArgument(Argument):
"""A concrete argument originating from a call to a setter.
This is the same as :class:`.Argument` except in its handling of variadic arguments.
Where this represents a variadic argument, it contains information as to which
variadic argument it represents (either the sequence number or keyword). It is used
to resolve self-references and to map compilation errors back to the original
script.
"""
# The sequence number for variadic positional arguments.
var_sequence: int = None
[docs]@dataclass
class ArgumentDump(Argument):
"""A Finesse object argument name, its current value, default value, kind,
annotation, and whether it should be dumped by value or reference.
This encapsulates an argument for a script instruction. It can represent Finesse
object parameters like floats, strings and model parameters, and is primarily used
to generate KatScript representations of Finesse objects.
"""
value: Any = EMPTY_VALUE
other_defaults: List[Any] = None
reference: bool = False
@property
def is_default(self):
"""Whether the value is the parameter's default."""
if self.default is EMPTY_VALUE and self.other_defaults is None:
return False
try:
return any([self.value == default for default in self._defaults])
except ValueError:
# Assume we have a sequence.
return any([all(self.value == default) for default in self._defaults])
@property
def _defaults(self):
yield self.default
if self.other_defaults:
yield from self.other_defaults
[docs]class ItemAdapter:
"""Adapter defining how a script instruction maps to/from a Python type.
This encapsulates the required information to take a script instruction and generate
a corresponding Python object (e.g. a :class:`.Laser` from a `laser l1 ...`
instruction), to add it to a :class:`.Model` (or in the case of commands, set some
model attribute), to dump that Python object back to script, and to generate
documentation.
Parameters
----------
full_name : :class:`str`
The instruction's unabbreviated name. This must be alphanumeric and can contain
underscores but no spaces.
short_name : :class:`str`, optional
The instruction's short form name, used when generating compact script. If not
specified, `full_name` is used in cases where the short form is desired.
other_names : sequence, optional
Any other supported names for this instruction.
getter : :class:`.ItemDumper`, optional
Object handling the retrieval of parameters from the Python object corresponding
to this instruction.
factory : :class:`.ItemFactory`, optional
Object handling the creation of the Python object corresponding to this
instruction.
setter : :class:`.ItemSetter`, optional
Object handling the setting of parameters in the Python object corresponding to
this instruction's parameters.
documenter : :class:`Documenter`
Object handling the retrieval of docstrings and syntax suggestions for the
instruction.
singular : :class:`bool`, optional
Flag indicating that this instruction can be defined only once per script.
Defaults to `False`.
build_last : :class:`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.
"""
def __init__(
self,
full_name,
short_name=None,
other_names=None,
getter=None,
factory=None,
setter=None,
documenter=None,
singular=False,
build_last=False,
):
if other_names is None:
other_names = []
self.full_name = full_name
self.short_name = short_name
self.other_names = other_names
self.getter = getter
self.factory = factory
self.setter = setter
self.documenter = documenter
self.singular = singular
self.build_last = build_last
@property
def aliases(self):
"""The instruction alias(es).
:getter: Sequence of aliases for this instruction.
"""
aliases = [self.full_name]
if self.short_name:
aliases.append(self.short_name)
if self.other_names:
aliases.extend(self.other_names)
return aliases
def __repr__(self):
return f"❮{self.__class__.__name__}.{self.full_name} @ {hex(id(self))}❯"
[docs]class ItemHandler(metaclass=abc.ABCMeta):
"""Root class for all dumper, setter, factory and documenter objects."""
def __init__(self, *, item_type):
self.item_type = item_type
[docs]class ItemDumper(ItemHandler, metaclass=abc.ABCMeta):
@abc.abstractmethod
def __call__(self, adapter, container):
raise NotImplementedError
[docs]class ItemFactory(ItemHandler, metaclass=abc.ABCMeta):
def __call__(self, *args, **kwargs):
raise NotImplementedError
[docs]class ItemSetter(ItemHandler, metaclass=abc.ABCMeta):
@abc.abstractmethod
def __call__(self, container, item):
raise NotImplementedError
[docs] def update_parameter(self, item, argument, value):
"""Update the built item's `name` parameter to `value`.
This is used to update a parameter after the item has been created and added to
the model, such as when resolving self-references.
Parameters
----------
item : object
The item.
argument : :class:`.BoundArgument`
The item argument corresponding to the value to update.
value : object
The new value.
"""
raise NotImplementedError(
f"{self} does not support parameter updates after construction"
)
[docs] @abc.abstractmethod
def arguments(self):
"""The supported constructor arguments for this item.
Returns
-------
:class:`dict`
Mapping of argument names to :class:`.Argument` objects for this setter.
"""
raise NotImplementedError
@property
def var_positional_argument(self):
for argument in self.arguments().values():
if argument.kind is ArgumentType.VAR_POS:
return argument
@property
def var_keyword_argument(self):
for argument in self.arguments().values():
if argument.kind is ArgumentType.VAR_KEYWORD:
return argument
[docs] def bind_argument(self, name_or_index):
"""Return a bound argument object for `name`.
Parameters
----------
name_or_index : :class:`str` or :class:`int`
The argument keyword or index.
Returns
-------
:class:`.BoundArgument`
The argument metadata corresponding to `name_or_index`.
"""
arguments = self.arguments()
kwargs = {}
if isinstance(name_or_index, int):
# This is a positional argument.
index = name_or_index
args_by_index = list(arguments.values())
try:
argument = args_by_index[index]
except IndexError as e:
# This could either be a variadic positional argument or invalid.
if var_argument := self.var_positional_argument:
argument = var_argument
else:
raise TypeError(
f"positional argument at index {repr(index)} does not exist "
f"for {repr(self.item_type)}"
) from e
# This is a variadic positional argument. Its sequence is the offset
# with respect to the index of the variadic argument.
sequence = index - args_by_index.index(argument)
assert sequence >= 0
kwargs["var_sequence"] = sequence
# Form the bound name from the variadic positional argument name and the
# sequence.
name = f"{argument.name}{sequence}"
else:
name = argument.name
else:
# This is a keyword argument.
try:
argument = arguments[name_or_index]
except KeyError as e:
# This could either be a variadic keyword argument or invalid.
if var_argument := self.var_keyword_argument:
argument = var_argument
else:
raise TypeError(
f"keyword argument {repr(name)} does not exist for "
f"{repr(self.item_type)}"
) from e
name = name_or_index
else:
name = argument.name
kwargs["name"] = name
kwargs["kind"] = argument.kind
kwargs["default"] = argument.default
kwargs["annotation"] = argument.annotation
return BoundArgument(**kwargs)
[docs] def positional_args(self, only=False, keyword_defaults=True):
"""The non-keyword-only arguments of the call signature.
Parameters
----------
only : :class:`bool`, optional
Only include positional-only arguments. Defaults to `False`.
keyword_defaults : :class:`bool`, optional
Include keyword arguments that have default values. Defaults to `True`.
Returns
-------
:class:`dict`
The call object's positional parameters.
"""
kinds = [ArgumentType.POS_ONLY]
if not only:
kinds.append(ArgumentType.ANY)
return self._filter_signature(kinds, keyword_defaults=keyword_defaults)
[docs] def keyword_args(self, only=False):
"""The non-positional-only arguments of the call signature.
Parameters
----------
only : :class:`bool`, optional
Only include keyword-only arguments; defaults to `False`.
Returns
-------
:class:`dict`
The call object's keyword parameters.
"""
kinds = [ArgumentType.KEYWORD_ONLY]
if not only:
kinds.append(ArgumentType.ANY)
return self._filter_signature(kinds, keyword_defaults=True)
def _filter_signature(self, kinds, keyword_defaults):
sig = {}
for name, argument in self.arguments().items():
if argument.kind not in kinds:
continue
if not keyword_defaults and not argument.has_no_default:
continue
sig[name] = argument
return sig
[docs]class ItemDocumenter(ItemHandler, metaclass=abc.ABCMeta):
@property
@abc.abstractmethod
def docstring(self):
raise NotImplementedError
[docs] def syntax(
self,
spec,
adapter,
short_names=True,
optional_as_positional=False,
multiline=None,
):
from .generator import KatUnbuilder
unbuilder = KatUnbuilder(spec=spec)
if short_names and adapter.short_name is not None:
item_name = adapter.short_name
else:
item_name = adapter.full_name
syntaxcall = partial(
self._syntax,
item_name,
unbuilder,
optional_as_positional=optional_as_positional,
)
if multiline is None:
# Return appropriate syntax suggestion based on the console width.
oneline_syntax = syntaxcall(multiline=False)
if len(oneline_syntax) > TERMINAL_WIDTH:
return syntaxcall(multiline=True)
else:
return oneline_syntax
return syntaxcall(multiline=multiline)
[docs] def syntax_correction(
self, user_directive, spec, optional_as_positional=False, multiline=False
):
"""Suggest syntax using the user's directive."""
from .generator import KatUnbuilder
unbuilder = KatUnbuilder(spec=spec)
return self._syntax(
user_directive,
unbuilder,
optional_as_positional=optional_as_positional,
multiline=multiline,
)
@abc.abstractmethod
def _syntax(self, item_name, unbuilder, optional_as_positional, multiline):
"""Generate syntax for `item_name`."""
raise NotImplementedError
[docs]class SignatureArgumentMixin(metaclass=abc.ABCMeta):
"""Mixin providing ability to retrieve signature arguments from a Python function.
Parameters
----------
sig_type : type, optional
The signature type to retrieve arguments from. Defaults to `item_type`.
sig_ignore : sequence, optional
Signature argument names to ignore.
"""
[docs] def __init__(self, *, sig_type=None, sig_ignore=None, **kwargs):
super().__init__(**kwargs)
if sig_ignore is None:
sig_ignore = []
self._sig_type = sig_type
self._sig_ignore = sig_ignore
@property
def sig_type(self):
sig_type = self._sig_type
if sig_type is None:
sig_type = self.item_type
return sig_type
[docs] def arguments(self):
arguments = {}
for name, parameter in inspect.signature(self.sig_type).parameters.items():
if name in self._sig_ignore:
continue
default = parameter.default
if default is inspect.Parameter.empty:
default = EMPTY_VALUE
annotation = parameter.annotation
if annotation is inspect.Parameter.empty:
annotation = EMPTY_VALUE
arguments[name] = Argument(
name=name,
kind=_INSPECT_KIND_CONVERSIONS[parameter.kind],
default=default,
annotation=annotation,
)
return arguments
[docs]class SignatureAttributeParameterMixin(SignatureArgumentMixin, metaclass=abc.ABCMeta):
"""Mixin providing the ability to get and set item parameters by inspecting its
constructor signature arguments matching equivalently named object attributes.
Parameters
----------
ref_args : sequence, optional
Names of arguments that should be considered to be references. Corresponding
:class:`.ArgumentDump` objects produced by this class will have their
`reference` flags set to `True` to indicate to the generator that these should
be treated as references instead of values.
var_pos_attr : str or callable, optional
The name of the field containing a sequence of variadic positional argument
values, or a callable that returns the name of the field to set given the
sequence number of the variadic argument, if the signature supports variadic
positional arguments. Defaults to `"args"`.
var_keyword_attr : str or callable, optional
The name of the field containing a mapping of variadic keyword arguments to
values, or a callable that returns the name of the field to set given the name
of the keyword argument, if the signature supports variadic positional
arguments. Defaults to the identity function.
"""
[docs] def __init__(
self, ref_args=None, var_pos_attr=None, var_keyword_attr=None, **kwargs
):
super().__init__(**kwargs)
if ref_args is None:
ref_args = []
self.ref_args = ref_args
if var_pos_attr is None:
var_pos_attr = "args"
self.var_pos_attr = var_pos_attr
if var_keyword_attr is None:
var_keyword_attr = lambda field: field
self.var_keyword_attr = var_keyword_attr
[docs] def dump_parameters(self, adapter, item):
"""Build parameter mapping by retrieving object attributes using the
signature."""
# Get type hints for the dump signature type.
#
# NOTE: this information is also included in the :class:`inspect.Parameter`
# objects returned by :func:`inspect.signature` used below, but these are
# potentially unresolved due to Python 3.9+'s lazily evaluation of
# annotations (see PEP 563). Instead we use the typing module to grab the
# resolved type, which is the way recommended by the Python docs.
hints = get_type_hints(self.sig_type)
values = self._parameter_values(adapter, item)
arguments = self.arguments().values()
dump_parameters = {}
for param, value in zip(arguments, values):
ref_name = param.name
if param.kind in (ArgumentType.VAR_POS, ArgumentType.VAR_KEYWORD):
ref_name = f"*{ref_name}"
if ref_name in self.ref_args:
assert isinstance(value, (Parameter, ParameterRef))
LOGGER.debug(
f"dumping parameter {repr(value)} by reference as it represents a "
f"target"
)
reference = True
else:
reference = False
dump_parameters[param.name] = ArgumentDump(
param.name,
value=value,
default=param.default,
kind=param.kind,
annotation=hints.get(param.name),
reference=reference,
)
return dump_parameters
def _parameter_values(self, adapter, item):
values = []
for arg, param in self.arguments().items():
name = param.name
try:
# Try to get the attribute value.
value = getattr(item, name)
except AttributeError as e:
expected_attrib = f"{item.__class__.__name__}.{name}"
error_msg = (
f"Error while generating parameter {repr(name)} from {repr(item)} "
f"for instruction {repr(adapter.full_name)}. The adapter for this "
f"object, {repr(adapter)}, specifies that parameters should be "
f"generated by looking for attributes or properties in the object "
f"corresponding to the names of the arguments in the constructor "
f"signature, {repr(self.sig_type)}; yet, no attribute "
f"{repr(expected_attrib)} was found. To fix this, ensure that this "
f"attribute or property is defined, or specify a different type of "
f"{ItemDumper.__name__} class as the 'getter' for the directive in "
f"the language specification."
)
raise NotImplementedError(error_msg) from e
values.append(value)
return values
[docs] def update_parameter(self, item, argument, value):
if argument.kind is ArgumentType.VAR_POS:
# This is a variadic positional argument.
try:
field = self.var_pos_attr(argument.var_index)
except TypeError:
field = self.var_pos_attr
elif argument.kind is ArgumentType.VAR_KEYWORD:
# This is a variadic keyword argument.
try:
field = self.var_keyword_attr(argument.name)
except TypeError:
field = self.var_keyword_attr
else:
# This is an ordinary positional or keyword argument.
field = argument.name
if not hasattr(item, field):
# The setter is probably incorrectly configured.
raise RuntimeError(
f"cannot set {repr(argument.name)} for item {repr(item)} "
f"(attribute {repr(field)} does not exist)"
)
setattr(item, field, value)
[docs]class NumpyStyleDocstringGetterMixin(metaclass=abc.ABCMeta):
[docs] def __init__(self, *, doc_type=None, **kwargs):
super().__init__(**kwargs)
self._doc_type = doc_type
@property
def doc_type(self):
doc_type = self._doc_type
if doc_type is None:
doc_type = self.item_type
return doc_type
@property
def docstring(self):
"""The Python API item's docstring.
Note: unlike :func:`inspect.getdoc`, this method returns only the docstring
directly defined in `doc_type`, rather than taking the inherited docstring if
not found. If no docstring is defined, `None` is returned.
"""
try:
doc = self.doc_type.__doc__
except AttributeError:
return None
if doc is not None:
doc = inspect.cleandoc(doc)
return doc
@cached_property
def _parsed_docstring_obj(self):
from finesse_numpydoc import NumpyDocString
docstring = self.docstring
if docstring is None:
docstring = ""
return NumpyDocString(docstring)
[docs] def summary(self):
"""The item's summary, parsed from the docstring."""
return self._docutils_implode(self._parsed_docstring_obj["Summary"])
[docs] def extended_summary(self):
"""The item's extended summary, parsed from the docstring."""
return self._docutils_implode(self._parsed_docstring_obj["Extended Summary"])
[docs] def argument_descriptions(self):
"""The types and descriptions for each argument as parsed from the docstring.
Returns
-------
:class:`dict`
Mapping of arguments to their type and docstrings as listed in the object's
docstring. Note that the arguments may not correspond to signature argument
names; numpydoc allows arguments to share docstrings so some keys may be
e.g. `n, m`.
"""
return {
name: (type_, self._docutils_implode(description))
for name, type_, description in self._parsed_docstring_obj["Parameters"]
}
def _docutils_implode(self, pieces):
"""Join docutils list of strings into a single string."""
if not pieces:
return None
return " ".join(pieces)
[docs]class ElementDumper(SignatureAttributeParameterMixin, ItemDumper):
def __init__(self, *, item_type, **kwargs):
super().__init__(
item_type=item_type,
sig_type=item_type.__init__,
sig_ignore=["self", "name"],
**kwargs,
)
def __call__(self, adapter, model):
"""Get element dump object(s) from `model`.
Parameters
----------
adapter : :class:`.ItemAdapter`
The adapter corresponding to this getter.
model : :class:`.Model`
The model from which to dump element(s) of this type.
Yields
------
:class:`.ElementDump`
Object containing a mapping of keyword argument names to
:class:`.ArgumentDump` objects and whether they are all default values.
"""
for element in model.get_elements_of_type(self.item_type):
parameters = self.dump_parameters(adapter, element)
yield ElementDump(
element=element,
adapter=adapter,
parameters=parameters,
# The only element that is in a model by default is Fsig, which is
# handled separately.
is_default=False,
)
[docs]@dataclass
class ElementDump:
"""A set of element parameters and metadata."""
element: ModelElement
adapter: ItemAdapter
parameters: Union[List[ArgumentDump], List[List[ArgumentDump]]]
is_default: bool
[docs]class AnalysisDumper(SignatureAttributeParameterMixin, ItemDumper):
[docs] def __init__(self, *, item_type, **kwargs):
super().__init__(
item_type=item_type,
sig_type=item_type.__init__,
sig_ignore=["self"],
**kwargs,
)
def __call__(self, adapter, model):
"""Get analysis dump object(s) from `model`.
Parameters
----------
adapter : :class:`.ItemAdapter`
The adapter corresponding to this getter.
model : :class:`.Model`
The model from which to dump analysis (or analyses) of this type.
Yields
------
:class:`.AnalysisDump`
Object containing a mapping of keyword argument names to
:class:`.ArgumentDump` objects and whether they are all default values.
"""
# Only yield something if there is an analysis to dump.
if model.analysis is None:
return
yield from self.dump(adapter, model.analysis)
[docs] def dump(self, adapter, analysis):
"""Get analysis dump object(s) for `analysis`.
Parameters
----------
adapter : :class:`.ItemAdapter`
The adapter corresponding to this getter.
analysis : :class:`.Action`
The action to dump.
Yields
------
:class:`.AnalysisDump`
Object containing a mapping of keyword argument names to
:class:`.ArgumentDump` objects and whether they are all default values.
"""
# Only yield something if the analysis is an exact type match.
if type(analysis) is not self.item_type:
return
parameters = self.dump_parameters(adapter, analysis)
yield AnalysisDump(
analysis=analysis,
adapter=adapter,
parameters=parameters,
is_default=False, # There is no default analysis in models.
)
[docs]@dataclass
class AnalysisDump:
"""A set of analysis parameters and metadata."""
analysis: Any # FIXME: We should have an Analysis type.
adapter: ItemAdapter
parameters: Union[List[ArgumentDump], List[List[ArgumentDump]]]
is_default: bool
@property
def item_name(self):
return self.analysis.__class__.__name__
[docs]class ElementFactory(ItemFactory):
def __init__(self, last=False, **kwargs):
super().__init__(**kwargs)
self.last = last
def __call__(self, *args, **kwargs):
return self.item_type(*args, **kwargs)
[docs]class ElementSetter(SignatureAttributeParameterMixin, ItemSetter):
def __init__(self, *, item_type, **kwargs):
super().__init__(
item_type=item_type,
sig_type=item_type.__init__,
sig_ignore=["self", "name"],
**kwargs,
)
def __call__(self, model, item):
model.add(item)
[docs]class AnalysisFactory(ItemFactory):
def __call__(self, *args, **kwargs):
return self.item_type(*args, **kwargs)
[docs]class AnalysisSetter(SignatureAttributeParameterMixin, ItemSetter):
def __init__(self, *, item_type, **kwargs):
super().__init__(
item_type=item_type,
sig_type=item_type.__init__,
sig_ignore=["self"],
**kwargs,
)
def __call__(self, model, item):
model.analysis = item
[docs]class SyntaxMixin(SignatureArgumentMixin, metaclass=abc.ABCMeta):
def _argument_syntax(self, unbuilder, optional_as_positional):
"""Sequence of formatted, ordered argument syntax."""
parameters = self.arguments()
positional = []
var_positional = None
keyword = []
var_keyword = None
for name, param in parameters.items():
kind = param.kind
if kind is ArgumentType.VAR_POS:
var_positional = f"*{name}"
continue
elif kind is ArgumentType.VAR_KEYWORD:
var_keyword = f"**{name}"
continue
elif kind is ArgumentType.ANY:
if optional_as_positional or param.has_no_default:
kind = ArgumentType.POS_ONLY
else:
kind = ArgumentType.KEYWORD_ONLY
if kind is ArgumentType.POS_ONLY:
positional.append(name)
else:
default = unbuilder.unbuild(param.default)
keyword.append(f"{name}={default}")
# Stack arguments in the correct order.
items = positional
if var_positional:
items.append(var_positional)
items.extend(keyword)
if var_keyword:
items.append(var_keyword)
return items
[docs]class ElementSyntaxMixin(SyntaxMixin, metaclass=abc.ABCMeta):
def _syntax(self, item_name, unbuilder, multiline, **kwargs):
# Multiline is ignored.
args = self._argument_syntax(unbuilder, **kwargs)
argstr = " " + " ".join(args) if args else " "
return f"{item_name} name{argstr}"
[docs]class FunctionalSyntaxMixin(SyntaxMixin, metaclass=abc.ABCMeta):
def _syntax(self, item_name, unbuilder, multiline, **kwargs):
args = self._argument_syntax(unbuilder, **kwargs)
if multiline:
mlargs = []
for arg in args:
mlargs.append(f"{INDENT}{arg}")
mlargstr = ",\n".join(mlargs)
syntax = f"{item_name}(\n{mlargstr}\n)"
else:
argstr = ", ".join(args) if args else ""
syntax = f"{item_name}({argstr})"
return syntax
[docs]class ElementDocumenter(
NumpyStyleDocstringGetterMixin, ElementSyntaxMixin, ItemDocumenter
):
def __init__(self, *, item_type, **kwargs):
super().__init__(
item_type=item_type,
sig_type=item_type.__init__,
sig_ignore=["self", "name"],
**kwargs,
)
[docs]class AnalysisDocumenter(
NumpyStyleDocstringGetterMixin, FunctionalSyntaxMixin, ItemDocumenter
):
def __init__(self, *, item_type, **kwargs):
super().__init__(
item_type=item_type,
sig_type=item_type.__init__,
sig_ignore=["self"],
**kwargs,
)
[docs]class CommandMethodSetter(SignatureAttributeParameterMixin, ItemSetter):
def __init__(self, *, sig_ignore=("self",), **kwargs):
super().__init__(sig_ignore=sig_ignore, **kwargs)
def __call__(self, model, argskwargs):
args, kwargs = argskwargs
# Wrap the call in a partial so that invalid argument errors correctly
# correspond to those available in the script instruction.
setter = partial(self.item_type, model)
setter(*args, **kwargs)
[docs]class CommandMethodDocumenter(
NumpyStyleDocstringGetterMixin, FunctionalSyntaxMixin, ItemDocumenter
):
def __init__(self, *, sig_ignore=("self",), **kwargs):
super().__init__(sig_ignore=sig_ignore, **kwargs)
[docs]class CommandPropertyDumper(SignatureAttributeParameterMixin, ItemDumper):
def __init__(self, *, item_type, default=EMPTY_VALUE, **kwargs):
assert isinstance(item_type, property)
# The sig_type is the setter because we use the setter to add metadata to the
# ArgumentDump below.
super().__init__(
item_type=item_type,
sig_type=item_type.fset,
sig_ignore=("self",),
**kwargs,
)
self._prop_default = default
def __call__(self, adapter, model):
arguments = self.arguments()
assert len(arguments) == 1
argument = next(iter(arguments.values()))
if self._prop_default is not EMPTY_VALUE:
if (
argument.default is not EMPTY_VALUE
and self._prop_default != argument.default
):
raise RuntimeError(
f"default specified in spec, {repr(self._prop_default)}, differs "
f"from that specified in the property setter, "
f"{repr(argument.default)}"
)
default = self._prop_default
else:
default = argument.default
parameter = ArgumentDump(
argument.name,
value=self.item_type.fget(model),
default=default,
kind=argument.kind,
)
yield CommandDump(
adapter=adapter,
parameters={argument.name: parameter},
is_default=parameter.is_default,
)
[docs]class CommandPropertyDocumenter(CommandMethodDocumenter):
def __init__(self, *, item_type, **kwargs):
assert isinstance(item_type, property)
super().__init__(
item_type=item_type,
sig_type=item_type.fset,
doc_type=item_type.fset,
**kwargs,
)
[docs]class CommandPropertySetter(CommandMethodSetter):
def __init__(self, *, item_type, **kwargs):
assert isinstance(item_type, property)
super().__init__(item_type=item_type, sig_type=item_type.fset, **kwargs)
def __call__(self, model, argskwargs):
args, kwargs = argskwargs
# Wrap the call in a partial so that invalid argument errors correctly
# correspond to those available in the script instruction.
setter = partial(self.item_type.fset, model)
setter(*args, **kwargs)
[docs]@dataclass
class CommandDump:
"""A set of command parameters and metadata."""
adapter: ItemAdapter
parameters: Union[List[ArgumentDump], List[List[ArgumentDump]]]
is_default: bool
@property
def item_name(self):
return self.adapter.full_name