Skip to content

Label meta

This file defines the LabelElement class, which is the base class for all the classes that form the in-memory representation of a MAST label.

LabelElement performs introspective magic to provide dataclasses- compatible instance fields, augmented with the capability to store LabelElement objects in YAML files, load them from YAML files, and detect and gracefully handle errors in human-written YAML labels.

See the docstrings for LabelElement, LabelElement._validate_label, and special_field for details on how to write a LabelElement subclass.

The decode_as_* functions toward the end of the file are also part of this module's public interface. It is recommended to implement special field decoding in terms of these functions.

BaseLabelElement dataclass

BaseLabelElement(lpath: str = '/', _errors: list[tuple[str, str]] = list())

This class defines a few properties that are shared by all concrete LabelElement classes and that need to be visible to LabelMeta. Also see LabelElement itself.

DecodingError

DecodingError(lpath: str, message: str)

Bases: ValueError

Represents failure to decode a value from the YAML representation.

ExplicitNullT

Pythonic 'None' in a field's type always means "this field can be omitted", and in a field's value, always means "this field was omitted." Writing 'foo: null' in YAML never produces SomeLabelElement(foo=None).

However, in a very few places, 'null' is a legitimate value of a label field, and distinct from the field having been omitted. Those fields have ExplicitNullT as their declared type and, when 'null' (or 'undefined' or 'none', case insensitive) is the explicitly given value of that type, they will have the singleton ExplicitNull as their value.

LabelElement dataclass

LabelElement(lpath: str = '/', _errors: list[tuple[str, str]] = list())

Bases: BaseLabelElement

Common functionality for all label element classes.

Label element classes are dataclasses with a few additional features to facilitate loading from YAML. It is not necessary to annotate LabelElement subclasses with @dataclass; LabelElement's metaclass handles this for you.

Define data fields of each label class as you would for an ordinary dataclass. Use label_meta.special_field() instead of dataclasses.field(); special_field can do everything dataclasses.field can do, plus a few more things.

Every field must have a default value, even if that field is required to be present in a YAML label; it is needed to handle the situation where an erroneous label omits the field. Mark required fields using special_field(required=True, ...).

There are a few exceptions to "every field must have a default value". If a field's type is a LabelElement subclass, don't specify a default value for that type; the default will be an instance of that subclass with all fields set to their defaults. Also, as a convenience, if a field's type is a list or a dict, and you leave that field with no default, it will be treated the same as specifying list or dict as the default factory for that field. (In other words, the default will be a new empty list or empty dict.)

Many label element classes will need to override the _validate_label method; see that function's docstring for details.

children property

children: Iterator[LabelElement]

Returns a list of all the immediate LabelElement children of this element, flattening lists and dicts.

errors property

errors: dict[str, list[str]]

All validation errors observed for this element and its descendants, as a dictionary { path : [problems] }.

valid property

valid: bool

True if neither this element nor any of its descendants have any validation errors.

as_text

as_text() -> str

Produce a textual YAML serialization of self.

as_yaml

as_yaml() -> FieldNode

Produce the YAML representation tree corresponding to self.

from_blank classmethod

from_blank(lpath: str = '/') -> Self

Construct an instance of CLS from default values for all its fields. The result might have errors.

from_text classmethod

from_text(text: str, lpath: str = '/') -> Self

Parse a YAML-format label from the string TEXT.

from_yaml classmethod

from_yaml(raw_spec: FieldNode, lpath: str = '/') -> Self

Construct an instance of CLS from a YAML MappingNode, RAW_SPEC. MappingNodes are produced by yaml.compose() from key-value mappings in YAML documents.

This function expects that the only YAML tags in the entire tree rooted at RAW_SPEC are tag:yaml.org,2002:{map,seq,str}, used for all MappingNodes, SequenceNodes, and ScalarNodes, respectively. With PyYAML, this is what you get if you pass 'Loader=yaml.BaseLoader' to yaml.compose.

LPATH is the path from the label's root element to the element to be constructed.

LabelMeta

Bases: type

Metaclass to be applied to LabelElement. See LabelElement for documentation.

adjust_field_default

adjust_field_default(ty: FieldType, default: Any) -> dataclasses.Field[Any]

Given a LabelElement field type TY and its default value DEFAULT (which may be a dataclasses.Field instance, and should be dataclasses.MISSING if there wasn't any default value), return an adjusted default value.

This implements the features that unlike a regular dataclass, you can write "some_field: dict = {}" as shorthand for "some_field: dict = dataclasses.field(default_factory = dict)",

This also verifies that default values are present for all fields, except when the field directly holds a LabelElement and nothing else, or when the field is a list or dict of something (in which cases a suitable default is provided).

Finally, default values that aren't already dataclass.Field objects are wrapped in dataclass.Field objects, and our special metadata properties are filled in.

decode_as_bool

decode_as_bool(val: FieldNode, lpath: str) -> bool

Decode YAML value 'val' as a boolean (true or false).

decode_as_date

decode_as_date(val: FieldNode, lpath: str) -> date

Decode YAML value 'val' as a date.

decode_as_dict

decode_as_dict(val: FieldNode, lpath: str, *, decode_kv_key: Callable[[FieldNode, str], U], decode_kv_val: Callable[[FieldNode, str], T]) -> dict[U, T]

Decode YAML value 'val' as a dict whose keys are of type U and whose values are of type T, as defined by the decode_kv_key and decode_kv_val hooks. We require that U is scalar.

decode_as_element

decode_as_element(val: FieldNode, lpath: str, *, element: type[LE]) -> LE

Decode YAML value 'val' as the concrete LabelElement subclass 'element'.

decode_as_explicit_null

decode_as_explicit_null(val: FieldNode, lpath: str) -> ExplicitNullT

Decode YAML value 'val' as an explicit null.

decode_as_float

decode_as_float(val: FieldNode, lpath: str) -> float

Decode YAML value 'val' as a floating point number.

decode_as_int

decode_as_int(val: FieldNode, lpath: str) -> int

Decode YAML value 'val' as an integer.

decode_as_itself

decode_as_itself(val: FieldNode, lpath: str) -> YAMLAny

Decode YAML value 'val' as itself. To put it another way, don't decode it at all.

decode_as_list

decode_as_list(val: FieldNode, lpath: str, *, decode_element: Callable[[FieldNode, str], T]) -> list[T]

Decode YAML value 'val' as a list of elements of type T, as defined by the decode_element hook.

decode_as_mapping

decode_as_mapping(val: FieldNode, lpath: str) -> list[tuple[FieldNode, FieldNode]]

Decode YAML value 'val' as a mapping.

decode_as_regex

decode_as_regex(val: FieldNode, lpath: str, *, adjust_pattern: Callable[[str], str] | None = None) -> Pattern[str]

Decode YAML value 'val' as a regex.

decode_as_scalar

decode_as_scalar(val: FieldNode, lpath: str) -> str

Decode YAML value 'val' as a scalar quantity.

decode_as_sequence

decode_as_sequence(val: FieldNode, lpath: str) -> list[FieldNode]

Decode YAML value 'val' as a sequence.

decode_as_str

decode_as_str(val: FieldNode, lpath: str) -> str

Decode YAML value 'val' as a string.

decode_label

decode_label(cls: type[LabelElement], raw_spec: FieldNode, lpath: str) -> tuple[dict[str, Any], list[tuple[str, str]]]

Perform generic validation and type conversion on YAML label input.

CLS must be a properly defined subclass of LabelElement. The only YAML tags in the entire tree rooted at RAW_SPEC must be tag:yaml.org,2002:{map,seq,str}, used for all MappingNodes, SequenceNodes, and ScalarNodes, respectively. With PyYAML, this is what you get if you pass 'Loader=yaml.BaseLoader' to yaml.compose.

Returns a pair (SPEC, ERRORS). SPEC is the result of validating and converting each field of RAW_SPEC in isolation, according to its type. ERRORS is a list of validation errors, each of which is a 2-tuple of strings (path, problem). If ERRORS is empty, the element is valid as far as this function can tell; the class's _validate_label hook may add more errors.

field_holds_child_attrs

field_holds_child_attrs(field: Field[Any]) -> bool

True if the field 'field' can hold LabelElement objects, either directly or as values in a list or dict.

make_decoder_for_type

make_decoder_for_type(field_type: FieldType, *, scalar_only: bool = False) -> Callable[[FieldNode, str], Any]

Return a function that decodes fields with type 'field_type'. This will either be one of the decode_as_* functions, or a specialization of one of them.

If 'scalar_only' is True, throws an exception if 'field_type' is not a scalar.

make_decoder_for_union

make_decoder_for_union(alts: Sequence[FieldType], *, scalar_only: bool = False) -> Callable[[FieldNode, str], Any]

Specialize 'decode_as_union' to parse a value as one of the alternatives given by ALTS. If SCALAR_ONLY is True, all of the alternatives must be scalar (not sequences or mappings).

special_field

special_field(*, required: bool = False, decode_value: Callable[[FieldNode, str], Any] | None = None, decode_with_spec: Callable[[FieldNode, str, dict[str, Any]], Any] | None = None, **kwargs: Any) -> Any

This function wraps dataclasses.field and adds some additional ways fields can have special properties. Use it in preference to dataclasses.field when defining LabelElements, even if you don't need any of its special options.

Set required=True when a field is required to be present in a YAML label. You must still specify a default value, except as noted in the docstring for LabelElement; this value will be used when a label, erroneously, leaves the field undefined. Example:

class TimeInfo(LabelElement):
    observation_start_date: date | None = None
    delivery_start_date: date | None = special_field(
        required=True, default=None
    )

Set decode_value= when you need to override the default type-based rules for decoding a field from YAML, but you don't need any extra information to decide how to decode the field. This callback receives the same arguments as are expected by the decode_as_ family of functions defined at the bottom of this file, and should return the decoded value or throw a DecodingError (not a plain ValueError!)

Set decode_with_spec= when you need to look at the rest of the spec for the LabelElement object to determine how to decode the field from YAML. This callback receives all the same arguments as the ordinary decode_value callback, plus one more: the partially decoded spec object. All the fields that use the default decoding rules, or plain decode_value callbacks, will be decoded first, so when writing decode_with_spec callbackes you can assume, for instance, that spec["boolean_field"] has a boolean value, not a string. Example:

class ColumnObject(LabelElement):
    repeated: bool = False
    name_regex: bool = False
    name: str | re.Pattern[str] = special_field(
        required = True,
        default = "<name missing>",
        decode_with_spec = lambda val, lpath, spec: (
            label_meta.decode_as_regex(val, lpath)
                if spec["repeated"] or spec["name_regex"] else
            label_meta.decode_as_str(val, lpath)
        )
    )

to_yaml_repr

to_yaml_repr(datum: Any) -> FieldNode

Convert a LabelElement, or any of the primitive Python data types that might be contained in a LabelElement, to a YAML representation tree that would be decoded back to the original by LabelElement.from_yaml or by the appropriate decode_as_* function.