Skip to content

FoSpy.blocks.attachments

This site only contains documentation for the Block subclasses defined in this module. For a complete reference of all functions, classes, and variables, see the full documentation.

Block Types in this Module


Attachment

Bases: SingleBlock

Methods:

Name Description
TemplateClass

Create a template for a subclass of SingleBlock.

__delattr__
__eq__

Check equality of two SingleBlock objects.

__getattr__

Check both self and self.ext for attribute before returning.

__hash__
__init__
__new__
__setattr__
_assign_and_inject

Attaches attributes and methods to any value before assigning it as an

_get_filepath

Default behavior: Must be overwritten in subclasses.

_meta_to_front

Moves metadata to the front of _key_order. Metadata will always be

_rename_validators

Realigns any renamed

_resolve_relative_path

Resolves a relative object path string into an object or function.

_subprocess
_update_src
_validate_filename
add_all_calc_routines

Schedule all available calculation routines.

add_block

Adds an unexpected attribute with a validator mapped by type_alias.

add_calc_comment

Add a calculated comment to be injected during serialization.

add_calc_routine

Schedules a calculated comment.

add_comments

Default behavior to be overwritten when attached to a parent block.

add_dispatch
build_req_validators

Builds required keys and validators mapped to subclass.

build_validators

Builds expected keys and validators mapped to subclass.

clear_all_comments
clear_comments

Clear comments attached to top-level attributes only.

copy

Returns a deep-copy of self by serializing and reconstructing.

default_key_order

Set to default attribute order for serialization.

dispatch_subclass
dispatcher

Decorate a classmethod to dispatch to other subclasses.

enforce_subtype
fill_staged_template
find_attachments
find_fileblock

Finds the parent file object.

find_tempdir

Find the parent file object's temporary directory.

find_temppath

Find the parent file object's temporary directory path.

get_id

Returns an easily recognizable identifier for self. Non-unique.

get_parent_prop
get_prop_dict

Returns a dictionary mapping property names to their live object values.

get_prop_path
get_req_validators

Overrides class validators with any renamed properties.

get_validators

Overrides class validators with any renamed properties.

has_staged
inject_defaults
inspect
key_to_idx

Reorder attributes for serialization.

keys_to_end

Reorder attributes for serialization.

keys_to_front

Reorder attributes for serialization.

list_avail_routines

Lists all calc routines available to be added to self._calc_routines.

make_template

Converts self into a template of its original subclass.

print_summary
reflex
refresh_attachments
register_dispatch
rename_block
rename_dict
serialize

Return a recursively serialized dict representation of self.

set_dispatch
setup_dispatch

Decorate a class to dispatch to other classes during construction.

stage_template
to_json

Converts self into a JSON-formatted string or file.

track_attachments
Source code in FoSpy/blocks/attachments.py
@SingleBlock.setup_dispatch(from_key="_extension", allow_self=False)
class Attachment(SingleBlock):
    _id_key = "file_name"  
    def __init__(self, blockDict, **kwargs):
        super().__init__(blockDict, **kwargs)
        self._filepath = None

    def __setattr__(self, name, value):
        if name == "_extension":
            if value is None:
                return
            if hasattr(self, "_extension") and value != self._extension:
                from warnings import warn
                warn("You cannot change the extension of an attachment after construction. Skipping change.", RuntimeWarning)
                return

        if name == "file_name":
            old_ext = self._extension if hasattr(self, "_extension") else None
            value, new_ext = self._validate_filename(value, old_ext)
            self._extension = new_ext

        return super().__setattr__(name, value)

    @classmethod
    def _validate_filename(cls, filename:str, ext:str=None, warn=True):
        filename = str(filename)
        if ext is None:
            ext = f".{filename.rsplit('.')[-1]}" if "." in filename else ""
            # delegate to base validator routine to verify extension
            return filename, ext

        if "." not in filename:
            new_ext = ext
        else:
            new_ext = f".{filename.rsplit('.')[-1]}"

        if new_ext != ext:
            if warn:
                filename = filename + ext
                from warnings import warn
                warn(f"New filename contains a different extension: '{new_ext}'. Extensions cannot "
                    f"be changed after construction. The current extension ('{ext}') "
                    f"will be appended to the new filename to form: '{filename}'.", RuntimeWarning)
            else:
                raise ValueError(f"New filename contains a different extension: '{new_ext}'. Extensions cannot "
                                 "be changed after attachment construction.")

        return filename, new_ext

    @classmethod
    def reflex(cls, serialize=True, clean=False, **kwargs:dict):
        from .template import TemplateField
        if "file_name" not in kwargs:
            kwargs["file_name"] = TemplateField.serialize()
            kwargs.pop("path", None)
            add_embedded = "embedded" not in kwargs

        elif not any(k in kwargs for k in ("path", "embedded")):
            add_embedded = True

        if add_embedded:
            kwargs["embedded"] = TemplateField.serialize()

        return super().reflex(serialize=serialize, clean=clean, **kwargs)



    def _get_filepath(self):
        """
        Default behavior: Must be overwritten in subclasses.
        """
        raise AttachmentTypeError("Attachments must be constructed as a subclass with a set_filepath method.")


    @classmethod
    def enforce_subtype(cls, subcls, **kwargs):
        raise DeprecationWarning("Attachments no longer enforce subtype through this method. "
                                 "Simply spec the validator as the enforced subtype instead.")

    @classmethod
    def add_dispatch(cls, blockDict, dispatch_key, **kwargs):
        from .. import _errors as err

        # make sure wrapped
        _ = SingleBlock.add_dispatch(blockDict, dispatch_key, **kwargs)

        if "file_name" not in blockDict:
            raise err.MissingPropertyError("file_name", cls, blockDict=blockDict)

        _, ext = cls._validate_filename(blockDict["file_name"])

        return {dispatch_key: ext}

    @classmethod
    def register_dispatch(cls, registry_val, **kwargs):
        extension = registry_val or ".txt"
        fn = "attachment"+extension
        return super().register_dispatch(registry_val, setup_from_key="_location",
                                         setup_allow_self=False, defaults={"file_name":fn},
                                         inherit_dispatch=True,
                                         **kwargs)

    def find_attachments(self):
        attachments = super().find_attachments()
        if self not in attachments:
            attachments.append(self)

        return attachments

_aliases = new_als class-attribute instance-attribute

_calc_comments = {} instance-attribute

_calc_routines = [] instance-attribute

_constructed = True instance-attribute

_filepath = None instance-attribute

_id_key = 'file_name' class-attribute instance-attribute

_key_order = [] instance-attribute

_key_overrides = {} instance-attribute

_meta = SubContainer() instance-attribute

_reserved = ['ext'] instance-attribute

_sourceDict = blockDict.copy() instance-attribute

_staged_templates = {} instance-attribute

dispatch = {} class-attribute instance-attribute

dispatch_allow_self = True class-attribute instance-attribute

dispatch_default = None class-attribute instance-attribute

dispatch_key = None class-attribute instance-attribute

ext = SubContainer() instance-attribute

rename = rename instance-attribute

TemplateClass(*args) classmethod

Create a template for a subclass of SingleBlock.

Generates a hybridized subclass of the current block class and TemplateBlock. Template subclasses override original expected validators with either a TemplateField, TemplateBlock, or TemplateList depending on the type of the original validator.

Parameters:

Name Type Description Default
*args str

A list of properties to override as template types.

()
Source code in FoSpy/blocks/blocks.py
@classmethod
def TemplateClass(cls,*args:str):
    """
    Create a template for a subclass of `SingleBlock`.

    Generates a hybridized subclass of the current block class and
    [`TemplateBlock`][FoSpy.blocks.template.TemplateBlock]. Template
    subclasses override original expected validators with either a
    [`TemplateField`][FoSpy.blocks.template.TemplateField],
    [`TemplateBlock`][FoSpy.blocks.template.TemplateBlock], or
    [`TemplateList`][FoSpy.blocks.template.TemplateList] depending on the
    type of the original validator.

    Args:
        *args: A list of properties to override as template types.
    """
    from .template import TemplateBlock, FlexTemplate

    cls_registry = TemplateBlock.__dispatch__["registry"]

    if cls not in cls_registry:

        @TemplateBlock.register_dispatch(cls, setup_from_key="_fields", setup_allow_self=True, inherit_dispatch=True)
        class TemplateLocator(FlexTemplate, TemplateBlock, cls):
            _full_class = cls

        TemplateLocator.__name__ = f"{cls.__name__}TemplateLocator"
        TemplateLocator.__qualname__ = f"{cls.__name__}.TemplateClass.Locator"
        TemplateLocator.__module__ = cls.__module__

    fields = tuple(sorted(args))

    # construct a proxy dictionary that will correctly dispatch to the right
    # template class in TemplateBlock's dispatch chain.
    proxy_dict = {
        "__dispatch__": {
            "_full_class": cls,
            "_fields": fields
        }
    }

    return TemplateBlock.dispatch_subclass(proxy_dict)

__delattr__(attr)

Source code in FoSpy/blocks/blocks.py
def __delattr__(self, attr):
    if attr in self.get_req_validators():
        raise AttributeError(f"Cannot delete property: '{attr}'. It is registered as a required property for this object.")
    return super().__delattr__(attr)

__eq__(other, suppress_routine_paths=False)

Check equality of two SingleBlock objects.

Equality is checked by a deep difference of their serialized dictionaries.

Parameters:

Name Type Description Default
suppress_routine_paths bool

Optional flag to still return true if the only differences found are in calculation routine metadata. Calculation routines are for user information only and may not be relevant for equality.

False
Source code in FoSpy/blocks/blocks.py
def __eq__(self, other, suppress_routine_paths:bool=False):
    """
    Check equality of two `SingleBlock` objects.

    Equality is checked by a deep difference of their
    [serialized][FoSpy.blocks.blocks.SingleBlock.serialize] dictionaries.

    Args:
        suppress_routine_paths:
            Optional flag to still return true if the only differences found
            are in [calculation
            routine][FoSpy.blocks.blocks.SingleBlock.add_calc_routine]
            metadata. Calculation routines are for user information only and
            may not be relevant for equality.
    """
    from .._debug import deep_diff as dd, _debug as db
    try:
        db.msg("Serializing Blocks to check equality:", module = "SingleBlock.__eq__()")
        diffs = dd(self.serialize(), other.serialize(), suppress_routine_paths=suppress_routine_paths)
        passed = len(diffs) == 0
        if not passed:
            db.pmsg(diffs,module = "SingleBlock.__eq__()")
        return passed
    except Exception as e:
        db.msg(f"Equality failed by exception: {e}",module = "SingleBlock.__eq__()")
        return False

__getattr__(name)

Check both self and self.ext for attribute before returning.

A matching attribute of self will be returned first, but if self has no matching attribute, a matching attribute of self.ext can be returned instead.

Source code in FoSpy/blocks/blocks.py
def __getattr__(self, name:str):
    """
    Check both `self` and `self.ext` for attribute before returning.

    A matching attribute of `self` will be returned first, but if `self` has
    no matching attribute, a matching attribute of `self.ext` can be
    returned instead.
    """

    try:
        if name not in ("rename", "ext") and hasattr(self, "rename"):
            rename_dict = self.rename.serialize(shallow=True, clean=True)
            if name in rename_dict:
                return getattr(self, rename_dict[name])

        if name != 'ext':
            return getattr(self.ext, name)

        raise AttributeError()
    except AttributeError:
        raise AttributeError(
            f"{type(self).__name__} object "
            f"has no attribute {name!r}."
        )

__hash__()

Source code in FoSpy/blocks/blocks.py
def __hash__(self):
    return id(self)

__init__(blockDict, **kwargs)

Source code in FoSpy/blocks/attachments.py
def __init__(self, blockDict, **kwargs):
    super().__init__(blockDict, **kwargs)
    self._filepath = None

__new__(blockDict, *args, **kwargs)

Source code in FoSpy/blocks/blocks.py
def __new__(cls, blockDict, *args, **kwargs):
    _dispatched = kwargs.pop("_dispatched", False)
    if _dispatched:
        # blockDict should always be dict after dispatch. I want to see attributeerror if not.
        blockDict.pop("__dispatch__",None)
        return super().__new__(cls)

    blockDict = _unwrap_block(blockDict)

    dispatched_cls = cls.dispatch_subclass(blockDict, *args, **kwargs)

    if issubclass(dispatched_cls, cls):
        return dispatched_cls(blockDict, *args, _dispatched=True, **kwargs)

    dispatch = blockDict.pop("__dispatch__")
    raise err.BlockDispatchError(
        f"Attempted to construct the following dictionary as a {cls.__name__} block, "
        f"but it was dispatched to a {dispatched_cls.__name__} instead."
        f"\n\nINPUT:\n{blockDict}"
        f"\n\nDISPATCH:\n{dispatch}")

__setattr__(name, value)

Source code in FoSpy/blocks/attachments.py
def __setattr__(self, name, value):
    if name == "_extension":
        if value is None:
            return
        if hasattr(self, "_extension") and value != self._extension:
            from warnings import warn
            warn("You cannot change the extension of an attachment after construction. Skipping change.", RuntimeWarning)
            return

    if name == "file_name":
        old_ext = self._extension if hasattr(self, "_extension") else None
        value, new_ext = self._validate_filename(value, old_ext)
        self._extension = new_ext

    return super().__setattr__(name, value)

_assign_and_inject(name, value, extended=False)

Attaches attributes and methods to any value before assigning it as an attribute of self or self.ext.

Attributes Attached to Object

_parent_block: refers to self

Methods Attached to Object

add_comments_to_parent clear_comments_from_parent

Source code in FoSpy/blocks/blocks.py
def _assign_and_inject(self, name, value, extended=False):
    """
    Attaches attributes and methods to any value before assigning it as an
    attribute of `self` or `self.ext`.

    Attributes Attached to Object:
        `_parent_block`: refers to `self`

    Methods Attached to Object:
        [`add_comments_to_parent`][FoSpy.blocks.blocks._add_comments_to_parent]
        [`clear_comments_from_parent`][FoSpy.blocks.blocks._clear_comments_from_parent]
    """
    from .attachments import Attachment

    if name == 'ext':
        return super().__setattr__('ext', value)
    if not hasattr(value, "__dict__"):
        value = SimpleWrapper(value)

    if extended:
        setattr(self.ext, name, value)
    else:
        super().__setattr__(name, value)

    attr_obj = getattr(self.ext if extended else self, name)

    setattr(attr_obj, "_parent_block", self)

    if isinstance(attr_obj, Attachment):
        attr_obj._get_filepath()
    elif hasattr(attr_obj, "refresh_attachments"):
        attr_obj.refresh_attachments()

    methods = ((_add_comments_to_parent(name), "add_comments"),
            (_clear_comments_from_parent(name), "clear_comments"))

    attr_obj._reserved = ['ext'] if not hasattr(attr_obj,"_reserved") else attr_obj._reserved
    for method, method_name in methods:
        attr_obj._reserved.append(method_name)
        bound = method.__get__(attr_obj, type(attr_obj))
        setattr(attr_obj, method_name, bound)

    self._props_changed = True

_get_filepath()

Default behavior: Must be overwritten in subclasses.

Source code in FoSpy/blocks/attachments.py
def _get_filepath(self):
    """
    Default behavior: Must be overwritten in subclasses.
    """
    raise AttachmentTypeError("Attachments must be constructed as a subclass with a set_filepath method.")

_meta_to_front()

Moves metadata to the front of _key_order. Metadata will always be serialized first, but being elsewhere in the order leads to unexpected results when moving other keys to desired indices.

Source code in FoSpy/blocks/blocks.py
def _meta_to_front(self):
    """
    Moves metadata to the front of `_key_order`. Metadata will always be
    serialized first, but being elsewhere in the order leads to unexpected
    results when moving other keys to desired indices.
    """
    try:
        meta_idx =self._key_order.index("metadata")
        self._key_order.pop(meta_idx)
    # TODO: Better handling
    except Exception:
        pass
    self._key_order.insert(0,"metadata")

_rename_validators(validators)

Realigns any renamed attributes with their expected validator.

Parameters:

Name Type Description Default
validators dict

A dictionary mapping attribute names to validators, returned by either build_validators or build_req_validators

required
Source code in FoSpy/blocks/blocks.py
def _rename_validators(self, validators:dict):
    """
    Realigns any [renamed][FoSpy.blocks.blocks.SingleBlock.rename_block]
    attributes with their expected validator.

    Args:
        validators:
            A dictionary mapping attribute names to validators, returned by
            either
            [`build_validators`][FoSpy.blocks.blocks.SingleBlock.build_validators]
            or
            [`build_req_validators`][FoSpy.blocks.blocks.SingleBlock.build_req_validators]
    """
    if hasattr(self, "rename"):
        for name, rename in self.rename.serialize(shallow=True, clean=True).items():
            if name in validators and rename not in validators:
                val = validators.pop(name)
                validators[rename] = val
    return validators

_resolve_relative_path(path)

Resolves a relative object path string into an object or function.

Example:

    mySyn._resolve_relative_path("materials[1].ratio")
    ## returns mySyn.materials[1].ratio

Source code in FoSpy/blocks/blocks.py
def _resolve_relative_path(self, path: str):
    """
    Resolves a relative object path string into an object or function.

    Example:
    ```
        mySyn._resolve_relative_path("materials[1].ratio")
        ## returns mySyn.materials[1].ratio
    ```
    """
    import re

    _index_re = re.compile(r"^([A-Za-z_]\w*)\[(\d+)\]$")
    obj = self

    for part in path.split("."):

        # Case: attr[index]
        m = _index_re.match(part)
        if m:
            attr_name, idx_str = m.groups()
            idx = int(idx_str)

            # Get the ListBlock
            obj = getattr(obj, attr_name)

            # Index into its _objs
            obj = obj._objs[idx]
            continue

        # Case: simple attribute
        obj = getattr(obj, part)

    return obj

_subprocess(target, args=(), **kwargs)

Source code in FoSpy/blocks/blocks.py
def _subprocess(self, target, args=(), **kwargs):
    from multiprocessing import Process

    if kwargs is None:
        kwargs={}

    p = Process(target=target, args=args, kwargs=kwargs)
    p.start()

_update_src()

Source code in FoSpy/blocks/blocks.py
def _update_src(self):
    if self._constructed and self._props_changed:
        self._sourceDict = self.serialize(clean=True)
        self._props_changed = False

    return self._sourceDict   

_validate_filename(filename, ext=None, warn=True) classmethod

Source code in FoSpy/blocks/attachments.py
@classmethod
def _validate_filename(cls, filename:str, ext:str=None, warn=True):
    filename = str(filename)
    if ext is None:
        ext = f".{filename.rsplit('.')[-1]}" if "." in filename else ""
        # delegate to base validator routine to verify extension
        return filename, ext

    if "." not in filename:
        new_ext = ext
    else:
        new_ext = f".{filename.rsplit('.')[-1]}"

    if new_ext != ext:
        if warn:
            filename = filename + ext
            from warnings import warn
            warn(f"New filename contains a different extension: '{new_ext}'. Extensions cannot "
                f"be changed after construction. The current extension ('{ext}') "
                f"will be appended to the new filename to form: '{filename}'.", RuntimeWarning)
        else:
            raise ValueError(f"New filename contains a different extension: '{new_ext}'. Extensions cannot "
                             "be changed after attachment construction.")

    return filename, new_ext

add_all_calc_routines(recursive=False)

Schedule all available calculation routines.

Adds all available calc_routines to self._calc_routines using list_avail_routines() and add_calc_routine().

Parameters:

Name Type Description Default
recursive bool

Optional recursion. See SingleBlock.list_avail_routines()

False
Source code in FoSpy/blocks/blocks.py
def add_all_calc_routines(self, recursive:bool=False):
    """
    Schedule all available calculation routines.

    Adds all available calc_routines to `self._calc_routines` using
    [`list_avail_routines()`][FoSpy.blocks.blocks.SingleBlock.list_avail_routines]
    and
    [`add_calc_routine()`][FoSpy.blocks.blocks.SingleBlock.add_calc_routine].

    Args:
        recursive:
            Optional recursion. See `SingleBlock.list_avail_routines()`
    """
    for path in self.list_avail_routines(recursive=recursive, abbreviated=False):
        self.add_calc_routine(path)

add_block(block_name, type_alias, value=[])

Adds an unexpected attribute with a validator mapped by type_alias. Unexpected attributes not requiring a validator can be set directly without using this method.

Parameters:

Name Type Description Default
block_name str

new unexpected attribute name

required
type_alias str

Alias mapped to the desired validator in parsing.validation.aliases. For more information on how aliases are used, see __setattr__.

required
Source code in FoSpy/blocks/blocks.py
def add_block(self, block_name:str, type_alias:str, value=[]):
    """
    Adds an unexpected attribute with a validator mapped by `type_alias`.
    Unexpected attributes not requiring a validator can be set directly
    without using this method.

    Args:
        block_name: new unexpected attribute name
        type_alias:
            Alias mapped to the desired validator in
            [`parsing.validation.aliases`][FoSpy.parsing.validation.aliases].
            For more information on how aliases are used, see
            [`__setattr__`][FoSpy.blocks.blocks.SingleBlock.__setattr__].
    """
    if hasattr(self,block_name):
        raise ValueError(f"This object already has attribute: '{block_name}'.")
    return setattr(self, f"{block_name}${type_alias}", value)

add_calc_comment(key, comment, calc_id)

Add a calculated comment to be injected during serialization.

WARNING: This function can leave outdated calculations in comments after serialization. Recommended to use add_calc_routine() instead.

Calculated comments are for user information and will be formatted to be skipped by the parser when reading the file. This is useful for comments that should be recalculated and refreshed during saving/serialization, like weight percentages or summaries.

Parameters:

Name Type Description Default
key str

attribute to attach the calculated comment to. Comments appear above their attached attributes in FOS format.

required
comment str

comment text without comment formatting (don't include // or !)

required
calc_id str

unique identifier for the calculated comment. If it matches an existing comment (like when refreshing a value), the comment is overwritten

required
Source code in FoSpy/blocks/blocks.py
def add_calc_comment(self, key:str, comment:str, calc_id:str):
    """
    Add a calculated comment to be injected during serialization.

    WARNING: This function can leave outdated calculations in comments after
    serialization. Recommended to use `add_calc_routine()` instead.

    Calculated comments are for user information and will be formatted to be
    skipped by the parser when reading the file. This is useful for comments
    that should be recalculated and refreshed during saving/serialization,
    like weight percentages or summaries.

    Args:
        key:
            attribute to attach the calculated comment to. Comments appear
            above their attached attributes in FOS format.
        comment:
            comment text without comment formatting (don't include // or !)
        calc_id:
            unique identifier for the calculated comment. If it matches an
            existing comment (like when refreshing a value), the comment is
            overwritten

    """
    calc_comments = self._calc_comments.get(key, {})
    self._calc_comments[key] = calc_comments
    self._calc_comments[key][calc_id]=comment

add_calc_routine(path, **kwargs)

Schedules a calculated comment.

Appends a _calc_routine()-decorated function to self._calc_routines to be run at serialization.

Used to add calculated comments that should be refreshed during serialization.

Parameters:

Name Type Description Default
path str

a relative path string that can be resolved into a _calc_routine()-decorated function

required
**kwargs any

optional key word arguments to be passed to the function at path.

{}

Raises:

Type Description
TypeError

the attr or method at path is not registered as a _calc_routine

Example:

    mySyn.add_calc_routine("materials.add_weight_pcts", typ="reagent")
    ## mySyn.materials.add_weight_pcts(typ="reagent") is now scheduled
    ## to run at serialization

Source code in FoSpy/blocks/blocks.py
def add_calc_routine(self, path:str, **kwargs):
    """
    Schedules a calculated comment.

    Appends a
    [`_calc_routine()`][FoSpy.blocks._blockUtils._calc_routine]-decorated
    function to `self._calc_routines` to be run at
    [serialization][FoSpy.blocks.blocks.SingleBlock.serialize].

    Used to add calculated comments that should be refreshed during
    serialization.

    Args:
        path:
            a relative path string that can be resolved into a
            `_calc_routine()`-decorated function
        **kwargs (any):
            optional key word arguments to be passed to the function at
            path.

    Raises:
        TypeError:
            the attr or method at path is not registered as a
            _calc_routine

    Example:
    ```
        mySyn.add_calc_routine("materials.add_weight_pcts", typ="reagent")
        ## mySyn.materials.add_weight_pcts(typ="reagent") is now scheduled
        ## to run at serialization
    ```
    """

    func = self._resolve_relative_path(path)
    if not getattr(func, "_is_calc_routine", False):
        raise TypeError(f"'{path}' is not a registered calc routine.")

    self._meta.routine_paths.append(path)

    def wrapped(f=func, k=kwargs):
        return f(**k)

    self._calc_routines.append(wrapped)

add_comments(*comments)

Default behavior to be overwritten when attached to a parent block.

If a SingleBlock is stored as an attribute of another SingleBlock, this method will be overwritten by the parent's __setattr__.

Source code in FoSpy/blocks/blocks.py
def add_comments(self, *comments):
    """
    Default behavior to be overwritten when attached to a parent block.

    If a `SingleBlock` is stored as an attribute of another `SingleBlock`,
    this method will be overwritten by the parent's `__setattr__`.
    """
    keys = list(self.get_req_validators())

    keys = [k for k in keys if k != "metadata"]
    fallback = [k for k in self._key_order if k != "metadata"]
    if not (keys or fallback):
        raise ValueError("This object has not been correctly attached to a parent block "
                         "and could not identify a required key to attach to.")

    first = keys[0] if keys else fallback[0]

    self._meta.comments.setdefault(first, [])
    for comment in comments:
        self._meta.comments[first].append(comment)

add_dispatch(blockDict, dispatch_key, **kwargs) classmethod

Source code in FoSpy/blocks/attachments.py
@classmethod
def add_dispatch(cls, blockDict, dispatch_key, **kwargs):
    from .. import _errors as err

    # make sure wrapped
    _ = SingleBlock.add_dispatch(blockDict, dispatch_key, **kwargs)

    if "file_name" not in blockDict:
        raise err.MissingPropertyError("file_name", cls, blockDict=blockDict)

    _, ext = cls._validate_filename(blockDict["file_name"])

    return {dispatch_key: ext}

build_req_validators() classmethod

Builds required keys and validators mapped to subclass.

Walks all parent classes and builds a map of all keys that are required during __init__, and their respective validation routines. Subclasses are mapped to expected keys and validations in parsing.validation. Subclass validations override parent classes when applicable.

Returns:

Name Type Description
merged dict

Maps required keys to validation routines. Routines may be a class constructor or a func taking one arg.

Example:

>>> SingleBlock.build_req_validators()
{
    "name": str,
    "type": str,
    "formula": ChemFormula, # class constructor
    "supplier": str,
    "cas": str,
    "form": str,
    "env": str,
    "ratio": validators.material.ratio # validator function
}

Source code in FoSpy/blocks/blocks.py
@classmethod
def build_req_validators(cls):
    """
    Builds required keys and validators mapped to subclass.

    Walks all parent classes and builds a map of all keys that are required
    during `__init__`, and their respective validation routines. Subclasses
    are mapped to expected keys and validations in
    [`parsing.validation`][FoSpy.parsing.validation]. Subclass validations
    override parent classes when applicable.

    Returns:
        merged (dict):
            Maps required keys to validation routines. Routines may be
            a class constructor or a func taking one arg.
    Example:
        ``` 
        >>> SingleBlock.build_req_validators()
        {
            "name": str,
            "type": str,
            "formula": ChemFormula, # class constructor
            "supplier": str,
            "cas": str,
            "form": str,
            "env": str,
            "ratio": validators.material.ratio # validator function
        }
        ```
    """
    from ..parsing.validation import required_keys
    from ._blockUtils import _get_prop_mro, _merge_vals
    merged = {}
    # mro = list(reversed(cls.__mro__))
    # for i, base in enumerate(mro):
    #     base_reqs = required_keys.get(base,{})
    #     for key, validator in base_reqs.items():
    #         # allow subclasses to remove parent requirements.
    #         if not validator:
    #             merged.pop(key, None)
    #         else:
    #             merged[key] = validator

    req_mro = _get_prop_mro(cls, required_keys)
    for i in range(len(req_mro)):
        merged = _merge_vals(merged, req_mro, i)

    merged.pop("__all__")

    return merged

build_validators() classmethod

Builds expected keys and validators mapped to subclass.

Walks all parent classes and builds a map of all keys that are expected (required or optional), and their respective validation routines. Subclasses are mapped to keys and validations in parsing.validation. Subclass validations override parent classes when applicable.

See build_req_validators

Source code in FoSpy/blocks/blocks.py
@classmethod
def build_validators(cls):
    """
    Builds expected keys and validators mapped to subclass.

    Walks all parent classes and builds a map of all keys that are expected
    (required or optional), and their respective validation routines.
    Subclasses are mapped to keys and validations in
    [`parsing.validation`][FoSpy.parsing.validation]. Subclass validations
    override parent classes when applicable.

    See
    [`build_req_validators`][FoSpy.blocks.blocks.SingleBlock.build_req_validators]
    """
    from ..parsing.validation import required_keys, optional_keys
    from ._blockUtils import _merge_vals, _get_prop_mro
    from .._docs.properties import _validator_rules
    merged = {}
    # for base in reversed(cls.__mro__):
    #     for key_set in (required_keys, optional_keys):
    #         base_reqs = key_set.get(base,{})
    #         for key, validator in base_reqs.items():
    #             # allow subclasses to remove parent requirements.
    #             if validator is False:
    #                 merged.pop(key, None)
    #             else:
    #                 merged[key] = validator
    req_mro = _get_prop_mro(cls, required_keys)
    opt_mro = _get_prop_mro(cls, optional_keys)

    for i in range(len(req_mro)): # req_mro and opt_mro are the same length
        merged = _merge_vals(merged, req_mro, i)
        merged = _merge_vals(merged, opt_mro, i)

    universal_val = merged.pop("__all__")

    @_validator_rules(inherit_from=universal_val)
    def universal_val_method(cls, *_, _m=universal_val, **__):
        return _m(*_, **__)

    cls.universal_val = universal_val_method

    return merged

clear_all_comments()

Source code in FoSpy/blocks/blocks.py
def clear_all_comments(self):
    self._meta.comments = {}
    for attr, val in self.__dict__.items():
        if attr.startswith("_") or attr in self._reserved:
            continue
        if hasattr(val, "clear_all_comments"):
            val.clear_all_comments()

clear_comments()

Clear comments attached to top-level attributes only.

Source code in FoSpy/blocks/blocks.py
def clear_comments(self):
    """
    Clear comments attached to top-level attributes only.
    """
    self._meta.comments = {}

copy()

Returns a deep-copy of self by serializing and reconstructing.

_calc_comments are not preserved during copy, but _calc_routines are. This prevents mutation of the comments when reconstructing.

Source code in FoSpy/blocks/blocks.py
def copy(self):
    """
    Returns a deep-copy of `self` by serializing and reconstructing.

    _calc_comments are not preserved during copy, but _calc_routines are.
    This prevents mutation of the comments when reconstructing.
    """
    cls = type(self)
    # cache calculated comments before serializing
    c_cmts = self._calc_comments.copy()

    serial = self.serialize(keepListType=True)

    new_obj =  cls(serial)

    # restore cached calc comments
    self._calc_comments = c_cmts

    return new_obj

default_key_order(deep=False)

Set to default attribute order for serialization.

Rearrange attribute order to the default order assigned by build_validators

Parameters:

Name Type Description Default
deep bool

When true, recursively calls default_key_order on any other SingleBlock objects stored in attributes.

False
Source code in FoSpy/blocks/blocks.py
def default_key_order(self, deep:bool=False):
    """
    Set to default attribute order for serialization.

    Rearrange attribute order to the default order assigned by
    [`build_validators`][FoSpy.blocks.blocks.SingleBlock.build_validators]

    Args:
        deep:
            When true, recursively calls `default_key_order` on any other
            `SingleBlock` objects stored in attributes.
    """
    new_order = []
    for key in self.get_validators():
        if key != "ext" and key in self.serialize(shallow=True):
            new_order.append(key)
    for key in self._key_order:
        if key not in new_order:
            new_order.append(key)
    self._key_order = new_order
    self._meta_to_front()

    if deep:
        for name, obj in self.__dict__.items():
            if not name.startswith("_") and hasattr(obj, "default_key_order"):
                obj.default_key_order(deep=True)

dispatch_subclass(*args, **kwargs) classmethod

Source code in FoSpy/blocks/blocks.py
@classmethod
def dispatch_subclass(cls, *args, **kwargs):
    # fallback.
    # overridden by setup_dispatch decorator
    return cls

dispatcher(dispatch_method) staticmethod

Decorate a classmethod to dispatch to other subclasses.

Not normally used directly. See setup_dispatch decorator.

Source code in FoSpy/blocks/blocks.py
@staticmethod
def dispatcher(dispatch_method: Callable[..., dict])->classmethod:
    """
    Decorate a classmethod to dispatch to other subclasses.

    Not normally used directly. See
    [`setup_dispatch`][FoSpy.blocks.blocks.SingleBlock.setup_dispatch]
    decorator.
    """

    @classmethod
    def dispatch_subclass(cls:type[BlockType], blockDict, _add_defaults=False, **kwargs):
        from .. import _errors as err
        for_template = kwargs.get("for_template", False)

        block_dispatch = blockDict.setdefault("__dispatch__", {})
        visited = block_dispatch.setdefault("visited", [])
        cls_dispatch = getattr(cls, "__dispatch__", None)
        # shorthand for keying dispatch parameters
        d=cls_dispatch

        if d['dispatch_from'] in visited and (
            cls in visited or
            d is None or
            d['from_key'] is None):
            return cls
        try:
            blockDict = dispatch_method(cls, blockDict, add_defaults=_add_defaults, **kwargs)
        except Exception as e:
            if not for_template:
                raise e

        visited.append(cls)

        if d['dispatch_from'] not in visited:
            blockDict.pop("__dispatch__", None)
            return d['dispatch_from'].dispatch_subclass(blockDict, _add_defaults=_add_defaults, **kwargs)


        dispatch_val = block_dispatch.get(d['from_key'],
                            blockDict.get(d['from_key'], None))

        dispatched_cls = d['registry'].get(dispatch_val, d['registry'].get(None, cls))

        if dispatched_cls is cls and not d['allow_self']:
            if not for_template:
                raise err.BlockDispatchError(
                    f"The following blockDict was dispatched to {cls.__name__} "
                    "but could not be dispatched further. "
                    f"{cls.__name__} blocks are not allowed without a subclass.")
            return cls

        return dispatched_cls.dispatch_subclass(blockDict, **kwargs)
    return dispatch_subclass

enforce_subtype(subcls, **kwargs) classmethod

Source code in FoSpy/blocks/attachments.py
@classmethod
def enforce_subtype(cls, subcls, **kwargs):
    raise DeprecationWarning("Attachments no longer enforce subtype through this method. "
                             "Simply spec the validator as the enforced subtype instead.")

fill_staged_template(prop_name, **kwargs)

Source code in FoSpy/blocks/blocks.py
def fill_staged_template(self, prop_name, **kwargs):
    from .template import TemplateBlock

    prop_key = prop_name.split("$")[0] if "$" in prop_name else prop_name

    template = self._staged_templates.pop(prop_key, None)
    if template is None:
        prop_name, _ = self.stage_template(prop_name)
        return self.fill_staged_template(prop_name, **kwargs)

    prop_name = prop_key

    filled = template.fill(staged=True,**kwargs)

    if isinstance(filled, TemplateBlock):
        return self.stage_template(prop_name, filled)

    try:
        setattr(self, prop_name, filled)
    except Exception as e:
        raise Exception(f"Template was filled but could not be assigned {prop_name}") from e

    filled = getattr(self, prop_name)

    if isinstance(self, TemplateBlock):
        self.fill()

    return prop_name, filled

find_attachments()

Source code in FoSpy/blocks/attachments.py
def find_attachments(self):
    attachments = super().find_attachments()
    if self not in attachments:
        attachments.append(self)

    return attachments

find_fileblock()

Finds the parent file object.

Walks upward through _parent_block attributes until a FileBlock instance is found and returns that instance.

Source code in FoSpy/blocks/blocks.py
def find_fileblock(self):
    """
    Finds the parent file object.

    Walks upward through `_parent_block` attributes until a
    [`FileBlock`][FoSpy.blocks.files.FileBlock] instance is found and
    returns that instance.
    """
    from .files import FileBlock
    from .._errors import FileBlockNotFoundError

    blk = self
    while blk is not None:
        if isinstance(blk, FileBlock):
            return blk
        if hasattr(blk,"_parent_block"):
            blk = blk._parent_block
        else:
            blk = None
    raise FileBlockNotFoundError("Could not find a FileBlock containing the current object")

find_tempdir()

Find the parent file object's temporary directory.

Finds the temporary directory created by the FileBlock instance containing this block as one of its attributes.

Returns:

Name Type Description
tempdir tempfile.TemporaryDirectory

The temporary directory created by the parent file object

Source code in FoSpy/blocks/blocks.py
def find_tempdir(self):
    """
    Find the parent file object's temporary directory.

    Finds the temporary directory created by the
    [`FileBlock`][FoSpy.blocks.files.FileBlock] instance containing this
    block as one of its attributes. 

    Returns:
        tempdir (tempfile.TemporaryDirectory):
            The temporary directory created by the parent file object
    """
    fileblock = self.find_fileblock()
    if hasattr(fileblock, "_tempdir"):
        return fileblock._tempdir
    else:
        raise AttributeError("Could not find a temporary directory attached to this object's FileBlock")

find_temppath()

Find the parent file object's temporary directory path.

Similar to find_tempdir but returns the corresponding pathlib.Path object instead.

Source code in FoSpy/blocks/blocks.py
def find_temppath(self):
    """
    Find the parent file object's temporary directory path.

    Similar to [`find_tempdir`][FoSpy.blocks.blocks.Block.find_tempdir] but
    returns the corresponding `pathlib.Path` object instead.
    """
    fileblock = self.find_fileblock()
    if hasattr(fileblock, "_temppath"):
        return fileblock._temppath
    if hasattr(fileblock, "_temppdir"):
        raise AttributeError("This object's FileBlock has a temporary directory but no path mapped to it. "
                             "Use obj.find_tempdir() instead")
    raise AttributeError("Could not find a temporary directory object or path "
                         "attached to this object's FileBlock.")

get_id()

Returns an easily recognizable identifier for self. Non-unique.

Source code in FoSpy/blocks/blocks.py
def get_id(self):
    """Returns an easily recognizable identifier for self. Non-unique."""
    id_txt = str(getattr(self, self._id_key)) if self._id_key is not None else type(self).__name__
    return self._id_key, id_txt

get_parent_prop()

Source code in FoSpy/blocks/blocks.py
def get_parent_prop(self):
    if not hasattr(self, "_parent_block"):
        return None
    parent_blk = self._parent_block

    if isinstance(parent_blk, SingleBlock):
        for prop, val in parent_blk.get_prop_dict().items():
            if val is self:
                return prop

        raise err.FoSpyStructureError(f"Block {self} points to a parent block {parent_blk} that does not contain it as a property.")

    elif isinstance(parent_blk, ListBlock):
        return f"[{parent_blk.get_idx(self)}]"

    raise err.FoSpyStructureError(f"Block {self} has an unknown parent block type: {type(parent_blk)}")

get_prop_dict()

Returns a dictionary mapping property names to their live object values.

Source code in FoSpy/blocks/blocks.py
def get_prop_dict(self):
    """Returns a dictionary mapping property names to their live object values."""
    serial = self.serialize(shallow=True, clean=True)
    out = {}
    for prop in serial:
        if "$" in prop:
            prop = prop.split("$")[0]

        # guard for when templateblocks add staged templates to their serial
        if hasattr(self, prop):
            out[prop] = getattr(self, prop)

    return out

get_prop_path()

Source code in FoSpy/blocks/blocks.py
def get_prop_path(self):
    from .files import FileBlock

    if not hasattr(self, "_parent_block"):
        if isinstance(self, FileBlock):
            root_path = f"<{str(self.get_file_name())}>"
        else:
            root_path = f"<Root {type(self).__name__}"
            if isinstance(self, SingleBlock):
                id_key, id_txt = self.get_id()
                if id_key is not None:
                    root_path += f" ({id_key}={id_txt})"
            root_path += ">"
        return root_path

    parent_path = self._parent_block.get_prop_path()
    parent_prop = self.get_parent_prop()

    if "[" not in parent_prop:
        return parent_path + "." + parent_prop

    return parent_path + parent_prop

get_req_validators()

Overrides class validators with any renamed properties.

Similar to class method: build_req_validators, but uses _rename_validators to align any renamed properties with their original validators.

Source code in FoSpy/blocks/blocks.py
def get_req_validators(self):
    """
    Overrides class validators with any renamed properties.

    Similar to class method:
    [`build_req_validators`][FoSpy.blocks.blocks.SingleBlock.build_req_validators],
    but uses
    [`_rename_validators`][FoSpy.blocks.blocks.SingleBlock._rename_validators]
    to align any renamed properties with their original validators.
    """
    return self._rename_validators(self.build_req_validators())

get_validators()

Overrides class validators with any renamed properties.

Similar to class method: build_validators, but uses _rename_validators to align any renamed properties with their original validators. Also adds any optional key overrides added by key$alias syntax.

Returns:

Name Type Description
vals dict

maps expected keys to validation routines.

Source code in FoSpy/blocks/blocks.py
def get_validators(self):
    """
    Overrides class validators with any renamed properties.

    Similar to class method:
    [`build_validators`][FoSpy.blocks.blocks.SingleBlock.build_validators],
    but uses
    [`_rename_validators`][FoSpy.blocks.blocks.SingleBlock._rename_validators]
    to align any renamed properties with their original validators. Also
    adds any optional key overrides added by key$alias syntax.

    Returns:
        vals (dict): maps expected keys to validation routines.
    """
    vals = self._rename_validators(self.build_validators())
    if hasattr(self, "_key_overrides"):
        for key, val in self._key_overrides.items():
            vals[key] = val
    return vals

has_staged()

Source code in FoSpy/blocks/blocks.py
def has_staged(self):
    if len(self._staged_templates) > 0:
        return True

    for val in self.get_prop_dict().values():
        if hasattr(val, "has_staged") and val.has_staged():
            return True

    return False

inject_defaults(blockDict, *args, **kwargs) classmethod

Source code in FoSpy/blocks/blocks.py
@classmethod
def inject_defaults(cls, blockDict, *args, **kwargs):
    # fallback.
    # overridden by setup_dispatch decorator
    return blockDict

inspect() classmethod

Source code in FoSpy/blocks/blocks.py
@classmethod
def inspect(self):
    # for breaking to debugger from gui
    raise Exception("put a break point here")

key_to_idx(key, idx)

Reorder attributes for serialization.

Move any attribute name to a specific index in _key_order for serialization order. The invisible "metadata" key is always refreshed to the front of the list, so indices are effectively 1-based.

Parameters:

Name Type Description Default
key str

name of attribute to reorder

required
idx int

new index in _key_order

required
Source code in FoSpy/blocks/blocks.py
def key_to_idx(self, key:str, idx:int):
    """
    Reorder attributes for serialization.

    Move any attribute name to a specific index in `_key_order` for
    serialization order. The invisible `"metadata"` key is always refreshed
    to the front of the list, so indices are effectively 1-based.

    Args:
        key: name of attribute to reorder
        idx: new index in _key_order
    """
    self._meta_to_front()
    try:
        old_idx = self._key_order.index(key)
        self._key_order.pop(old_idx)
    # TODO: Better handling
    except Exception:
        pass
    self._key_order.insert(idx, key)

keys_to_end(*args)

Reorder attributes for serialization.

Move any attribute names in *args to the end of _key_order to be serialized last. Order within *args is maintained in result.

Source code in FoSpy/blocks/blocks.py
def keys_to_end(self, *args):
    """
    Reorder attributes for serialization.

    Move any attribute names in `*args` to the end of _key_order to be
    serialized last. Order within `*args` is maintained in result.
    """
    def remove_alias(key):
        return key.split("$")[0] if "$" in key else key
    for key in self.serialize(shallow=True):
        if not key.startswith("_") and remove_alias(key) not in self._key_order:
            self._key_order.append(remove_alias(key))
    for key in args:
        try:
            idx = self._key_order.index(key)
            self._key_order.pop(idx)
        # TODO: Better handling
        except Exception:
            pass
        self._key_order.append(key)
    self._meta_to_front()

keys_to_front(*args)

Reorder attributes for serialization.

Move any attribute names in *args to the front of _key_order to be serialized first. Order within *args is maintained in result.

Source code in FoSpy/blocks/blocks.py
def keys_to_front(self,*args):
    """
    Reorder attributes for serialization.

    Move any attribute names in `*args` to the front of _key_order to be
    serialized first. Order within `*args` is maintained in result.
    """
    try:
        meta_idx = args.index("metadata")
        args.pop(meta_idx)
    # TODO: Better handling
    except Exception:
        pass

    new_order = []
    for key in args:
        new_order.append(key)
    for key in self._key_order:
        if key not in new_order:
            new_order.append(key)
    self._key_order = new_order
    self._meta_to_front()

list_avail_routines(recursive=False, prefix='', abbreviated=False)

Lists all calc routines available to be added to self._calc_routines.

Non-abbreviated calc routine strings can be passed directly to self.add_calc_routine()

Parameters:

Name Type Description Default
recursive bool

If True, recursively walks all attributes and appends results from self.attr.list_avail_routines() to result. Otherwise only identifies methods of self.

False
prefix str

Used during recursion to build relative paths

''
abbreviated bool

optionally abbreviate recursively repeated routines for similar objects into one line. This line cannot be passed to self.add_calc_routine()

False

Returns:

Name Type Description
routines list

list of strings describing _calc_routine-decorated methods. Non-abbreviated calc routine strings can be passed directly to self.add_calc_routine()

Example:

    mySyn.list_avail_routines()
    ## returns []
    mySyn.list_avail_routines(recursive=True)
    ## returns [
    ##     'reaction.add_nom_MW',
    ##     'materials.add_weight_pcts',
    ##     'materials[0].add_MW',
    ##     'materials[1].add_MW',
    ##     ... 6 total materials with the same calc_routine
    ##     'materials[5].add_MW'
    ## ]
    mySyn.list_avail_routines(recursive=True, abbreviated=True)
    ## returns [
    ##     'reaction.add_nom_MW',
    ##     'materials.add_weight_pcts',
    ##     'materials[i].add_MW; i = [0, 1, 2, 3, 4, 5]'
    ## ]

Source code in FoSpy/blocks/blocks.py
def list_avail_routines(self, recursive:bool=False, prefix:str="", abbreviated:bool=False):
    """
    Lists all calc routines available to be added to `self._calc_routines`.

    Non-abbreviated calc routine strings can be passed directly to
    `self.add_calc_routine()`

    Args:
        recursive:
            If True, recursively walks all attributes and appends results
            from `self.attr.list_avail_routines()` to result. Otherwise only
            identifies methods of `self`.

        prefix: Used during recursion to build relative paths
        abbreviated:
            optionally abbreviate recursively repeated routines for similar
            objects into one line. This line cannot be passed to
            `self.add_calc_routine()`

    Returns:
        routines (list): 
            list of strings describing _calc_routine-decorated methods.
            Non-abbreviated calc routine strings can be passed directly to
            `self.add_calc_routine()`

    Example:
    ```
        mySyn.list_avail_routines()
        ## returns []
        mySyn.list_avail_routines(recursive=True)
        ## returns [
        ##     'reaction.add_nom_MW',
        ##     'materials.add_weight_pcts',
        ##     'materials[0].add_MW',
        ##     'materials[1].add_MW',
        ##     ... 6 total materials with the same calc_routine
        ##     'materials[5].add_MW'
        ## ]
        mySyn.list_avail_routines(recursive=True, abbreviated=True)
        ## returns [
        ##     'reaction.add_nom_MW',
        ##     'materials.add_weight_pcts',
        ##     'materials[i].add_MW; i = [0, 1, 2, 3, 4, 5]'
        ## ]
    ```
    """
    routines = []

    # Local routines
    for name in dir(self):
        attr = getattr(self, name)
        if callable(attr) and getattr(attr, "_is_calc_routine", False):
            routines.append(prefix + name)

    if recursive:
        for attr, val in self.__dict__.items():
            if attr.startswith("_"):
                continue

            # Recurse into child blocks
            if hasattr(val, "list_avail_routines"):
                child_prefix = f"{prefix}{attr}."
                routines.extend(val.list_avail_routines(True, child_prefix, abbreviated))

    return routines

make_template(template_name, *args)

Converts self into a template of its original subclass.

Returns a copy of self as a template of its original subclass, with specified fields replaced with template types. See TemplateClass for more information on template generation.

Parameters:

Name Type Description Default
template_name str

All templates require an identifying name.

required
*args str

properties to clear and replace with template types.

()
Source code in FoSpy/blocks/blocks.py
def make_template(self,template_name:str,*args:str):
    """
    Converts `self` into a template of its original subclass.

    Returns a copy of `self` as a template of its original subclass, with
    specified fields replaced with template types. See
    [`TemplateClass`][FoSpy.blocks.blocks.SingleBlock.TemplateClass] for
    more information on template generation.

    Args:
        template_name: All templates require an identifying name.
        *args: properties to clear and replace with template types.
    """

    from ..parsing.format_fos import format_field

    serial = self.serialize(keepListType=True)
    validators = self.get_validators()
    for key in args:
        val = validators.get(key, None)
        if isinstance(val,type) and (issubclass(val, SingleBlock) or issubclass(val, ListBlock)):
            serial.setdefault(key, [{}])
        else:
            serial[key] = format_field("template")
    serial["template_name"] = template_name
    return self.TemplateClass(*args)(serial)

print_summary(mode='cli') classmethod

Source code in FoSpy/blocks/blocks.py
@classmethod
def print_summary(cls, mode="cli"):
    from .._docs.properties import get_summary

    print(get_summary(cls, mode=mode))

reflex(serialize=True, clean=False, **kwargs) classmethod

Source code in FoSpy/blocks/attachments.py
@classmethod
def reflex(cls, serialize=True, clean=False, **kwargs:dict):
    from .template import TemplateField
    if "file_name" not in kwargs:
        kwargs["file_name"] = TemplateField.serialize()
        kwargs.pop("path", None)
        add_embedded = "embedded" not in kwargs

    elif not any(k in kwargs for k in ("path", "embedded")):
        add_embedded = True

    if add_embedded:
        kwargs["embedded"] = TemplateField.serialize()

    return super().reflex(serialize=serialize, clean=clean, **kwargs)

refresh_attachments(new_copy=None, overwrite=None, **kwargs)

Source code in FoSpy/blocks/blocks.py
def refresh_attachments(self, new_copy=None, overwrite=None, **kwargs):
    from .attachments import Attachment

    if new_copy is None:
        new_copy = self._att_new_copy
    if overwrite is None:
        overwrite = self._att_overwrite

    for propDict in self.__dict__, self.ext.__dict__:
        for key, val in propDict.items():
            if key.startswith("_") or key in self._reserved:
                continue
            if hasattr(val, "refresh_attachments"):
                val.refresh_attachments(new_copy=new_copy, overwrite=overwrite, **kwargs)
            elif isinstance(val, Attachment) and hasattr(val, "refresh"):
                val.refresh(new_copy=new_copy, overwrite=overwrite, **kwargs)

register_dispatch(registry_val, **kwargs) classmethod

Source code in FoSpy/blocks/attachments.py
@classmethod
def register_dispatch(cls, registry_val, **kwargs):
    extension = registry_val or ".txt"
    fn = "attachment"+extension
    return super().register_dispatch(registry_val, setup_from_key="_location",
                                     setup_allow_self=False, defaults={"file_name":fn},
                                     inherit_dispatch=True,
                                     **kwargs)

rename_block(old, new)

Source code in FoSpy/blocks/blocks.py
def rename_block(self, old, new):
    validators = self.get_validators()
    req = self.get_req_validators()
    if any(name.startswith("_") for name in (old, new)):
        raise ValueError("You cannot set private attributes (starting with '_') using obj.rename_block()")

    if old in req and new in validators:
        raise ValueError(f"You cannot rename '{old}' to '{new}'. '{old}' is a required property that "
                            f"can only be renamed to an unregistered key; '{new}' is already registered "
                            "as an expected property.")

    if hasattr(self, new):
        raise ValueError(f"'{new}' is already a property for this object, you cannot overwrite it with "
                         "obj.rename_block()")

    if "rename" in (old, new):
        raise ValueError("obj.rename property cannot be set or changed by obj.rename_block()")

    if hasattr(self, "rename") and hasattr(self.rename, old):
        old = getattr(self.rename, old)()

    if old in self._key_overrides:
        val = self._key_overrides.pop(old)
        self._key_overrides[new] = val

    else:
        if not hasattr(self,"rename"):
            self.rename = {}

        rename_dict = self.rename_dict()
        rename_from = {v:k for k,v in rename_dict.items()}
        if old in rename_from:
            base = rename_from[old]
        else:
            base = old

        _debug.msg(f"Registering '{base}':'{new}' into rename block")
        setattr(self.rename, base, new)
    _debug.msg(f"Moving '{old}' over to '{new}'.")
    setattr(self,new,getattr(self, old))
    delattr(self,old)

    try:
        idx = self._key_order.index(old)
        self._key_order[idx] = new
    # TODO: Better handling
    except Exception:
        self._key_order.append(new)

rename_dict()

Source code in FoSpy/blocks/blocks.py
def rename_dict(self):
    if not hasattr(self, "rename"):
        return {}
    return self.rename.serialize(shallow=True, clean=True)

serialize(keepListType=False, shallow=False, clean=False, **kwargs)

Return a recursively serialized dict representation of self.

Fully serialized SingleBlocks are a single dict that can be passed to another constructor or emitted into lines for a FOS file. Serialized values at any nest level are either dicts, lists, or strings to allow full type-coersion when reconstructing or simplified emission when writing files.

Serialized dict is deep copied to prevent object mutation.

Parameters:

Name Type Description Default
keepListType bool

When True, maintains its current FOS printing mode (looped keys or explicit key:value lines), instead of explicit default

False
shallow bool

When True, no recursive serialization occurs. Recommended when serialization is used only to inspect top-level keys.

False
clean bool

When True, no FOS format read/write metadata is included in the serial. Recommended for sending output to other formats like JSON.

False

Private attributes starting with "_" are either skipped or unpacked in special cases:

  • _key_order: attributes are added to the serialized dict in the order they appear in this list.

  • _calc_comments: calculated comments are attached to their mapped attribute after serialization to avoid mutation of object comments

  • _calc_routines: A list of functions scheduled to be called right before serialization to update _calc_comments. Scheduling calc routines ensures that their calculated values are up-to-date.

  • _meta: attributes of this container are given their own private _keys mapped by FoSpy.parsing.syntax.meta_keys in the serialized dict.

  • _key_overrides: per-instance override mapping that tracks which unexpected attributes require $alias suffixes.

  • _aliases: maps attribute names to alias tags used to emit $alias suffixed keys.

  • _reserved: attribute names in reserved are non-private attributes which should not be serialized. This usually applies to the ext attribute or methods attached after construction.

Source code in FoSpy/blocks/blocks.py
def serialize(self, keepListType:bool=False, shallow:bool=False, clean:bool=False, **kwargs):
    """
    Return a recursively serialized `dict` representation of `self`.

    Fully serialized `SingleBlock`s are a single dict that can be passed to
    another constructor or emitted into lines for a FOS file. Serialized
    values at any nest level are either dicts, lists, or strings to allow
    full type-coersion when reconstructing or simplified emission when
    writing files.

    Serialized dict is deep copied to prevent object mutation.

    Args:
        keepListType:
            When True, maintains its current FOS printing mode (looped keys
            or explicit key:value lines), instead of explicit default

        shallow:
            When True, no recursive serialization occurs. Recommended when
            serialization is used only to inspect top-level keys.

        clean:
            When True, no FOS format read/write metadata is included in the
            serial. Recommended for sending output to other formats like
            JSON.

    Private attributes starting with "_" are either skipped or unpacked in
    special cases:

    * `_key_order`:
        attributes are added to the serialized dict in the order they
        appear in this list.

    * `_calc_comments`:
        calculated comments are attached to their mapped attribute after
        serialization to avoid mutation of object comments

    * `_calc_routines`:
        A list of functions scheduled to be called right before
        serialization to update _calc_comments. Scheduling calc routines
        ensures that their calculated values are up-to-date.

    * `_meta`:
        attributes of this container are given their own private `_key`s
        mapped by `FoSpy.parsing.syntax.meta_keys` in the serialized
        dict.

    * `_key_overrides`:
        per-instance override mapping that tracks which unexpected
        attributes require $alias suffixes.

    * `_aliases`:
        maps attribute names to alias tags used to emit $alias suffixed
        keys.

    * `_reserved`:
        attribute names in reserved are non-private attributes which
        should *not* be serialized. This usually applies to the `ext`
        attribute or methods attached after construction.
    """
    from copy import deepcopy
    from ..parsing.format_fos import format_calc_comment
    from .template import TemplateBlock

    val_to_alias = {v:k for k,v in self._aliases.items()}

    all_attrs = {}
    out = {}

    for routine in self._calc_routines:
        routine()

    def add_alias(key):
        if key in self._key_overrides:
            alias = val_to_alias[self._key_overrides[key]]
            return f"{key}${alias}"
        return key


    def try_serial(obj):
        if isinstance(obj, SimpleWrapper):
            obj = obj()
        serialize = getattr(obj, "serialize", None)
        if callable(serialize) and not shallow:
            return obj.serialize(clean=clean)
        if isinstance(obj, list):
            return [try_serial(item) for item in obj]
        if isinstance(obj, dict):
            return {k:try_serial(v) for k,v in obj.items()}
        return str(obj)

    for attr,val in self.__dict__.items():
        if attr == "ext" and val is not None:
            for ext_attr, ext_val in val.__dict__.items():
                all_attrs[ext_attr] = ext_val
        elif not (attr.startswith("_") or attr in self._reserved):
            all_attrs[attr] = val


    for key in self._key_order:
        if key in all_attrs:
            val = all_attrs.pop(key)
            out[add_alias(key)] = try_serial(val)

    for key, val in all_attrs.items():
        out[add_alias(key)] = try_serial(val)

    for attr, key in mk.items():
        try:
            k = md[key].copy()
        except AttributeError:
            k = md[key]
        val = getattr(self._meta,attr,k)
        out[key] = val

    comments = {}
    for key, comment_list in out[mk["comments"]].items():
        comments[add_alias(key)] = comment_list
    out[mk["comments"]] = comments

    out = deepcopy(out)

    # _debug.pmsg(self._calc_comments)
    for key, comments in self._calc_comments.items():
        for comment in comments.values():
            out[mk["comments"]].setdefault(add_alias(key),[])
            out[mk["comments"]][add_alias(key)].append(format_calc_comment(comment))

    if not keepListType:
        out[mk["list_type"]] = "explicit"

    if "template_name" in out and not isinstance(self, TemplateBlock):
        out.pop("template_name")

    if clean:
        scan = out.copy()
        for key, val in scan.items():
            if key.startswith("_") or val is None:
                out.pop(key)

    if not any(k for k in out.get("rename", {}) if not k.startswith("_")):
        out.pop("rename", None)

    return out

set_dispatch(value=None, from_parent=None, from_key=None, allow_self=None) classmethod

Source code in FoSpy/blocks/blocks.py
@classmethod
def set_dispatch(cls, value=None, from_parent=None, from_key=None, allow_self=None):

    # Abstract classes are sometimes made without SingleBlock in MRO
    if from_parent is not None:
        target_cls = from_parent
    else:
        target_cls = cls

    if "dispatch" not in target_cls.__dict__:
        target_cls.dispatch = {}

    if from_key is not None:
        target_cls.dispatch_key = from_key

    if allow_self is not None:
        target_cls.dispatch_allow_self = allow_self

    def dispatched_cls(subcls, v=value, _cls=target_cls):
        subcls.dispatch_from = _cls
        _cls.dispatch[v] = subcls
        return subcls
    return dispatched_cls

setup_dispatch(from_key=None, allow_self=True, _dispatch_from=None, _defaults={}) staticmethod

Decorate a class to dispatch to other classes during construction.

The decorated class's add_dispatch method will be wrapped into a new method, dispatch_subclass, which is decorator as a dispatcher. The parent class's from_key is found in the blockDict passed to the constructor, and the value mapped to from_key is mapped to dispatchable subclasses in the registry.

add_dispatch returns a dictionary of values that are injected into the blockDict, either to be detected by dispatch, or to be delegated to the constructor.

This decorator should only be used directly for the start of a dispatch chain. For later dispatches, use register_dispatch

Parameters:

Name Type Description Default
cls SingleBlock subclass

The class to be decorated. If provided, the decorator is most likely being called as a bare decorator. Otherwise, the decorators is being called with other keyword arguments and returns the modified decorator.

None
from_key str

The key to be located in the blockDict after optional injection by add_dispatch. Private from_keys will be injected and located under the __dispatch__ key which is popped before final construction.

None
allow_self bool

When True, the decorated class will dispatch to itself if no subclasses can be found. When False, error is raised during construction if dispatchable subclass is not found.

True
_dispatch_from SingleBlock subclass

To be passed only by register_dispatch, which decorates subclasses to populate this class's registry. Identifies the parent class that the constructor must start at. If not provided, the decorated class is assumed to be the start of a dispatch chain.

None
_defaults dict

To be passed only by register_dispatch, which decorates subclasses to populat this class's registry. Provides default values that should be injected into the blockDict when trying to guarantee dispatching to the decorated class (usually by a template constructor).

{}
Source code in FoSpy/blocks/blocks.py
@staticmethod
def setup_dispatch(cls:type[BlockType]=None,
    from_key=None,
    allow_self=True,
    _dispatch_from=None,
    _defaults={}
):
    """
    Decorate a class to dispatch to other classes during construction.

    The decorated class's [`add_dispatch`
    method][FoSpy.blocks.blocks.SingleBlock.add_dispatch] will be wrapped
    into a new method,
    [`dispatch_subclass`][FoSpy.blocks.blocks.SingleBlock.dispatch_subclass],
    which is decorator as a
    [`dispatcher`][FoSpy.blocks.blocks.SingleBlock.dispatcher]. The parent
    class's `from_key` is found in the blockDict passed to the constructor,
    and the value mapped to `from_key` is mapped to dispatchable subclasses
    in the `registry`.

    `add_dispatch` returns a dictionary of values that are injected into the
    blockDict, either to be detected by dispatch, or to be delegated to the
    constructor.

    This decorator should only be used directly for the start of a dispatch
    chain. For later dispatches, use
    [`register_dispatch`][FoSpy.blocks.blocks.SingleBlock.register_dispatch]

    Args:
        cls (SingleBlock subclass):
            The class to be decorated. If provided, the decorator is most
            likely being called as a bare decorator. Otherwise, the
            decorators is being called with other keyword arguments and
            returns the modified decorator.

        from_key (str):
            The key to be located in the blockDict after optional injection
            by `add_dispatch`. Private `from_key`s will be injected and
            located under the `__dispatch__` key which is popped before
            final construction.

        allow_self (bool):
            When True, the decorated class will dispatch to itself if no
            subclasses can be found. When False, error is raised during
            construction if dispatchable subclass is not found.

        _dispatch_from (SingleBlock subclass):
            To be passed only by `register_dispatch`, which decorates
            subclasses to populate this class's registry. Identifies the
            parent class that the constructor must start at. If not
            provided, the decorated class is assumed to be the start of a
            dispatch chain. 
        _defaults (dict):
            To be passed only by `register_dispatch`, which decorates
            subclasses to populat this class's registry. Provides default
            values that should be injected into the blockDict when trying to
            guarantee dispatching to the decorated class (usually by a
            template constructor).
        """
    if cls is not None and not isinstance(cls, type):
        raise Exception("@setup_dispatch must be used as a bare decorator, or with "
                        "a class as the first positional argument. You may have tried "
                        "to decorate a class with positional args instead of keywords.")

    def decorator(_cls:type[BlockType], _fk=from_key, _as=allow_self, _df=_dispatch_from, _def=_defaults):
        _cls.__dispatch__ = {
            "from_key": _fk,
            "allow_self": _as,
            "dispatch_from": _df or _cls,
            "registry": {}
        }

        def inject(bD, k, v, is_default=False):
            target_dict = bD["__dispatch__"] if k.startswith("_") else bD

            if is_default and v is None and k in target_dict:
                return bD

            target_dict[k] = v

            return bD

        @classmethod
        def inject_defaults(current_cls, blockDict, _d=_def):
            d = current_cls.__dispatch__
            blk_d = blockDict.setdefault("__dispatch__", {})
            if (not d['allow_self'] and
                None not in d['registry'] and
                blockDict.get(d['from_key'],blk_d.get(d['from_key'], None)) is None):
                default_dispatch = next(iter(d['registry'].values()))
                blockDict = default_dispatch.inject_defaults(blockDict)

            for k, v in _d.items():
                blockDict = inject(blockDict, k, v, is_default=True)
            return blockDict

        @SingleBlock.dispatcher
        def dispatch_subclass(current_cls:type[BlockType], blockDict:dict, _dispatch_key=_fk, **kwargs):
            injection = current_cls.add_dispatch(blockDict, _dispatch_key, _wrapped=True, **kwargs)

            for k, v in injection.items():
                blockDict = inject(blockDict, k, v)

            return blockDict

        # inject methods
        _cls.inject_defaults = inject_defaults
        _cls.dispatch_subclass = dispatch_subclass

        return _cls

    if cls is not None:
        return decorator(cls)

    return decorator

stage_template(prop_name, template=None)

Source code in FoSpy/blocks/blocks.py
def stage_template(self, prop_name, template:Block|dict=None):
    from .template import TemplateBlock
    if template is None:
        template = {}

    if not isinstance(template, (TemplateBlock, dict)):
        raise ValueError("Template must be a TemplateBlock or dictionary. To 'stage' a ListBlock, "
                         "you can stage a SingleBlock template with a ListBlock alias. This creates "
                         "a non-template ListBlock with the template staged as its first entry.")

    alias_validator = None
    if "$" in prop_name:
        prop_name, alias = prop_name.split("$",1)
        try:
            alias_validator = self._aliases[alias]
        except KeyError as e:
            raise err.PropertyAliasError(prop_name, self, blockDict={prop_name: "<staged template>"},
                                         hint=f"Unrecognized block alias: '{alias}' assigned to property: ",
                                         posthint=f"Valid aliases: {list(self._aliases.keys())}") from e

    if hasattr(self, prop_name):
        raise ValueError(f"Property {prop_name} already exists. You cannot stage a template for a property that already exists.")

    validators = self.build_validators()
    validator = validators.get(prop_name, None)

    if validator is not None:
        alias = None

    if alias_validator is not validator and None not in (validator, alias_validator):
        raise ValueError(f"Property {prop_name} already has a validator. You cannot alias a different validator for the same property.")

    validator = next(v for v in (validator, alias_validator) if v is not None)

    if validator is None:
        try:
            if not isinstance(template, TemplateBlock):
                raise TypeError("Dictionary templates must be staged with an alias.")

            alias = next(k for k, v in self._aliases.items() if isinstance(template, v))
            validator = self._aliases[alias]
        except (TypeError,StopIteration) as e:
            raise ValueError(f"Property {prop_name} is unexpected. In order to stage a template "
                            "for and unexpected property, you must specify the validator with a '$' alias "
                            "in the property name, or stage a pre-constructed template of an aliasable validator."
                            ) from e

    if isinstance(validator, type) and issubclass(validator, ListBlock):
        # let setattr handle ListBlock construction using alias
        # this creates an empty ListBlock under self.prop_name
        # (alias stripped during setattr)
        setattr(self, prop_name+"$"+alias if alias is not None else prop_name, [])
        empty_lb = getattr(self, prop_name)
        return empty_lb.stage_template("entry0", template)

    if isinstance(template, dict):
        # reflex returns a TemplateBlock subclassed from the validator
        template = validator.reflex(serialize=False, include_temp_names=True, clean=False, **template)
        template.template_name = prop_name

    elif not isinstance(template, validator):
        val_nm = validator.__name__
        if alias is None:
            error_msg = f"The provided template is not compatible with the validator expected for property '{prop_name}' ({val_nm})."
        else:
            error_msg = f"The provided template is not compatible with the validator specified by alias '{alias}' ({val_nm})."
        raise ValueError(error_msg)


    template._staged_parent = self

    if alias is not None:
        self._key_overrides[prop_name] = validator

    self._staged_templates[prop_name] = template

    return prop_name, template

to_json(filepath=None, clean=True, indent=4, **kwargs)

Converts self into a JSON-formatted string or file.

Serializes and either returns as a JSON-formatted string or saves to a JSON file.

Parameters:

Name Type Description Default
filepath pathlike

JSON file save destination. If None, returns JSON-formatted string instead.

None
clean bool

When True, no FOS format read/write metadata is included in the serial. FOS metadata has no impact on JSON format but may be useful to view in JSON for troubleshooting.

True
indent int

indent value passed to json.dump for file saving.

4
**kwargs any

other arguments passed to json.dump for file saving.

{}
Source code in FoSpy/blocks/blocks.py
def to_json(self, filepath=None, clean:bool=True, indent:int=4, **kwargs):
    """
    Converts `self` into a JSON-formatted string or file.

    [Serializes][FoSpy.blocks.blocks.SingleBlock.serialize] and either
    returns as a JSON-formatted string or saves to a JSON file.

    Args:
        filepath (pathlike):
            JSON file save destination. If `None`, returns JSON-formatted
            string instead.

        clean:
            When True, no FOS format read/write metadata is included in the
            serial. FOS metadata has no impact on JSON format but may be
            useful to view in JSON for troubleshooting.

        indent:
            `indent` value passed to `json.dump` for file saving.

        **kwargs (any):
            other arguments passed to `json.dump` for file saving.
    """
    import json
    serial = self.serialize(clean=clean)

    if filepath is None:
        return json.dumps(serial)

    with open(filepath, "w") as f:
        json.dump(serial, f, indent=indent, **kwargs)

track_attachments(new_copy='prompt', overwrite='prompt', **kwargs)

Source code in FoSpy/blocks/blocks.py
def track_attachments(self, new_copy="prompt",overwrite="prompt", **kwargs):
    self._att_new_copy = new_copy
    self._att_overwrite = overwrite

AttachmentList


CIFFile

Bases: AnyFile, Attachment

Methods:

Name Description
TemplateClass

Create a template for a subclass of SingleBlock.

__delattr__
__eq__

Check equality of two SingleBlock objects.

__getattr__

Check both self and self.ext for attribute before returning.

__hash__
__init__
__new__
__setattr__
_assign_and_inject

Attaches attributes and methods to any value before assigning it as an

_get_engine
_get_filepath

Default behavior: Must be overwritten in subclasses.

_meta_to_front

Moves metadata to the front of _key_order. Metadata will always be

_rename_validators

Realigns any renamed

_resolve_relative_path

Resolves a relative object path string into an object or function.

_subprocess
_update_src
_validate_filename
add_all_calc_routines

Schedule all available calculation routines.

add_block

Adds an unexpected attribute with a validator mapped by type_alias.

add_calc_comment

Add a calculated comment to be injected during serialization.

add_calc_routine

Schedules a calculated comment.

add_comments

Default behavior to be overwritten when attached to a parent block.

add_dispatch
build_req_validators

Builds required keys and validators mapped to subclass.

build_validators

Builds expected keys and validators mapped to subclass.

clear_all_comments
clear_comments

Clear comments attached to top-level attributes only.

copy

Returns a deep-copy of self by serializing and reconstructing.

default_key_order

Set to default attribute order for serialization.

dispatch_subclass
dispatcher

Decorate a classmethod to dispatch to other subclasses.

enforce_subtype
fill_staged_template
find_attachments
find_fileblock

Finds the parent file object.

find_tempdir

Find the parent file object's temporary directory.

find_temppath

Find the parent file object's temporary directory path.

get_id

Returns an easily recognizable identifier for self. Non-unique.

get_parent_prop
get_pattern
get_peaks
get_prop_dict

Returns a dictionary mapping property names to their live object values.

get_prop_path
get_req_validators

Overrides class validators with any renamed properties.

get_validators

Overrides class validators with any renamed properties.

has_staged
inject_defaults
inspect
key_to_idx

Reorder attributes for serialization.

keys_to_end

Reorder attributes for serialization.

keys_to_front

Reorder attributes for serialization.

list_avail_routines

Lists all calc routines available to be added to self._calc_routines.

make_template

Converts self into a template of its original subclass.

new_engine
print_summary
quick_pattern
reflex
refresh_attachments
register_dispatch
rename_block
rename_dict
serialize

Return a recursively serialized dict representation of self.

set_dispatch
setup_dispatch

Decorate a class to dispatch to other classes during construction.

stage_template
to_json

Converts self into a JSON-formatted string or file.

track_attachments
Source code in FoSpy/blocks/attachments.py
@Attachment.register_dispatch(".cif")
class CIFFile(AnyFile, Attachment):
    def __init__(self, blockDict, **kwargs):
        super().__init__(blockDict,**kwargs)
        self._reserved.append("engine")
        self.engine = None

    def _get_engine(self, engine_name=None):
        if engine_name is None:
            if self.engine is None:
                self.engine = self.new_engine()
            engine = self.engine
        else:
            engine = self.new_engine(engine_name=engine_name)

        return engine

    def get_pattern(self, engine_name=None):
        engine = self._get_engine(engine_name=engine_name)

        return engine.get_pattern()

    def get_peaks(self, engine_name=None):
        engine = self._get_engine(engine_name=engine_name)

        return engine.get_peaks()

    def new_engine(self, engine_name=None):
        from ..config import values as cfg
        from ..plotting.diffraction.engines import ENGINES
        if engine_name is None:
            engine_name = cfg.get("diffraction.default_engine")
        return ENGINES[engine_name](self._get_filepath())

    def quick_pattern(self,subprocess=False):
        from ..plotting._utils import _quick_pattern

        df = self.get_pattern()

        x,y = df.columns[:2]

        tth, intensity = df[x].to_numpy(), df[y].to_numpy()

        if subprocess:
            return self._subprocess(_quick_pattern, args=(tth, intensity))

        return _quick_pattern(tth, intensity)

_aliases = new_als class-attribute instance-attribute

_calc_comments = {} instance-attribute

_calc_routines = [] instance-attribute

_constructed = True instance-attribute

_filepath = None instance-attribute

_id_key = 'file_name' class-attribute instance-attribute

_key_order = [] instance-attribute

_key_overrides = {} instance-attribute

_meta = SubContainer() instance-attribute

_reserved = ['ext'] instance-attribute

_sourceDict = blockDict.copy() instance-attribute

_staged_templates = {} instance-attribute

dispatch = {} class-attribute instance-attribute

dispatch_allow_self = True class-attribute instance-attribute

dispatch_default = None class-attribute instance-attribute

dispatch_key = None class-attribute instance-attribute

engine = None instance-attribute

ext = SubContainer() instance-attribute

rename = rename instance-attribute

TemplateClass(*args) classmethod

Create a template for a subclass of SingleBlock.

Generates a hybridized subclass of the current block class and TemplateBlock. Template subclasses override original expected validators with either a TemplateField, TemplateBlock, or TemplateList depending on the type of the original validator.

Parameters:

Name Type Description Default
*args str

A list of properties to override as template types.

()
Source code in FoSpy/blocks/blocks.py
@classmethod
def TemplateClass(cls,*args:str):
    """
    Create a template for a subclass of `SingleBlock`.

    Generates a hybridized subclass of the current block class and
    [`TemplateBlock`][FoSpy.blocks.template.TemplateBlock]. Template
    subclasses override original expected validators with either a
    [`TemplateField`][FoSpy.blocks.template.TemplateField],
    [`TemplateBlock`][FoSpy.blocks.template.TemplateBlock], or
    [`TemplateList`][FoSpy.blocks.template.TemplateList] depending on the
    type of the original validator.

    Args:
        *args: A list of properties to override as template types.
    """
    from .template import TemplateBlock, FlexTemplate

    cls_registry = TemplateBlock.__dispatch__["registry"]

    if cls not in cls_registry:

        @TemplateBlock.register_dispatch(cls, setup_from_key="_fields", setup_allow_self=True, inherit_dispatch=True)
        class TemplateLocator(FlexTemplate, TemplateBlock, cls):
            _full_class = cls

        TemplateLocator.__name__ = f"{cls.__name__}TemplateLocator"
        TemplateLocator.__qualname__ = f"{cls.__name__}.TemplateClass.Locator"
        TemplateLocator.__module__ = cls.__module__

    fields = tuple(sorted(args))

    # construct a proxy dictionary that will correctly dispatch to the right
    # template class in TemplateBlock's dispatch chain.
    proxy_dict = {
        "__dispatch__": {
            "_full_class": cls,
            "_fields": fields
        }
    }

    return TemplateBlock.dispatch_subclass(proxy_dict)

__delattr__(attr)

Source code in FoSpy/blocks/blocks.py
def __delattr__(self, attr):
    if attr in self.get_req_validators():
        raise AttributeError(f"Cannot delete property: '{attr}'. It is registered as a required property for this object.")
    return super().__delattr__(attr)

__eq__(other, suppress_routine_paths=False)

Check equality of two SingleBlock objects.

Equality is checked by a deep difference of their serialized dictionaries.

Parameters:

Name Type Description Default
suppress_routine_paths bool

Optional flag to still return true if the only differences found are in calculation routine metadata. Calculation routines are for user information only and may not be relevant for equality.

False
Source code in FoSpy/blocks/blocks.py
def __eq__(self, other, suppress_routine_paths:bool=False):
    """
    Check equality of two `SingleBlock` objects.

    Equality is checked by a deep difference of their
    [serialized][FoSpy.blocks.blocks.SingleBlock.serialize] dictionaries.

    Args:
        suppress_routine_paths:
            Optional flag to still return true if the only differences found
            are in [calculation
            routine][FoSpy.blocks.blocks.SingleBlock.add_calc_routine]
            metadata. Calculation routines are for user information only and
            may not be relevant for equality.
    """
    from .._debug import deep_diff as dd, _debug as db
    try:
        db.msg("Serializing Blocks to check equality:", module = "SingleBlock.__eq__()")
        diffs = dd(self.serialize(), other.serialize(), suppress_routine_paths=suppress_routine_paths)
        passed = len(diffs) == 0
        if not passed:
            db.pmsg(diffs,module = "SingleBlock.__eq__()")
        return passed
    except Exception as e:
        db.msg(f"Equality failed by exception: {e}",module = "SingleBlock.__eq__()")
        return False

__getattr__(name)

Check both self and self.ext for attribute before returning.

A matching attribute of self will be returned first, but if self has no matching attribute, a matching attribute of self.ext can be returned instead.

Source code in FoSpy/blocks/blocks.py
def __getattr__(self, name:str):
    """
    Check both `self` and `self.ext` for attribute before returning.

    A matching attribute of `self` will be returned first, but if `self` has
    no matching attribute, a matching attribute of `self.ext` can be
    returned instead.
    """

    try:
        if name not in ("rename", "ext") and hasattr(self, "rename"):
            rename_dict = self.rename.serialize(shallow=True, clean=True)
            if name in rename_dict:
                return getattr(self, rename_dict[name])

        if name != 'ext':
            return getattr(self.ext, name)

        raise AttributeError()
    except AttributeError:
        raise AttributeError(
            f"{type(self).__name__} object "
            f"has no attribute {name!r}."
        )

__hash__()

Source code in FoSpy/blocks/blocks.py
def __hash__(self):
    return id(self)

__init__(blockDict, **kwargs)

Source code in FoSpy/blocks/attachments.py
def __init__(self, blockDict, **kwargs):
    super().__init__(blockDict,**kwargs)
    self._reserved.append("engine")
    self.engine = None

__new__(blockDict, *args, **kwargs)

Source code in FoSpy/blocks/blocks.py
def __new__(cls, blockDict, *args, **kwargs):
    _dispatched = kwargs.pop("_dispatched", False)
    if _dispatched:
        # blockDict should always be dict after dispatch. I want to see attributeerror if not.
        blockDict.pop("__dispatch__",None)
        return super().__new__(cls)

    blockDict = _unwrap_block(blockDict)

    dispatched_cls = cls.dispatch_subclass(blockDict, *args, **kwargs)

    if issubclass(dispatched_cls, cls):
        return dispatched_cls(blockDict, *args, _dispatched=True, **kwargs)

    dispatch = blockDict.pop("__dispatch__")
    raise err.BlockDispatchError(
        f"Attempted to construct the following dictionary as a {cls.__name__} block, "
        f"but it was dispatched to a {dispatched_cls.__name__} instead."
        f"\n\nINPUT:\n{blockDict}"
        f"\n\nDISPATCH:\n{dispatch}")

__setattr__(name, value)

Source code in FoSpy/blocks/attachments.py
def __setattr__(self, name, value):
    if name == "_extension":
        if value is None:
            return
        if hasattr(self, "_extension") and value != self._extension:
            from warnings import warn
            warn("You cannot change the extension of an attachment after construction. Skipping change.", RuntimeWarning)
            return

    if name == "file_name":
        old_ext = self._extension if hasattr(self, "_extension") else None
        value, new_ext = self._validate_filename(value, old_ext)
        self._extension = new_ext

    return super().__setattr__(name, value)

_assign_and_inject(name, value, extended=False)

Attaches attributes and methods to any value before assigning it as an attribute of self or self.ext.

Attributes Attached to Object

_parent_block: refers to self

Methods Attached to Object

add_comments_to_parent clear_comments_from_parent

Source code in FoSpy/blocks/blocks.py
def _assign_and_inject(self, name, value, extended=False):
    """
    Attaches attributes and methods to any value before assigning it as an
    attribute of `self` or `self.ext`.

    Attributes Attached to Object:
        `_parent_block`: refers to `self`

    Methods Attached to Object:
        [`add_comments_to_parent`][FoSpy.blocks.blocks._add_comments_to_parent]
        [`clear_comments_from_parent`][FoSpy.blocks.blocks._clear_comments_from_parent]
    """
    from .attachments import Attachment

    if name == 'ext':
        return super().__setattr__('ext', value)
    if not hasattr(value, "__dict__"):
        value = SimpleWrapper(value)

    if extended:
        setattr(self.ext, name, value)
    else:
        super().__setattr__(name, value)

    attr_obj = getattr(self.ext if extended else self, name)

    setattr(attr_obj, "_parent_block", self)

    if isinstance(attr_obj, Attachment):
        attr_obj._get_filepath()
    elif hasattr(attr_obj, "refresh_attachments"):
        attr_obj.refresh_attachments()

    methods = ((_add_comments_to_parent(name), "add_comments"),
            (_clear_comments_from_parent(name), "clear_comments"))

    attr_obj._reserved = ['ext'] if not hasattr(attr_obj,"_reserved") else attr_obj._reserved
    for method, method_name in methods:
        attr_obj._reserved.append(method_name)
        bound = method.__get__(attr_obj, type(attr_obj))
        setattr(attr_obj, method_name, bound)

    self._props_changed = True

_get_engine(engine_name=None)

Source code in FoSpy/blocks/attachments.py
def _get_engine(self, engine_name=None):
    if engine_name is None:
        if self.engine is None:
            self.engine = self.new_engine()
        engine = self.engine
    else:
        engine = self.new_engine(engine_name=engine_name)

    return engine

_get_filepath()

Default behavior: Must be overwritten in subclasses.

Source code in FoSpy/blocks/attachments.py
def _get_filepath(self):
    """
    Default behavior: Must be overwritten in subclasses.
    """
    raise AttachmentTypeError("Attachments must be constructed as a subclass with a set_filepath method.")

_meta_to_front()

Moves metadata to the front of _key_order. Metadata will always be serialized first, but being elsewhere in the order leads to unexpected results when moving other keys to desired indices.

Source code in FoSpy/blocks/blocks.py
def _meta_to_front(self):
    """
    Moves metadata to the front of `_key_order`. Metadata will always be
    serialized first, but being elsewhere in the order leads to unexpected
    results when moving other keys to desired indices.
    """
    try:
        meta_idx =self._key_order.index("metadata")
        self._key_order.pop(meta_idx)
    # TODO: Better handling
    except Exception:
        pass
    self._key_order.insert(0,"metadata")

_rename_validators(validators)

Realigns any renamed attributes with their expected validator.

Parameters:

Name Type Description Default
validators dict

A dictionary mapping attribute names to validators, returned by either build_validators or build_req_validators

required
Source code in FoSpy/blocks/blocks.py
def _rename_validators(self, validators:dict):
    """
    Realigns any [renamed][FoSpy.blocks.blocks.SingleBlock.rename_block]
    attributes with their expected validator.

    Args:
        validators:
            A dictionary mapping attribute names to validators, returned by
            either
            [`build_validators`][FoSpy.blocks.blocks.SingleBlock.build_validators]
            or
            [`build_req_validators`][FoSpy.blocks.blocks.SingleBlock.build_req_validators]
    """
    if hasattr(self, "rename"):
        for name, rename in self.rename.serialize(shallow=True, clean=True).items():
            if name in validators and rename not in validators:
                val = validators.pop(name)
                validators[rename] = val
    return validators

_resolve_relative_path(path)

Resolves a relative object path string into an object or function.

Example:

    mySyn._resolve_relative_path("materials[1].ratio")
    ## returns mySyn.materials[1].ratio

Source code in FoSpy/blocks/blocks.py
def _resolve_relative_path(self, path: str):
    """
    Resolves a relative object path string into an object or function.

    Example:
    ```
        mySyn._resolve_relative_path("materials[1].ratio")
        ## returns mySyn.materials[1].ratio
    ```
    """
    import re

    _index_re = re.compile(r"^([A-Za-z_]\w*)\[(\d+)\]$")
    obj = self

    for part in path.split("."):

        # Case: attr[index]
        m = _index_re.match(part)
        if m:
            attr_name, idx_str = m.groups()
            idx = int(idx_str)

            # Get the ListBlock
            obj = getattr(obj, attr_name)

            # Index into its _objs
            obj = obj._objs[idx]
            continue

        # Case: simple attribute
        obj = getattr(obj, part)

    return obj

_subprocess(target, args=(), **kwargs)

Source code in FoSpy/blocks/blocks.py
def _subprocess(self, target, args=(), **kwargs):
    from multiprocessing import Process

    if kwargs is None:
        kwargs={}

    p = Process(target=target, args=args, kwargs=kwargs)
    p.start()

_update_src()

Source code in FoSpy/blocks/blocks.py
def _update_src(self):
    if self._constructed and self._props_changed:
        self._sourceDict = self.serialize(clean=True)
        self._props_changed = False

    return self._sourceDict   

_validate_filename(filename, ext=None, warn=True) classmethod

Source code in FoSpy/blocks/attachments.py
@classmethod
def _validate_filename(cls, filename:str, ext:str=None, warn=True):
    filename = str(filename)
    if ext is None:
        ext = f".{filename.rsplit('.')[-1]}" if "." in filename else ""
        # delegate to base validator routine to verify extension
        return filename, ext

    if "." not in filename:
        new_ext = ext
    else:
        new_ext = f".{filename.rsplit('.')[-1]}"

    if new_ext != ext:
        if warn:
            filename = filename + ext
            from warnings import warn
            warn(f"New filename contains a different extension: '{new_ext}'. Extensions cannot "
                f"be changed after construction. The current extension ('{ext}') "
                f"will be appended to the new filename to form: '{filename}'.", RuntimeWarning)
        else:
            raise ValueError(f"New filename contains a different extension: '{new_ext}'. Extensions cannot "
                             "be changed after attachment construction.")

    return filename, new_ext

add_all_calc_routines(recursive=False)

Schedule all available calculation routines.

Adds all available calc_routines to self._calc_routines using list_avail_routines() and add_calc_routine().

Parameters:

Name Type Description Default
recursive bool

Optional recursion. See SingleBlock.list_avail_routines()

False
Source code in FoSpy/blocks/blocks.py
def add_all_calc_routines(self, recursive:bool=False):
    """
    Schedule all available calculation routines.

    Adds all available calc_routines to `self._calc_routines` using
    [`list_avail_routines()`][FoSpy.blocks.blocks.SingleBlock.list_avail_routines]
    and
    [`add_calc_routine()`][FoSpy.blocks.blocks.SingleBlock.add_calc_routine].

    Args:
        recursive:
            Optional recursion. See `SingleBlock.list_avail_routines()`
    """
    for path in self.list_avail_routines(recursive=recursive, abbreviated=False):
        self.add_calc_routine(path)

add_block(block_name, type_alias, value=[])

Adds an unexpected attribute with a validator mapped by type_alias. Unexpected attributes not requiring a validator can be set directly without using this method.

Parameters:

Name Type Description Default
block_name str

new unexpected attribute name

required
type_alias str

Alias mapped to the desired validator in parsing.validation.aliases. For more information on how aliases are used, see __setattr__.

required
Source code in FoSpy/blocks/blocks.py
def add_block(self, block_name:str, type_alias:str, value=[]):
    """
    Adds an unexpected attribute with a validator mapped by `type_alias`.
    Unexpected attributes not requiring a validator can be set directly
    without using this method.

    Args:
        block_name: new unexpected attribute name
        type_alias:
            Alias mapped to the desired validator in
            [`parsing.validation.aliases`][FoSpy.parsing.validation.aliases].
            For more information on how aliases are used, see
            [`__setattr__`][FoSpy.blocks.blocks.SingleBlock.__setattr__].
    """
    if hasattr(self,block_name):
        raise ValueError(f"This object already has attribute: '{block_name}'.")
    return setattr(self, f"{block_name}${type_alias}", value)

add_calc_comment(key, comment, calc_id)

Add a calculated comment to be injected during serialization.

WARNING: This function can leave outdated calculations in comments after serialization. Recommended to use add_calc_routine() instead.

Calculated comments are for user information and will be formatted to be skipped by the parser when reading the file. This is useful for comments that should be recalculated and refreshed during saving/serialization, like weight percentages or summaries.

Parameters:

Name Type Description Default
key str

attribute to attach the calculated comment to. Comments appear above their attached attributes in FOS format.

required
comment str

comment text without comment formatting (don't include // or !)

required
calc_id str

unique identifier for the calculated comment. If it matches an existing comment (like when refreshing a value), the comment is overwritten

required
Source code in FoSpy/blocks/blocks.py
def add_calc_comment(self, key:str, comment:str, calc_id:str):
    """
    Add a calculated comment to be injected during serialization.

    WARNING: This function can leave outdated calculations in comments after
    serialization. Recommended to use `add_calc_routine()` instead.

    Calculated comments are for user information and will be formatted to be
    skipped by the parser when reading the file. This is useful for comments
    that should be recalculated and refreshed during saving/serialization,
    like weight percentages or summaries.

    Args:
        key:
            attribute to attach the calculated comment to. Comments appear
            above their attached attributes in FOS format.
        comment:
            comment text without comment formatting (don't include // or !)
        calc_id:
            unique identifier for the calculated comment. If it matches an
            existing comment (like when refreshing a value), the comment is
            overwritten

    """
    calc_comments = self._calc_comments.get(key, {})
    self._calc_comments[key] = calc_comments
    self._calc_comments[key][calc_id]=comment

add_calc_routine(path, **kwargs)

Schedules a calculated comment.

Appends a _calc_routine()-decorated function to self._calc_routines to be run at serialization.

Used to add calculated comments that should be refreshed during serialization.

Parameters:

Name Type Description Default
path str

a relative path string that can be resolved into a _calc_routine()-decorated function

required
**kwargs any

optional key word arguments to be passed to the function at path.

{}

Raises:

Type Description
TypeError

the attr or method at path is not registered as a _calc_routine

Example:

    mySyn.add_calc_routine("materials.add_weight_pcts", typ="reagent")
    ## mySyn.materials.add_weight_pcts(typ="reagent") is now scheduled
    ## to run at serialization

Source code in FoSpy/blocks/blocks.py
def add_calc_routine(self, path:str, **kwargs):
    """
    Schedules a calculated comment.

    Appends a
    [`_calc_routine()`][FoSpy.blocks._blockUtils._calc_routine]-decorated
    function to `self._calc_routines` to be run at
    [serialization][FoSpy.blocks.blocks.SingleBlock.serialize].

    Used to add calculated comments that should be refreshed during
    serialization.

    Args:
        path:
            a relative path string that can be resolved into a
            `_calc_routine()`-decorated function
        **kwargs (any):
            optional key word arguments to be passed to the function at
            path.

    Raises:
        TypeError:
            the attr or method at path is not registered as a
            _calc_routine

    Example:
    ```
        mySyn.add_calc_routine("materials.add_weight_pcts", typ="reagent")
        ## mySyn.materials.add_weight_pcts(typ="reagent") is now scheduled
        ## to run at serialization
    ```
    """

    func = self._resolve_relative_path(path)
    if not getattr(func, "_is_calc_routine", False):
        raise TypeError(f"'{path}' is not a registered calc routine.")

    self._meta.routine_paths.append(path)

    def wrapped(f=func, k=kwargs):
        return f(**k)

    self._calc_routines.append(wrapped)

add_comments(*comments)

Default behavior to be overwritten when attached to a parent block.

If a SingleBlock is stored as an attribute of another SingleBlock, this method will be overwritten by the parent's __setattr__.

Source code in FoSpy/blocks/blocks.py
def add_comments(self, *comments):
    """
    Default behavior to be overwritten when attached to a parent block.

    If a `SingleBlock` is stored as an attribute of another `SingleBlock`,
    this method will be overwritten by the parent's `__setattr__`.
    """
    keys = list(self.get_req_validators())

    keys = [k for k in keys if k != "metadata"]
    fallback = [k for k in self._key_order if k != "metadata"]
    if not (keys or fallback):
        raise ValueError("This object has not been correctly attached to a parent block "
                         "and could not identify a required key to attach to.")

    first = keys[0] if keys else fallback[0]

    self._meta.comments.setdefault(first, [])
    for comment in comments:
        self._meta.comments[first].append(comment)

add_dispatch(blockDict, dispatch_key, **kwargs) classmethod

Source code in FoSpy/blocks/attachments.py
@classmethod
def add_dispatch(cls, blockDict, dispatch_key, **kwargs):
    from .. import _errors as err

    # make sure wrapped
    _ = SingleBlock.add_dispatch(blockDict, dispatch_key, **kwargs)

    dispatch = cls.__dispatch__

    # additional location types will never be added after runtime.
    # but subclasses of AnyFile need to inherit a *copy* of AnyFile's runtime registry
    # they will all still inherit dispatch_from=Attachment

    # only cache hybrids one class at a time.
    # First call of CIFFile.add_dispatch will populate CIFFile's registry with location bases.
    finalized = dispatch.get("final", False)
    if not finalized:
        if cls is not AnyFile:
            # populate all subclasses registry with AnyFile's runtime registry
            for location, sub in AnyFile.__dispatch__["registry"].items():
                cls.register_dispatch(location)(sub)

        # mark as finalized
        dispatch["final"] = True

    registry = cls.__dispatch__["registry"]

    location = None
    # the first key that exists in blockDict is the location
    for key, loc_cls in registry.items():
        # after location found, pop redundant keys
        if location is not None:
            blockDict.pop(key, None)
        elif key in blockDict:
            location, LocationClass = key, loc_cls

    if location is None:
        raise err.MissingPropertyError(" or ".join(registry.keys()), cls, blockDict=blockDict)

    # initial copied registry entries are not subclasses of this class, but will be
    # re-mapped as hybrids on the first pass of a matching location.
    if not issubclass(LocationClass, cls):
        # all location base classes are cached from AnyFile on first call,
        # but location hybrid classes are only created and cached on the first call with their location.
        class LocatedFileType(LocationClass, cls):
            pass

        cap_loc = location.capitalize()

        LocatedFileType.__name__ = cap_loc + cls.__name__
        LocatedFileType.__qualname__ = cap_loc + cls.__qualname__
        LocatedFileType.__module__ = cls.__module__

        LocatedFileType = SingleBlock.register_dispatch(
            location, from_parent=cls,
            # defaults should make sure that the location key is present
            defaults={location: None})(LocatedFileType)

        # overwrite current registry
        cls.register_dispatch(location)(LocatedFileType)

    # location key ("path" or "embedded") is already in blockDict, but caching it at
    # "_location" makes it easy for base dispatcher to use in normal routine.
    # this is injected to blockDict before passing back to dispatch
    return {dispatch_key: location}

build_req_validators() classmethod

Builds required keys and validators mapped to subclass.

Walks all parent classes and builds a map of all keys that are required during __init__, and their respective validation routines. Subclasses are mapped to expected keys and validations in parsing.validation. Subclass validations override parent classes when applicable.

Returns:

Name Type Description
merged dict

Maps required keys to validation routines. Routines may be a class constructor or a func taking one arg.

Example:

>>> SingleBlock.build_req_validators()
{
    "name": str,
    "type": str,
    "formula": ChemFormula, # class constructor
    "supplier": str,
    "cas": str,
    "form": str,
    "env": str,
    "ratio": validators.material.ratio # validator function
}

Source code in FoSpy/blocks/blocks.py
@classmethod
def build_req_validators(cls):
    """
    Builds required keys and validators mapped to subclass.

    Walks all parent classes and builds a map of all keys that are required
    during `__init__`, and their respective validation routines. Subclasses
    are mapped to expected keys and validations in
    [`parsing.validation`][FoSpy.parsing.validation]. Subclass validations
    override parent classes when applicable.

    Returns:
        merged (dict):
            Maps required keys to validation routines. Routines may be
            a class constructor or a func taking one arg.
    Example:
        ``` 
        >>> SingleBlock.build_req_validators()
        {
            "name": str,
            "type": str,
            "formula": ChemFormula, # class constructor
            "supplier": str,
            "cas": str,
            "form": str,
            "env": str,
            "ratio": validators.material.ratio # validator function
        }
        ```
    """
    from ..parsing.validation import required_keys
    from ._blockUtils import _get_prop_mro, _merge_vals
    merged = {}
    # mro = list(reversed(cls.__mro__))
    # for i, base in enumerate(mro):
    #     base_reqs = required_keys.get(base,{})
    #     for key, validator in base_reqs.items():
    #         # allow subclasses to remove parent requirements.
    #         if not validator:
    #             merged.pop(key, None)
    #         else:
    #             merged[key] = validator

    req_mro = _get_prop_mro(cls, required_keys)
    for i in range(len(req_mro)):
        merged = _merge_vals(merged, req_mro, i)

    merged.pop("__all__")

    return merged

build_validators() classmethod

Builds expected keys and validators mapped to subclass.

Walks all parent classes and builds a map of all keys that are expected (required or optional), and their respective validation routines. Subclasses are mapped to keys and validations in parsing.validation. Subclass validations override parent classes when applicable.

See build_req_validators

Source code in FoSpy/blocks/blocks.py
@classmethod
def build_validators(cls):
    """
    Builds expected keys and validators mapped to subclass.

    Walks all parent classes and builds a map of all keys that are expected
    (required or optional), and their respective validation routines.
    Subclasses are mapped to keys and validations in
    [`parsing.validation`][FoSpy.parsing.validation]. Subclass validations
    override parent classes when applicable.

    See
    [`build_req_validators`][FoSpy.blocks.blocks.SingleBlock.build_req_validators]
    """
    from ..parsing.validation import required_keys, optional_keys
    from ._blockUtils import _merge_vals, _get_prop_mro
    from .._docs.properties import _validator_rules
    merged = {}
    # for base in reversed(cls.__mro__):
    #     for key_set in (required_keys, optional_keys):
    #         base_reqs = key_set.get(base,{})
    #         for key, validator in base_reqs.items():
    #             # allow subclasses to remove parent requirements.
    #             if validator is False:
    #                 merged.pop(key, None)
    #             else:
    #                 merged[key] = validator
    req_mro = _get_prop_mro(cls, required_keys)
    opt_mro = _get_prop_mro(cls, optional_keys)

    for i in range(len(req_mro)): # req_mro and opt_mro are the same length
        merged = _merge_vals(merged, req_mro, i)
        merged = _merge_vals(merged, opt_mro, i)

    universal_val = merged.pop("__all__")

    @_validator_rules(inherit_from=universal_val)
    def universal_val_method(cls, *_, _m=universal_val, **__):
        return _m(*_, **__)

    cls.universal_val = universal_val_method

    return merged

clear_all_comments()

Source code in FoSpy/blocks/blocks.py
def clear_all_comments(self):
    self._meta.comments = {}
    for attr, val in self.__dict__.items():
        if attr.startswith("_") or attr in self._reserved:
            continue
        if hasattr(val, "clear_all_comments"):
            val.clear_all_comments()

clear_comments()

Clear comments attached to top-level attributes only.

Source code in FoSpy/blocks/blocks.py
def clear_comments(self):
    """
    Clear comments attached to top-level attributes only.
    """
    self._meta.comments = {}

copy()

Returns a deep-copy of self by serializing and reconstructing.

_calc_comments are not preserved during copy, but _calc_routines are. This prevents mutation of the comments when reconstructing.

Source code in FoSpy/blocks/blocks.py
def copy(self):
    """
    Returns a deep-copy of `self` by serializing and reconstructing.

    _calc_comments are not preserved during copy, but _calc_routines are.
    This prevents mutation of the comments when reconstructing.
    """
    cls = type(self)
    # cache calculated comments before serializing
    c_cmts = self._calc_comments.copy()

    serial = self.serialize(keepListType=True)

    new_obj =  cls(serial)

    # restore cached calc comments
    self._calc_comments = c_cmts

    return new_obj

default_key_order(deep=False)

Set to default attribute order for serialization.

Rearrange attribute order to the default order assigned by build_validators

Parameters:

Name Type Description Default
deep bool

When true, recursively calls default_key_order on any other SingleBlock objects stored in attributes.

False
Source code in FoSpy/blocks/blocks.py
def default_key_order(self, deep:bool=False):
    """
    Set to default attribute order for serialization.

    Rearrange attribute order to the default order assigned by
    [`build_validators`][FoSpy.blocks.blocks.SingleBlock.build_validators]

    Args:
        deep:
            When true, recursively calls `default_key_order` on any other
            `SingleBlock` objects stored in attributes.
    """
    new_order = []
    for key in self.get_validators():
        if key != "ext" and key in self.serialize(shallow=True):
            new_order.append(key)
    for key in self._key_order:
        if key not in new_order:
            new_order.append(key)
    self._key_order = new_order
    self._meta_to_front()

    if deep:
        for name, obj in self.__dict__.items():
            if not name.startswith("_") and hasattr(obj, "default_key_order"):
                obj.default_key_order(deep=True)

dispatch_subclass(*args, **kwargs) classmethod

Source code in FoSpy/blocks/blocks.py
@classmethod
def dispatch_subclass(cls, *args, **kwargs):
    # fallback.
    # overridden by setup_dispatch decorator
    return cls

dispatcher(dispatch_method) staticmethod

Decorate a classmethod to dispatch to other subclasses.

Not normally used directly. See setup_dispatch decorator.

Source code in FoSpy/blocks/blocks.py
@staticmethod
def dispatcher(dispatch_method: Callable[..., dict])->classmethod:
    """
    Decorate a classmethod to dispatch to other subclasses.

    Not normally used directly. See
    [`setup_dispatch`][FoSpy.blocks.blocks.SingleBlock.setup_dispatch]
    decorator.
    """

    @classmethod
    def dispatch_subclass(cls:type[BlockType], blockDict, _add_defaults=False, **kwargs):
        from .. import _errors as err
        for_template = kwargs.get("for_template", False)

        block_dispatch = blockDict.setdefault("__dispatch__", {})
        visited = block_dispatch.setdefault("visited", [])
        cls_dispatch = getattr(cls, "__dispatch__", None)
        # shorthand for keying dispatch parameters
        d=cls_dispatch

        if d['dispatch_from'] in visited and (
            cls in visited or
            d is None or
            d['from_key'] is None):
            return cls
        try:
            blockDict = dispatch_method(cls, blockDict, add_defaults=_add_defaults, **kwargs)
        except Exception as e:
            if not for_template:
                raise e

        visited.append(cls)

        if d['dispatch_from'] not in visited:
            blockDict.pop("__dispatch__", None)
            return d['dispatch_from'].dispatch_subclass(blockDict, _add_defaults=_add_defaults, **kwargs)


        dispatch_val = block_dispatch.get(d['from_key'],
                            blockDict.get(d['from_key'], None))

        dispatched_cls = d['registry'].get(dispatch_val, d['registry'].get(None, cls))

        if dispatched_cls is cls and not d['allow_self']:
            if not for_template:
                raise err.BlockDispatchError(
                    f"The following blockDict was dispatched to {cls.__name__} "
                    "but could not be dispatched further. "
                    f"{cls.__name__} blocks are not allowed without a subclass.")
            return cls

        return dispatched_cls.dispatch_subclass(blockDict, **kwargs)
    return dispatch_subclass

enforce_subtype(subcls, **kwargs) classmethod

Source code in FoSpy/blocks/attachments.py
@classmethod
def enforce_subtype(cls, subcls, **kwargs):
    raise DeprecationWarning("Attachments no longer enforce subtype through this method. "
                             "Simply spec the validator as the enforced subtype instead.")

fill_staged_template(prop_name, **kwargs)

Source code in FoSpy/blocks/blocks.py
def fill_staged_template(self, prop_name, **kwargs):
    from .template import TemplateBlock

    prop_key = prop_name.split("$")[0] if "$" in prop_name else prop_name

    template = self._staged_templates.pop(prop_key, None)
    if template is None:
        prop_name, _ = self.stage_template(prop_name)
        return self.fill_staged_template(prop_name, **kwargs)

    prop_name = prop_key

    filled = template.fill(staged=True,**kwargs)

    if isinstance(filled, TemplateBlock):
        return self.stage_template(prop_name, filled)

    try:
        setattr(self, prop_name, filled)
    except Exception as e:
        raise Exception(f"Template was filled but could not be assigned {prop_name}") from e

    filled = getattr(self, prop_name)

    if isinstance(self, TemplateBlock):
        self.fill()

    return prop_name, filled

find_attachments()

Source code in FoSpy/blocks/attachments.py
def find_attachments(self):
    attachments = super().find_attachments()
    if self not in attachments:
        attachments.append(self)

    return attachments

find_fileblock()

Finds the parent file object.

Walks upward through _parent_block attributes until a FileBlock instance is found and returns that instance.

Source code in FoSpy/blocks/blocks.py
def find_fileblock(self):
    """
    Finds the parent file object.

    Walks upward through `_parent_block` attributes until a
    [`FileBlock`][FoSpy.blocks.files.FileBlock] instance is found and
    returns that instance.
    """
    from .files import FileBlock
    from .._errors import FileBlockNotFoundError

    blk = self
    while blk is not None:
        if isinstance(blk, FileBlock):
            return blk
        if hasattr(blk,"_parent_block"):
            blk = blk._parent_block
        else:
            blk = None
    raise FileBlockNotFoundError("Could not find a FileBlock containing the current object")

find_tempdir()

Find the parent file object's temporary directory.

Finds the temporary directory created by the FileBlock instance containing this block as one of its attributes.

Returns:

Name Type Description
tempdir tempfile.TemporaryDirectory

The temporary directory created by the parent file object

Source code in FoSpy/blocks/blocks.py
def find_tempdir(self):
    """
    Find the parent file object's temporary directory.

    Finds the temporary directory created by the
    [`FileBlock`][FoSpy.blocks.files.FileBlock] instance containing this
    block as one of its attributes. 

    Returns:
        tempdir (tempfile.TemporaryDirectory):
            The temporary directory created by the parent file object
    """
    fileblock = self.find_fileblock()
    if hasattr(fileblock, "_tempdir"):
        return fileblock._tempdir
    else:
        raise AttributeError("Could not find a temporary directory attached to this object's FileBlock")

find_temppath()

Find the parent file object's temporary directory path.

Similar to find_tempdir but returns the corresponding pathlib.Path object instead.

Source code in FoSpy/blocks/blocks.py
def find_temppath(self):
    """
    Find the parent file object's temporary directory path.

    Similar to [`find_tempdir`][FoSpy.blocks.blocks.Block.find_tempdir] but
    returns the corresponding `pathlib.Path` object instead.
    """
    fileblock = self.find_fileblock()
    if hasattr(fileblock, "_temppath"):
        return fileblock._temppath
    if hasattr(fileblock, "_temppdir"):
        raise AttributeError("This object's FileBlock has a temporary directory but no path mapped to it. "
                             "Use obj.find_tempdir() instead")
    raise AttributeError("Could not find a temporary directory object or path "
                         "attached to this object's FileBlock.")

get_id()

Returns an easily recognizable identifier for self. Non-unique.

Source code in FoSpy/blocks/blocks.py
def get_id(self):
    """Returns an easily recognizable identifier for self. Non-unique."""
    id_txt = str(getattr(self, self._id_key)) if self._id_key is not None else type(self).__name__
    return self._id_key, id_txt

get_parent_prop()

Source code in FoSpy/blocks/blocks.py
def get_parent_prop(self):
    if not hasattr(self, "_parent_block"):
        return None
    parent_blk = self._parent_block

    if isinstance(parent_blk, SingleBlock):
        for prop, val in parent_blk.get_prop_dict().items():
            if val is self:
                return prop

        raise err.FoSpyStructureError(f"Block {self} points to a parent block {parent_blk} that does not contain it as a property.")

    elif isinstance(parent_blk, ListBlock):
        return f"[{parent_blk.get_idx(self)}]"

    raise err.FoSpyStructureError(f"Block {self} has an unknown parent block type: {type(parent_blk)}")

get_pattern(engine_name=None)

Source code in FoSpy/blocks/attachments.py
def get_pattern(self, engine_name=None):
    engine = self._get_engine(engine_name=engine_name)

    return engine.get_pattern()

get_peaks(engine_name=None)

Source code in FoSpy/blocks/attachments.py
def get_peaks(self, engine_name=None):
    engine = self._get_engine(engine_name=engine_name)

    return engine.get_peaks()

get_prop_dict()

Returns a dictionary mapping property names to their live object values.

Source code in FoSpy/blocks/blocks.py
def get_prop_dict(self):
    """Returns a dictionary mapping property names to their live object values."""
    serial = self.serialize(shallow=True, clean=True)
    out = {}
    for prop in serial:
        if "$" in prop:
            prop = prop.split("$")[0]

        # guard for when templateblocks add staged templates to their serial
        if hasattr(self, prop):
            out[prop] = getattr(self, prop)

    return out

get_prop_path()

Source code in FoSpy/blocks/blocks.py
def get_prop_path(self):
    from .files import FileBlock

    if not hasattr(self, "_parent_block"):
        if isinstance(self, FileBlock):
            root_path = f"<{str(self.get_file_name())}>"
        else:
            root_path = f"<Root {type(self).__name__}"
            if isinstance(self, SingleBlock):
                id_key, id_txt = self.get_id()
                if id_key is not None:
                    root_path += f" ({id_key}={id_txt})"
            root_path += ">"
        return root_path

    parent_path = self._parent_block.get_prop_path()
    parent_prop = self.get_parent_prop()

    if "[" not in parent_prop:
        return parent_path + "." + parent_prop

    return parent_path + parent_prop

get_req_validators()

Overrides class validators with any renamed properties.

Similar to class method: build_req_validators, but uses _rename_validators to align any renamed properties with their original validators.

Source code in FoSpy/blocks/blocks.py
def get_req_validators(self):
    """
    Overrides class validators with any renamed properties.

    Similar to class method:
    [`build_req_validators`][FoSpy.blocks.blocks.SingleBlock.build_req_validators],
    but uses
    [`_rename_validators`][FoSpy.blocks.blocks.SingleBlock._rename_validators]
    to align any renamed properties with their original validators.
    """
    return self._rename_validators(self.build_req_validators())

get_validators()

Overrides class validators with any renamed properties.

Similar to class method: build_validators, but uses _rename_validators to align any renamed properties with their original validators. Also adds any optional key overrides added by key$alias syntax.

Returns:

Name Type Description
vals dict

maps expected keys to validation routines.

Source code in FoSpy/blocks/blocks.py
def get_validators(self):
    """
    Overrides class validators with any renamed properties.

    Similar to class method:
    [`build_validators`][FoSpy.blocks.blocks.SingleBlock.build_validators],
    but uses
    [`_rename_validators`][FoSpy.blocks.blocks.SingleBlock._rename_validators]
    to align any renamed properties with their original validators. Also
    adds any optional key overrides added by key$alias syntax.

    Returns:
        vals (dict): maps expected keys to validation routines.
    """
    vals = self._rename_validators(self.build_validators())
    if hasattr(self, "_key_overrides"):
        for key, val in self._key_overrides.items():
            vals[key] = val
    return vals

has_staged()

Source code in FoSpy/blocks/blocks.py
def has_staged(self):
    if len(self._staged_templates) > 0:
        return True

    for val in self.get_prop_dict().values():
        if hasattr(val, "has_staged") and val.has_staged():
            return True

    return False

inject_defaults(blockDict, *args, **kwargs) classmethod

Source code in FoSpy/blocks/blocks.py
@classmethod
def inject_defaults(cls, blockDict, *args, **kwargs):
    # fallback.
    # overridden by setup_dispatch decorator
    return blockDict

inspect() classmethod

Source code in FoSpy/blocks/blocks.py
@classmethod
def inspect(self):
    # for breaking to debugger from gui
    raise Exception("put a break point here")

key_to_idx(key, idx)

Reorder attributes for serialization.

Move any attribute name to a specific index in _key_order for serialization order. The invisible "metadata" key is always refreshed to the front of the list, so indices are effectively 1-based.

Parameters:

Name Type Description Default
key str

name of attribute to reorder

required
idx int

new index in _key_order

required
Source code in FoSpy/blocks/blocks.py
def key_to_idx(self, key:str, idx:int):
    """
    Reorder attributes for serialization.

    Move any attribute name to a specific index in `_key_order` for
    serialization order. The invisible `"metadata"` key is always refreshed
    to the front of the list, so indices are effectively 1-based.

    Args:
        key: name of attribute to reorder
        idx: new index in _key_order
    """
    self._meta_to_front()
    try:
        old_idx = self._key_order.index(key)
        self._key_order.pop(old_idx)
    # TODO: Better handling
    except Exception:
        pass
    self._key_order.insert(idx, key)

keys_to_end(*args)

Reorder attributes for serialization.

Move any attribute names in *args to the end of _key_order to be serialized last. Order within *args is maintained in result.

Source code in FoSpy/blocks/blocks.py
def keys_to_end(self, *args):
    """
    Reorder attributes for serialization.

    Move any attribute names in `*args` to the end of _key_order to be
    serialized last. Order within `*args` is maintained in result.
    """
    def remove_alias(key):
        return key.split("$")[0] if "$" in key else key
    for key in self.serialize(shallow=True):
        if not key.startswith("_") and remove_alias(key) not in self._key_order:
            self._key_order.append(remove_alias(key))
    for key in args:
        try:
            idx = self._key_order.index(key)
            self._key_order.pop(idx)
        # TODO: Better handling
        except Exception:
            pass
        self._key_order.append(key)
    self._meta_to_front()

keys_to_front(*args)

Reorder attributes for serialization.

Move any attribute names in *args to the front of _key_order to be serialized first. Order within *args is maintained in result.

Source code in FoSpy/blocks/blocks.py
def keys_to_front(self,*args):
    """
    Reorder attributes for serialization.

    Move any attribute names in `*args` to the front of _key_order to be
    serialized first. Order within `*args` is maintained in result.
    """
    try:
        meta_idx = args.index("metadata")
        args.pop(meta_idx)
    # TODO: Better handling
    except Exception:
        pass

    new_order = []
    for key in args:
        new_order.append(key)
    for key in self._key_order:
        if key not in new_order:
            new_order.append(key)
    self._key_order = new_order
    self._meta_to_front()

list_avail_routines(recursive=False, prefix='', abbreviated=False)

Lists all calc routines available to be added to self._calc_routines.

Non-abbreviated calc routine strings can be passed directly to self.add_calc_routine()

Parameters:

Name Type Description Default
recursive bool

If True, recursively walks all attributes and appends results from self.attr.list_avail_routines() to result. Otherwise only identifies methods of self.

False
prefix str

Used during recursion to build relative paths

''
abbreviated bool

optionally abbreviate recursively repeated routines for similar objects into one line. This line cannot be passed to self.add_calc_routine()

False

Returns:

Name Type Description
routines list

list of strings describing _calc_routine-decorated methods. Non-abbreviated calc routine strings can be passed directly to self.add_calc_routine()

Example:

    mySyn.list_avail_routines()
    ## returns []
    mySyn.list_avail_routines(recursive=True)
    ## returns [
    ##     'reaction.add_nom_MW',
    ##     'materials.add_weight_pcts',
    ##     'materials[0].add_MW',
    ##     'materials[1].add_MW',
    ##     ... 6 total materials with the same calc_routine
    ##     'materials[5].add_MW'
    ## ]
    mySyn.list_avail_routines(recursive=True, abbreviated=True)
    ## returns [
    ##     'reaction.add_nom_MW',
    ##     'materials.add_weight_pcts',
    ##     'materials[i].add_MW; i = [0, 1, 2, 3, 4, 5]'
    ## ]

Source code in FoSpy/blocks/blocks.py
def list_avail_routines(self, recursive:bool=False, prefix:str="", abbreviated:bool=False):
    """
    Lists all calc routines available to be added to `self._calc_routines`.

    Non-abbreviated calc routine strings can be passed directly to
    `self.add_calc_routine()`

    Args:
        recursive:
            If True, recursively walks all attributes and appends results
            from `self.attr.list_avail_routines()` to result. Otherwise only
            identifies methods of `self`.

        prefix: Used during recursion to build relative paths
        abbreviated:
            optionally abbreviate recursively repeated routines for similar
            objects into one line. This line cannot be passed to
            `self.add_calc_routine()`

    Returns:
        routines (list): 
            list of strings describing _calc_routine-decorated methods.
            Non-abbreviated calc routine strings can be passed directly to
            `self.add_calc_routine()`

    Example:
    ```
        mySyn.list_avail_routines()
        ## returns []
        mySyn.list_avail_routines(recursive=True)
        ## returns [
        ##     'reaction.add_nom_MW',
        ##     'materials.add_weight_pcts',
        ##     'materials[0].add_MW',
        ##     'materials[1].add_MW',
        ##     ... 6 total materials with the same calc_routine
        ##     'materials[5].add_MW'
        ## ]
        mySyn.list_avail_routines(recursive=True, abbreviated=True)
        ## returns [
        ##     'reaction.add_nom_MW',
        ##     'materials.add_weight_pcts',
        ##     'materials[i].add_MW; i = [0, 1, 2, 3, 4, 5]'
        ## ]
    ```
    """
    routines = []

    # Local routines
    for name in dir(self):
        attr = getattr(self, name)
        if callable(attr) and getattr(attr, "_is_calc_routine", False):
            routines.append(prefix + name)

    if recursive:
        for attr, val in self.__dict__.items():
            if attr.startswith("_"):
                continue

            # Recurse into child blocks
            if hasattr(val, "list_avail_routines"):
                child_prefix = f"{prefix}{attr}."
                routines.extend(val.list_avail_routines(True, child_prefix, abbreviated))

    return routines

make_template(template_name, *args)

Converts self into a template of its original subclass.

Returns a copy of self as a template of its original subclass, with specified fields replaced with template types. See TemplateClass for more information on template generation.

Parameters:

Name Type Description Default
template_name str

All templates require an identifying name.

required
*args str

properties to clear and replace with template types.

()
Source code in FoSpy/blocks/blocks.py
def make_template(self,template_name:str,*args:str):
    """
    Converts `self` into a template of its original subclass.

    Returns a copy of `self` as a template of its original subclass, with
    specified fields replaced with template types. See
    [`TemplateClass`][FoSpy.blocks.blocks.SingleBlock.TemplateClass] for
    more information on template generation.

    Args:
        template_name: All templates require an identifying name.
        *args: properties to clear and replace with template types.
    """

    from ..parsing.format_fos import format_field

    serial = self.serialize(keepListType=True)
    validators = self.get_validators()
    for key in args:
        val = validators.get(key, None)
        if isinstance(val,type) and (issubclass(val, SingleBlock) or issubclass(val, ListBlock)):
            serial.setdefault(key, [{}])
        else:
            serial[key] = format_field("template")
    serial["template_name"] = template_name
    return self.TemplateClass(*args)(serial)

new_engine(engine_name=None)

Source code in FoSpy/blocks/attachments.py
def new_engine(self, engine_name=None):
    from ..config import values as cfg
    from ..plotting.diffraction.engines import ENGINES
    if engine_name is None:
        engine_name = cfg.get("diffraction.default_engine")
    return ENGINES[engine_name](self._get_filepath())

print_summary(mode='cli') classmethod

Source code in FoSpy/blocks/blocks.py
@classmethod
def print_summary(cls, mode="cli"):
    from .._docs.properties import get_summary

    print(get_summary(cls, mode=mode))

quick_pattern(subprocess=False)

Source code in FoSpy/blocks/attachments.py
def quick_pattern(self,subprocess=False):
    from ..plotting._utils import _quick_pattern

    df = self.get_pattern()

    x,y = df.columns[:2]

    tth, intensity = df[x].to_numpy(), df[y].to_numpy()

    if subprocess:
        return self._subprocess(_quick_pattern, args=(tth, intensity))

    return _quick_pattern(tth, intensity)

reflex(serialize=True, clean=False, **kwargs) classmethod

Source code in FoSpy/blocks/attachments.py
@classmethod
def reflex(cls, serialize=True, clean=False, **kwargs:dict):
    from .template import TemplateField
    if "file_name" not in kwargs:
        kwargs["file_name"] = TemplateField.serialize()
        kwargs.pop("path", None)
        add_embedded = "embedded" not in kwargs

    elif not any(k in kwargs for k in ("path", "embedded")):
        add_embedded = True

    if add_embedded:
        kwargs["embedded"] = TemplateField.serialize()

    return super().reflex(serialize=serialize, clean=clean, **kwargs)

refresh_attachments(new_copy=None, overwrite=None, **kwargs)

Source code in FoSpy/blocks/blocks.py
def refresh_attachments(self, new_copy=None, overwrite=None, **kwargs):
    from .attachments import Attachment

    if new_copy is None:
        new_copy = self._att_new_copy
    if overwrite is None:
        overwrite = self._att_overwrite

    for propDict in self.__dict__, self.ext.__dict__:
        for key, val in propDict.items():
            if key.startswith("_") or key in self._reserved:
                continue
            if hasattr(val, "refresh_attachments"):
                val.refresh_attachments(new_copy=new_copy, overwrite=overwrite, **kwargs)
            elif isinstance(val, Attachment) and hasattr(val, "refresh"):
                val.refresh(new_copy=new_copy, overwrite=overwrite, **kwargs)

register_dispatch(registry_val, from_parent=None, **kwargs) classmethod

Source code in FoSpy/blocks/attachments.py
@classmethod
def register_dispatch(cls, registry_val, from_parent=None, **kwargs):
    from_parent = from_parent or cls
    # {registry_val:None} guarantees that "path" or "embedded" are present in blockDict to be found by add_dispatch
    return SingleBlock.register_dispatch(registry_val, from_parent=from_parent, defaults={registry_val:None}, **kwargs)

rename_block(old, new)

Source code in FoSpy/blocks/blocks.py
def rename_block(self, old, new):
    validators = self.get_validators()
    req = self.get_req_validators()
    if any(name.startswith("_") for name in (old, new)):
        raise ValueError("You cannot set private attributes (starting with '_') using obj.rename_block()")

    if old in req and new in validators:
        raise ValueError(f"You cannot rename '{old}' to '{new}'. '{old}' is a required property that "
                            f"can only be renamed to an unregistered key; '{new}' is already registered "
                            "as an expected property.")

    if hasattr(self, new):
        raise ValueError(f"'{new}' is already a property for this object, you cannot overwrite it with "
                         "obj.rename_block()")

    if "rename" in (old, new):
        raise ValueError("obj.rename property cannot be set or changed by obj.rename_block()")

    if hasattr(self, "rename") and hasattr(self.rename, old):
        old = getattr(self.rename, old)()

    if old in self._key_overrides:
        val = self._key_overrides.pop(old)
        self._key_overrides[new] = val

    else:
        if not hasattr(self,"rename"):
            self.rename = {}

        rename_dict = self.rename_dict()
        rename_from = {v:k for k,v in rename_dict.items()}
        if old in rename_from:
            base = rename_from[old]
        else:
            base = old

        _debug.msg(f"Registering '{base}':'{new}' into rename block")
        setattr(self.rename, base, new)
    _debug.msg(f"Moving '{old}' over to '{new}'.")
    setattr(self,new,getattr(self, old))
    delattr(self,old)

    try:
        idx = self._key_order.index(old)
        self._key_order[idx] = new
    # TODO: Better handling
    except Exception:
        self._key_order.append(new)

rename_dict()

Source code in FoSpy/blocks/blocks.py
def rename_dict(self):
    if not hasattr(self, "rename"):
        return {}
    return self.rename.serialize(shallow=True, clean=True)

serialize(keepListType=False, shallow=False, clean=False, **kwargs)

Return a recursively serialized dict representation of self.

Fully serialized SingleBlocks are a single dict that can be passed to another constructor or emitted into lines for a FOS file. Serialized values at any nest level are either dicts, lists, or strings to allow full type-coersion when reconstructing or simplified emission when writing files.

Serialized dict is deep copied to prevent object mutation.

Parameters:

Name Type Description Default
keepListType bool

When True, maintains its current FOS printing mode (looped keys or explicit key:value lines), instead of explicit default

False
shallow bool

When True, no recursive serialization occurs. Recommended when serialization is used only to inspect top-level keys.

False
clean bool

When True, no FOS format read/write metadata is included in the serial. Recommended for sending output to other formats like JSON.

False

Private attributes starting with "_" are either skipped or unpacked in special cases:

  • _key_order: attributes are added to the serialized dict in the order they appear in this list.

  • _calc_comments: calculated comments are attached to their mapped attribute after serialization to avoid mutation of object comments

  • _calc_routines: A list of functions scheduled to be called right before serialization to update _calc_comments. Scheduling calc routines ensures that their calculated values are up-to-date.

  • _meta: attributes of this container are given their own private _keys mapped by FoSpy.parsing.syntax.meta_keys in the serialized dict.

  • _key_overrides: per-instance override mapping that tracks which unexpected attributes require $alias suffixes.

  • _aliases: maps attribute names to alias tags used to emit $alias suffixed keys.

  • _reserved: attribute names in reserved are non-private attributes which should not be serialized. This usually applies to the ext attribute or methods attached after construction.

Source code in FoSpy/blocks/blocks.py
def serialize(self, keepListType:bool=False, shallow:bool=False, clean:bool=False, **kwargs):
    """
    Return a recursively serialized `dict` representation of `self`.

    Fully serialized `SingleBlock`s are a single dict that can be passed to
    another constructor or emitted into lines for a FOS file. Serialized
    values at any nest level are either dicts, lists, or strings to allow
    full type-coersion when reconstructing or simplified emission when
    writing files.

    Serialized dict is deep copied to prevent object mutation.

    Args:
        keepListType:
            When True, maintains its current FOS printing mode (looped keys
            or explicit key:value lines), instead of explicit default

        shallow:
            When True, no recursive serialization occurs. Recommended when
            serialization is used only to inspect top-level keys.

        clean:
            When True, no FOS format read/write metadata is included in the
            serial. Recommended for sending output to other formats like
            JSON.

    Private attributes starting with "_" are either skipped or unpacked in
    special cases:

    * `_key_order`:
        attributes are added to the serialized dict in the order they
        appear in this list.

    * `_calc_comments`:
        calculated comments are attached to their mapped attribute after
        serialization to avoid mutation of object comments

    * `_calc_routines`:
        A list of functions scheduled to be called right before
        serialization to update _calc_comments. Scheduling calc routines
        ensures that their calculated values are up-to-date.

    * `_meta`:
        attributes of this container are given their own private `_key`s
        mapped by `FoSpy.parsing.syntax.meta_keys` in the serialized
        dict.

    * `_key_overrides`:
        per-instance override mapping that tracks which unexpected
        attributes require $alias suffixes.

    * `_aliases`:
        maps attribute names to alias tags used to emit $alias suffixed
        keys.

    * `_reserved`:
        attribute names in reserved are non-private attributes which
        should *not* be serialized. This usually applies to the `ext`
        attribute or methods attached after construction.
    """
    from copy import deepcopy
    from ..parsing.format_fos import format_calc_comment
    from .template import TemplateBlock

    val_to_alias = {v:k for k,v in self._aliases.items()}

    all_attrs = {}
    out = {}

    for routine in self._calc_routines:
        routine()

    def add_alias(key):
        if key in self._key_overrides:
            alias = val_to_alias[self._key_overrides[key]]
            return f"{key}${alias}"
        return key


    def try_serial(obj):
        if isinstance(obj, SimpleWrapper):
            obj = obj()
        serialize = getattr(obj, "serialize", None)
        if callable(serialize) and not shallow:
            return obj.serialize(clean=clean)
        if isinstance(obj, list):
            return [try_serial(item) for item in obj]
        if isinstance(obj, dict):
            return {k:try_serial(v) for k,v in obj.items()}
        return str(obj)

    for attr,val in self.__dict__.items():
        if attr == "ext" and val is not None:
            for ext_attr, ext_val in val.__dict__.items():
                all_attrs[ext_attr] = ext_val
        elif not (attr.startswith("_") or attr in self._reserved):
            all_attrs[attr] = val


    for key in self._key_order:
        if key in all_attrs:
            val = all_attrs.pop(key)
            out[add_alias(key)] = try_serial(val)

    for key, val in all_attrs.items():
        out[add_alias(key)] = try_serial(val)

    for attr, key in mk.items():
        try:
            k = md[key].copy()
        except AttributeError:
            k = md[key]
        val = getattr(self._meta,attr,k)
        out[key] = val

    comments = {}
    for key, comment_list in out[mk["comments"]].items():
        comments[add_alias(key)] = comment_list
    out[mk["comments"]] = comments

    out = deepcopy(out)

    # _debug.pmsg(self._calc_comments)
    for key, comments in self._calc_comments.items():
        for comment in comments.values():
            out[mk["comments"]].setdefault(add_alias(key),[])
            out[mk["comments"]][add_alias(key)].append(format_calc_comment(comment))

    if not keepListType:
        out[mk["list_type"]] = "explicit"

    if "template_name" in out and not isinstance(self, TemplateBlock):
        out.pop("template_name")

    if clean:
        scan = out.copy()
        for key, val in scan.items():
            if key.startswith("_") or val is None:
                out.pop(key)

    if not any(k for k in out.get("rename", {}) if not k.startswith("_")):
        out.pop("rename", None)

    return out

set_dispatch(value=None, from_parent=None, from_key=None, allow_self=None) classmethod

Source code in FoSpy/blocks/blocks.py
@classmethod
def set_dispatch(cls, value=None, from_parent=None, from_key=None, allow_self=None):

    # Abstract classes are sometimes made without SingleBlock in MRO
    if from_parent is not None:
        target_cls = from_parent
    else:
        target_cls = cls

    if "dispatch" not in target_cls.__dict__:
        target_cls.dispatch = {}

    if from_key is not None:
        target_cls.dispatch_key = from_key

    if allow_self is not None:
        target_cls.dispatch_allow_self = allow_self

    def dispatched_cls(subcls, v=value, _cls=target_cls):
        subcls.dispatch_from = _cls
        _cls.dispatch[v] = subcls
        return subcls
    return dispatched_cls

setup_dispatch(from_key=None, allow_self=True, _dispatch_from=None, _defaults={}) staticmethod

Decorate a class to dispatch to other classes during construction.

The decorated class's add_dispatch method will be wrapped into a new method, dispatch_subclass, which is decorator as a dispatcher. The parent class's from_key is found in the blockDict passed to the constructor, and the value mapped to from_key is mapped to dispatchable subclasses in the registry.

add_dispatch returns a dictionary of values that are injected into the blockDict, either to be detected by dispatch, or to be delegated to the constructor.

This decorator should only be used directly for the start of a dispatch chain. For later dispatches, use register_dispatch

Parameters:

Name Type Description Default
cls SingleBlock subclass

The class to be decorated. If provided, the decorator is most likely being called as a bare decorator. Otherwise, the decorators is being called with other keyword arguments and returns the modified decorator.

None
from_key str

The key to be located in the blockDict after optional injection by add_dispatch. Private from_keys will be injected and located under the __dispatch__ key which is popped before final construction.

None
allow_self bool

When True, the decorated class will dispatch to itself if no subclasses can be found. When False, error is raised during construction if dispatchable subclass is not found.

True
_dispatch_from SingleBlock subclass

To be passed only by register_dispatch, which decorates subclasses to populate this class's registry. Identifies the parent class that the constructor must start at. If not provided, the decorated class is assumed to be the start of a dispatch chain.

None
_defaults dict

To be passed only by register_dispatch, which decorates subclasses to populat this class's registry. Provides default values that should be injected into the blockDict when trying to guarantee dispatching to the decorated class (usually by a template constructor).

{}
Source code in FoSpy/blocks/blocks.py
@staticmethod
def setup_dispatch(cls:type[BlockType]=None,
    from_key=None,
    allow_self=True,
    _dispatch_from=None,
    _defaults={}
):
    """
    Decorate a class to dispatch to other classes during construction.

    The decorated class's [`add_dispatch`
    method][FoSpy.blocks.blocks.SingleBlock.add_dispatch] will be wrapped
    into a new method,
    [`dispatch_subclass`][FoSpy.blocks.blocks.SingleBlock.dispatch_subclass],
    which is decorator as a
    [`dispatcher`][FoSpy.blocks.blocks.SingleBlock.dispatcher]. The parent
    class's `from_key` is found in the blockDict passed to the constructor,
    and the value mapped to `from_key` is mapped to dispatchable subclasses
    in the `registry`.

    `add_dispatch` returns a dictionary of values that are injected into the
    blockDict, either to be detected by dispatch, or to be delegated to the
    constructor.

    This decorator should only be used directly for the start of a dispatch
    chain. For later dispatches, use
    [`register_dispatch`][FoSpy.blocks.blocks.SingleBlock.register_dispatch]

    Args:
        cls (SingleBlock subclass):
            The class to be decorated. If provided, the decorator is most
            likely being called as a bare decorator. Otherwise, the
            decorators is being called with other keyword arguments and
            returns the modified decorator.

        from_key (str):
            The key to be located in the blockDict after optional injection
            by `add_dispatch`. Private `from_key`s will be injected and
            located under the `__dispatch__` key which is popped before
            final construction.

        allow_self (bool):
            When True, the decorated class will dispatch to itself if no
            subclasses can be found. When False, error is raised during
            construction if dispatchable subclass is not found.

        _dispatch_from (SingleBlock subclass):
            To be passed only by `register_dispatch`, which decorates
            subclasses to populate this class's registry. Identifies the
            parent class that the constructor must start at. If not
            provided, the decorated class is assumed to be the start of a
            dispatch chain. 
        _defaults (dict):
            To be passed only by `register_dispatch`, which decorates
            subclasses to populat this class's registry. Provides default
            values that should be injected into the blockDict when trying to
            guarantee dispatching to the decorated class (usually by a
            template constructor).
        """
    if cls is not None and not isinstance(cls, type):
        raise Exception("@setup_dispatch must be used as a bare decorator, or with "
                        "a class as the first positional argument. You may have tried "
                        "to decorate a class with positional args instead of keywords.")

    def decorator(_cls:type[BlockType], _fk=from_key, _as=allow_self, _df=_dispatch_from, _def=_defaults):
        _cls.__dispatch__ = {
            "from_key": _fk,
            "allow_self": _as,
            "dispatch_from": _df or _cls,
            "registry": {}
        }

        def inject(bD, k, v, is_default=False):
            target_dict = bD["__dispatch__"] if k.startswith("_") else bD

            if is_default and v is None and k in target_dict:
                return bD

            target_dict[k] = v

            return bD

        @classmethod
        def inject_defaults(current_cls, blockDict, _d=_def):
            d = current_cls.__dispatch__
            blk_d = blockDict.setdefault("__dispatch__", {})
            if (not d['allow_self'] and
                None not in d['registry'] and
                blockDict.get(d['from_key'],blk_d.get(d['from_key'], None)) is None):
                default_dispatch = next(iter(d['registry'].values()))
                blockDict = default_dispatch.inject_defaults(blockDict)

            for k, v in _d.items():
                blockDict = inject(blockDict, k, v, is_default=True)
            return blockDict

        @SingleBlock.dispatcher
        def dispatch_subclass(current_cls:type[BlockType], blockDict:dict, _dispatch_key=_fk, **kwargs):
            injection = current_cls.add_dispatch(blockDict, _dispatch_key, _wrapped=True, **kwargs)

            for k, v in injection.items():
                blockDict = inject(blockDict, k, v)

            return blockDict

        # inject methods
        _cls.inject_defaults = inject_defaults
        _cls.dispatch_subclass = dispatch_subclass

        return _cls

    if cls is not None:
        return decorator(cls)

    return decorator

stage_template(prop_name, template=None)

Source code in FoSpy/blocks/blocks.py
def stage_template(self, prop_name, template:Block|dict=None):
    from .template import TemplateBlock
    if template is None:
        template = {}

    if not isinstance(template, (TemplateBlock, dict)):
        raise ValueError("Template must be a TemplateBlock or dictionary. To 'stage' a ListBlock, "
                         "you can stage a SingleBlock template with a ListBlock alias. This creates "
                         "a non-template ListBlock with the template staged as its first entry.")

    alias_validator = None
    if "$" in prop_name:
        prop_name, alias = prop_name.split("$",1)
        try:
            alias_validator = self._aliases[alias]
        except KeyError as e:
            raise err.PropertyAliasError(prop_name, self, blockDict={prop_name: "<staged template>"},
                                         hint=f"Unrecognized block alias: '{alias}' assigned to property: ",
                                         posthint=f"Valid aliases: {list(self._aliases.keys())}") from e

    if hasattr(self, prop_name):
        raise ValueError(f"Property {prop_name} already exists. You cannot stage a template for a property that already exists.")

    validators = self.build_validators()
    validator = validators.get(prop_name, None)

    if validator is not None:
        alias = None

    if alias_validator is not validator and None not in (validator, alias_validator):
        raise ValueError(f"Property {prop_name} already has a validator. You cannot alias a different validator for the same property.")

    validator = next(v for v in (validator, alias_validator) if v is not None)

    if validator is None:
        try:
            if not isinstance(template, TemplateBlock):
                raise TypeError("Dictionary templates must be staged with an alias.")

            alias = next(k for k, v in self._aliases.items() if isinstance(template, v))
            validator = self._aliases[alias]
        except (TypeError,StopIteration) as e:
            raise ValueError(f"Property {prop_name} is unexpected. In order to stage a template "
                            "for and unexpected property, you must specify the validator with a '$' alias "
                            "in the property name, or stage a pre-constructed template of an aliasable validator."
                            ) from e

    if isinstance(validator, type) and issubclass(validator, ListBlock):
        # let setattr handle ListBlock construction using alias
        # this creates an empty ListBlock under self.prop_name
        # (alias stripped during setattr)
        setattr(self, prop_name+"$"+alias if alias is not None else prop_name, [])
        empty_lb = getattr(self, prop_name)
        return empty_lb.stage_template("entry0", template)

    if isinstance(template, dict):
        # reflex returns a TemplateBlock subclassed from the validator
        template = validator.reflex(serialize=False, include_temp_names=True, clean=False, **template)
        template.template_name = prop_name

    elif not isinstance(template, validator):
        val_nm = validator.__name__
        if alias is None:
            error_msg = f"The provided template is not compatible with the validator expected for property '{prop_name}' ({val_nm})."
        else:
            error_msg = f"The provided template is not compatible with the validator specified by alias '{alias}' ({val_nm})."
        raise ValueError(error_msg)


    template._staged_parent = self

    if alias is not None:
        self._key_overrides[prop_name] = validator

    self._staged_templates[prop_name] = template

    return prop_name, template

to_json(filepath=None, clean=True, indent=4, **kwargs)

Converts self into a JSON-formatted string or file.

Serializes and either returns as a JSON-formatted string or saves to a JSON file.

Parameters:

Name Type Description Default
filepath pathlike

JSON file save destination. If None, returns JSON-formatted string instead.

None
clean bool

When True, no FOS format read/write metadata is included in the serial. FOS metadata has no impact on JSON format but may be useful to view in JSON for troubleshooting.

True
indent int

indent value passed to json.dump for file saving.

4
**kwargs any

other arguments passed to json.dump for file saving.

{}
Source code in FoSpy/blocks/blocks.py
def to_json(self, filepath=None, clean:bool=True, indent:int=4, **kwargs):
    """
    Converts `self` into a JSON-formatted string or file.

    [Serializes][FoSpy.blocks.blocks.SingleBlock.serialize] and either
    returns as a JSON-formatted string or saves to a JSON file.

    Args:
        filepath (pathlike):
            JSON file save destination. If `None`, returns JSON-formatted
            string instead.

        clean:
            When True, no FOS format read/write metadata is included in the
            serial. FOS metadata has no impact on JSON format but may be
            useful to view in JSON for troubleshooting.

        indent:
            `indent` value passed to `json.dump` for file saving.

        **kwargs (any):
            other arguments passed to `json.dump` for file saving.
    """
    import json
    serial = self.serialize(clean=clean)

    if filepath is None:
        return json.dumps(serial)

    with open(filepath, "w") as f:
        json.dump(serial, f, indent=indent, **kwargs)

track_attachments(new_copy='prompt', overwrite='prompt', **kwargs)

Source code in FoSpy/blocks/blocks.py
def track_attachments(self, new_copy="prompt",overwrite="prompt", **kwargs):
    self._att_new_copy = new_copy
    self._att_overwrite = overwrite

CifList


EmbeddedFile

Bases: Attachment

Methods:

Name Description
TemplateClass

Create a template for a subclass of SingleBlock.

__delattr__
__eq__

Check equality of two SingleBlock objects.

__getattr__

Check both self and self.ext for attribute before returning.

__hash__
__init__
__new__
__setattr__
_assign_and_inject

Attaches attributes and methods to any value before assigning it as an

_get_filepath
_meta_to_front

Moves metadata to the front of _key_order. Metadata will always be

_rename_validators

Realigns any renamed

_resolve_relative_path

Resolves a relative object path string into an object or function.

_subprocess
_update_src
_validate_filename
_write_to_temp
add_all_calc_routines

Schedule all available calculation routines.

add_block

Adds an unexpected attribute with a validator mapped by type_alias.

add_calc_comment

Add a calculated comment to be injected during serialization.

add_calc_routine

Schedules a calculated comment.

add_comments

Default behavior to be overwritten when attached to a parent block.

add_dispatch
build_req_validators

Builds required keys and validators mapped to subclass.

build_validators

Builds expected keys and validators mapped to subclass.

clear_all_comments
clear_comments

Clear comments attached to top-level attributes only.

copy

Returns a deep-copy of self by serializing and reconstructing.

default_key_order

Set to default attribute order for serialization.

dispatch_subclass
dispatcher

Decorate a classmethod to dispatch to other subclasses.

enforce_subtype
fill_staged_template
find_attachments
find_fileblock

Finds the parent file object.

find_tempdir

Find the parent file object's temporary directory.

find_temppath

Find the parent file object's temporary directory path.

get_id

Returns an easily recognizable identifier for self. Non-unique.

get_parent_prop
get_prop_dict

Returns a dictionary mapping property names to their live object values.

get_prop_path
get_req_validators

Overrides class validators with any renamed properties.

get_validators

Overrides class validators with any renamed properties.

has_staged
inject_defaults
inspect
key_to_idx

Reorder attributes for serialization.

keys_to_end

Reorder attributes for serialization.

keys_to_front

Reorder attributes for serialization.

list_avail_routines

Lists all calc routines available to be added to self._calc_routines.

make_template

Converts self into a template of its original subclass.

print_summary
reflex
refresh_attachments
register_dispatch
rename_block
rename_dict
serialize

Performs the default SingleBlock serialization, but restores the

set_dispatch
setup_dispatch

Decorate a class to dispatch to other classes during construction.

stage_template
to_json

Converts self into a JSON-formatted string or file.

track_attachments
Source code in FoSpy/blocks/attachments.py
@AnyFile.register_dispatch("embedded")
class EmbeddedFile(Attachment):

    def _write_to_temp(self, encoding="utf-8"):
        try:
            temppath = self.find_temppath()
        except Exception as e:
            _debug.msg(f"Could not find a temporary path to write to.\n{e}")
            return None
        filepath = temppath / self.file_name()
        with open(filepath, "w", encoding=encoding) as f:
            for line in self.embedded:
                f.write(line.rstrip("\r\n") + "\n")
        _debug.msg(f"Successfully wrote embedded file to temporary path: {filepath}")
        return filepath

    def _get_filepath(self):
        if self._filepath is not None:
            return self._filepath
        return self._write_to_temp()

    def serialize(self,**kwargs):
        """
        Performs the default `SingleBlock` serialization, but restores the
        "embedded" key to the full list of embedded lines instead of a string.
        """
        serial = super().serialize(**kwargs)
        #serial["embedded"] = self.embedded.copy()
        return serial

_aliases = new_als class-attribute instance-attribute

_calc_comments = {} instance-attribute

_calc_routines = [] instance-attribute

_constructed = True instance-attribute

_filepath = None instance-attribute

_id_key = 'file_name' class-attribute instance-attribute

_key_order = [] instance-attribute

_key_overrides = {} instance-attribute

_meta = SubContainer() instance-attribute

_reserved = ['ext'] instance-attribute

_sourceDict = blockDict.copy() instance-attribute

_staged_templates = {} instance-attribute

dispatch = {} class-attribute instance-attribute

dispatch_allow_self = True class-attribute instance-attribute

dispatch_default = None class-attribute instance-attribute

dispatch_key = None class-attribute instance-attribute

ext = SubContainer() instance-attribute

rename = rename instance-attribute

TemplateClass(*args) classmethod

Create a template for a subclass of SingleBlock.

Generates a hybridized subclass of the current block class and TemplateBlock. Template subclasses override original expected validators with either a TemplateField, TemplateBlock, or TemplateList depending on the type of the original validator.

Parameters:

Name Type Description Default
*args str

A list of properties to override as template types.

()
Source code in FoSpy/blocks/blocks.py
@classmethod
def TemplateClass(cls,*args:str):
    """
    Create a template for a subclass of `SingleBlock`.

    Generates a hybridized subclass of the current block class and
    [`TemplateBlock`][FoSpy.blocks.template.TemplateBlock]. Template
    subclasses override original expected validators with either a
    [`TemplateField`][FoSpy.blocks.template.TemplateField],
    [`TemplateBlock`][FoSpy.blocks.template.TemplateBlock], or
    [`TemplateList`][FoSpy.blocks.template.TemplateList] depending on the
    type of the original validator.

    Args:
        *args: A list of properties to override as template types.
    """
    from .template import TemplateBlock, FlexTemplate

    cls_registry = TemplateBlock.__dispatch__["registry"]

    if cls not in cls_registry:

        @TemplateBlock.register_dispatch(cls, setup_from_key="_fields", setup_allow_self=True, inherit_dispatch=True)
        class TemplateLocator(FlexTemplate, TemplateBlock, cls):
            _full_class = cls

        TemplateLocator.__name__ = f"{cls.__name__}TemplateLocator"
        TemplateLocator.__qualname__ = f"{cls.__name__}.TemplateClass.Locator"
        TemplateLocator.__module__ = cls.__module__

    fields = tuple(sorted(args))

    # construct a proxy dictionary that will correctly dispatch to the right
    # template class in TemplateBlock's dispatch chain.
    proxy_dict = {
        "__dispatch__": {
            "_full_class": cls,
            "_fields": fields
        }
    }

    return TemplateBlock.dispatch_subclass(proxy_dict)

__delattr__(attr)

Source code in FoSpy/blocks/blocks.py
def __delattr__(self, attr):
    if attr in self.get_req_validators():
        raise AttributeError(f"Cannot delete property: '{attr}'. It is registered as a required property for this object.")
    return super().__delattr__(attr)

__eq__(other, suppress_routine_paths=False)

Check equality of two SingleBlock objects.

Equality is checked by a deep difference of their serialized dictionaries.

Parameters:

Name Type Description Default
suppress_routine_paths bool

Optional flag to still return true if the only differences found are in calculation routine metadata. Calculation routines are for user information only and may not be relevant for equality.

False
Source code in FoSpy/blocks/blocks.py
def __eq__(self, other, suppress_routine_paths:bool=False):
    """
    Check equality of two `SingleBlock` objects.

    Equality is checked by a deep difference of their
    [serialized][FoSpy.blocks.blocks.SingleBlock.serialize] dictionaries.

    Args:
        suppress_routine_paths:
            Optional flag to still return true if the only differences found
            are in [calculation
            routine][FoSpy.blocks.blocks.SingleBlock.add_calc_routine]
            metadata. Calculation routines are for user information only and
            may not be relevant for equality.
    """
    from .._debug import deep_diff as dd, _debug as db
    try:
        db.msg("Serializing Blocks to check equality:", module = "SingleBlock.__eq__()")
        diffs = dd(self.serialize(), other.serialize(), suppress_routine_paths=suppress_routine_paths)
        passed = len(diffs) == 0
        if not passed:
            db.pmsg(diffs,module = "SingleBlock.__eq__()")
        return passed
    except Exception as e:
        db.msg(f"Equality failed by exception: {e}",module = "SingleBlock.__eq__()")
        return False

__getattr__(name)

Check both self and self.ext for attribute before returning.

A matching attribute of self will be returned first, but if self has no matching attribute, a matching attribute of self.ext can be returned instead.

Source code in FoSpy/blocks/blocks.py
def __getattr__(self, name:str):
    """
    Check both `self` and `self.ext` for attribute before returning.

    A matching attribute of `self` will be returned first, but if `self` has
    no matching attribute, a matching attribute of `self.ext` can be
    returned instead.
    """

    try:
        if name not in ("rename", "ext") and hasattr(self, "rename"):
            rename_dict = self.rename.serialize(shallow=True, clean=True)
            if name in rename_dict:
                return getattr(self, rename_dict[name])

        if name != 'ext':
            return getattr(self.ext, name)

        raise AttributeError()
    except AttributeError:
        raise AttributeError(
            f"{type(self).__name__} object "
            f"has no attribute {name!r}."
        )

__hash__()

Source code in FoSpy/blocks/blocks.py
def __hash__(self):
    return id(self)

__init__(blockDict, **kwargs)

Source code in FoSpy/blocks/attachments.py
def __init__(self, blockDict, **kwargs):
    super().__init__(blockDict, **kwargs)
    self._filepath = None

__new__(blockDict, *args, **kwargs)

Source code in FoSpy/blocks/blocks.py
def __new__(cls, blockDict, *args, **kwargs):
    _dispatched = kwargs.pop("_dispatched", False)
    if _dispatched:
        # blockDict should always be dict after dispatch. I want to see attributeerror if not.
        blockDict.pop("__dispatch__",None)
        return super().__new__(cls)

    blockDict = _unwrap_block(blockDict)

    dispatched_cls = cls.dispatch_subclass(blockDict, *args, **kwargs)

    if issubclass(dispatched_cls, cls):
        return dispatched_cls(blockDict, *args, _dispatched=True, **kwargs)

    dispatch = blockDict.pop("__dispatch__")
    raise err.BlockDispatchError(
        f"Attempted to construct the following dictionary as a {cls.__name__} block, "
        f"but it was dispatched to a {dispatched_cls.__name__} instead."
        f"\n\nINPUT:\n{blockDict}"
        f"\n\nDISPATCH:\n{dispatch}")

__setattr__(name, value)

Source code in FoSpy/blocks/attachments.py
def __setattr__(self, name, value):
    if name == "_extension":
        if value is None:
            return
        if hasattr(self, "_extension") and value != self._extension:
            from warnings import warn
            warn("You cannot change the extension of an attachment after construction. Skipping change.", RuntimeWarning)
            return

    if name == "file_name":
        old_ext = self._extension if hasattr(self, "_extension") else None
        value, new_ext = self._validate_filename(value, old_ext)
        self._extension = new_ext

    return super().__setattr__(name, value)

_assign_and_inject(name, value, extended=False)

Attaches attributes and methods to any value before assigning it as an attribute of self or self.ext.

Attributes Attached to Object

_parent_block: refers to self

Methods Attached to Object

add_comments_to_parent clear_comments_from_parent

Source code in FoSpy/blocks/blocks.py
def _assign_and_inject(self, name, value, extended=False):
    """
    Attaches attributes and methods to any value before assigning it as an
    attribute of `self` or `self.ext`.

    Attributes Attached to Object:
        `_parent_block`: refers to `self`

    Methods Attached to Object:
        [`add_comments_to_parent`][FoSpy.blocks.blocks._add_comments_to_parent]
        [`clear_comments_from_parent`][FoSpy.blocks.blocks._clear_comments_from_parent]
    """
    from .attachments import Attachment

    if name == 'ext':
        return super().__setattr__('ext', value)
    if not hasattr(value, "__dict__"):
        value = SimpleWrapper(value)

    if extended:
        setattr(self.ext, name, value)
    else:
        super().__setattr__(name, value)

    attr_obj = getattr(self.ext if extended else self, name)

    setattr(attr_obj, "_parent_block", self)

    if isinstance(attr_obj, Attachment):
        attr_obj._get_filepath()
    elif hasattr(attr_obj, "refresh_attachments"):
        attr_obj.refresh_attachments()

    methods = ((_add_comments_to_parent(name), "add_comments"),
            (_clear_comments_from_parent(name), "clear_comments"))

    attr_obj._reserved = ['ext'] if not hasattr(attr_obj,"_reserved") else attr_obj._reserved
    for method, method_name in methods:
        attr_obj._reserved.append(method_name)
        bound = method.__get__(attr_obj, type(attr_obj))
        setattr(attr_obj, method_name, bound)

    self._props_changed = True

_get_filepath()

Source code in FoSpy/blocks/attachments.py
def _get_filepath(self):
    if self._filepath is not None:
        return self._filepath
    return self._write_to_temp()

_meta_to_front()

Moves metadata to the front of _key_order. Metadata will always be serialized first, but being elsewhere in the order leads to unexpected results when moving other keys to desired indices.

Source code in FoSpy/blocks/blocks.py
def _meta_to_front(self):
    """
    Moves metadata to the front of `_key_order`. Metadata will always be
    serialized first, but being elsewhere in the order leads to unexpected
    results when moving other keys to desired indices.
    """
    try:
        meta_idx =self._key_order.index("metadata")
        self._key_order.pop(meta_idx)
    # TODO: Better handling
    except Exception:
        pass
    self._key_order.insert(0,"metadata")

_rename_validators(validators)

Realigns any renamed attributes with their expected validator.

Parameters:

Name Type Description Default
validators dict

A dictionary mapping attribute names to validators, returned by either build_validators or build_req_validators

required
Source code in FoSpy/blocks/blocks.py
def _rename_validators(self, validators:dict):
    """
    Realigns any [renamed][FoSpy.blocks.blocks.SingleBlock.rename_block]
    attributes with their expected validator.

    Args:
        validators:
            A dictionary mapping attribute names to validators, returned by
            either
            [`build_validators`][FoSpy.blocks.blocks.SingleBlock.build_validators]
            or
            [`build_req_validators`][FoSpy.blocks.blocks.SingleBlock.build_req_validators]
    """
    if hasattr(self, "rename"):
        for name, rename in self.rename.serialize(shallow=True, clean=True).items():
            if name in validators and rename not in validators:
                val = validators.pop(name)
                validators[rename] = val
    return validators

_resolve_relative_path(path)

Resolves a relative object path string into an object or function.

Example:

    mySyn._resolve_relative_path("materials[1].ratio")
    ## returns mySyn.materials[1].ratio

Source code in FoSpy/blocks/blocks.py
def _resolve_relative_path(self, path: str):
    """
    Resolves a relative object path string into an object or function.

    Example:
    ```
        mySyn._resolve_relative_path("materials[1].ratio")
        ## returns mySyn.materials[1].ratio
    ```
    """
    import re

    _index_re = re.compile(r"^([A-Za-z_]\w*)\[(\d+)\]$")
    obj = self

    for part in path.split("."):

        # Case: attr[index]
        m = _index_re.match(part)
        if m:
            attr_name, idx_str = m.groups()
            idx = int(idx_str)

            # Get the ListBlock
            obj = getattr(obj, attr_name)

            # Index into its _objs
            obj = obj._objs[idx]
            continue

        # Case: simple attribute
        obj = getattr(obj, part)

    return obj

_subprocess(target, args=(), **kwargs)

Source code in FoSpy/blocks/blocks.py
def _subprocess(self, target, args=(), **kwargs):
    from multiprocessing import Process

    if kwargs is None:
        kwargs={}

    p = Process(target=target, args=args, kwargs=kwargs)
    p.start()

_update_src()

Source code in FoSpy/blocks/blocks.py
def _update_src(self):
    if self._constructed and self._props_changed:
        self._sourceDict = self.serialize(clean=True)
        self._props_changed = False

    return self._sourceDict   

_validate_filename(filename, ext=None, warn=True) classmethod

Source code in FoSpy/blocks/attachments.py
@classmethod
def _validate_filename(cls, filename:str, ext:str=None, warn=True):
    filename = str(filename)
    if ext is None:
        ext = f".{filename.rsplit('.')[-1]}" if "." in filename else ""
        # delegate to base validator routine to verify extension
        return filename, ext

    if "." not in filename:
        new_ext = ext
    else:
        new_ext = f".{filename.rsplit('.')[-1]}"

    if new_ext != ext:
        if warn:
            filename = filename + ext
            from warnings import warn
            warn(f"New filename contains a different extension: '{new_ext}'. Extensions cannot "
                f"be changed after construction. The current extension ('{ext}') "
                f"will be appended to the new filename to form: '{filename}'.", RuntimeWarning)
        else:
            raise ValueError(f"New filename contains a different extension: '{new_ext}'. Extensions cannot "
                             "be changed after attachment construction.")

    return filename, new_ext

_write_to_temp(encoding='utf-8')

Source code in FoSpy/blocks/attachments.py
def _write_to_temp(self, encoding="utf-8"):
    try:
        temppath = self.find_temppath()
    except Exception as e:
        _debug.msg(f"Could not find a temporary path to write to.\n{e}")
        return None
    filepath = temppath / self.file_name()
    with open(filepath, "w", encoding=encoding) as f:
        for line in self.embedded:
            f.write(line.rstrip("\r\n") + "\n")
    _debug.msg(f"Successfully wrote embedded file to temporary path: {filepath}")
    return filepath

add_all_calc_routines(recursive=False)

Schedule all available calculation routines.

Adds all available calc_routines to self._calc_routines using list_avail_routines() and add_calc_routine().

Parameters:

Name Type Description Default
recursive bool

Optional recursion. See SingleBlock.list_avail_routines()

False
Source code in FoSpy/blocks/blocks.py
def add_all_calc_routines(self, recursive:bool=False):
    """
    Schedule all available calculation routines.

    Adds all available calc_routines to `self._calc_routines` using
    [`list_avail_routines()`][FoSpy.blocks.blocks.SingleBlock.list_avail_routines]
    and
    [`add_calc_routine()`][FoSpy.blocks.blocks.SingleBlock.add_calc_routine].

    Args:
        recursive:
            Optional recursion. See `SingleBlock.list_avail_routines()`
    """
    for path in self.list_avail_routines(recursive=recursive, abbreviated=False):
        self.add_calc_routine(path)

add_block(block_name, type_alias, value=[])

Adds an unexpected attribute with a validator mapped by type_alias. Unexpected attributes not requiring a validator can be set directly without using this method.

Parameters:

Name Type Description Default
block_name str

new unexpected attribute name

required
type_alias str

Alias mapped to the desired validator in parsing.validation.aliases. For more information on how aliases are used, see __setattr__.

required
Source code in FoSpy/blocks/blocks.py
def add_block(self, block_name:str, type_alias:str, value=[]):
    """
    Adds an unexpected attribute with a validator mapped by `type_alias`.
    Unexpected attributes not requiring a validator can be set directly
    without using this method.

    Args:
        block_name: new unexpected attribute name
        type_alias:
            Alias mapped to the desired validator in
            [`parsing.validation.aliases`][FoSpy.parsing.validation.aliases].
            For more information on how aliases are used, see
            [`__setattr__`][FoSpy.blocks.blocks.SingleBlock.__setattr__].
    """
    if hasattr(self,block_name):
        raise ValueError(f"This object already has attribute: '{block_name}'.")
    return setattr(self, f"{block_name}${type_alias}", value)

add_calc_comment(key, comment, calc_id)

Add a calculated comment to be injected during serialization.

WARNING: This function can leave outdated calculations in comments after serialization. Recommended to use add_calc_routine() instead.

Calculated comments are for user information and will be formatted to be skipped by the parser when reading the file. This is useful for comments that should be recalculated and refreshed during saving/serialization, like weight percentages or summaries.

Parameters:

Name Type Description Default
key str

attribute to attach the calculated comment to. Comments appear above their attached attributes in FOS format.

required
comment str

comment text without comment formatting (don't include // or !)

required
calc_id str

unique identifier for the calculated comment. If it matches an existing comment (like when refreshing a value), the comment is overwritten

required
Source code in FoSpy/blocks/blocks.py
def add_calc_comment(self, key:str, comment:str, calc_id:str):
    """
    Add a calculated comment to be injected during serialization.

    WARNING: This function can leave outdated calculations in comments after
    serialization. Recommended to use `add_calc_routine()` instead.

    Calculated comments are for user information and will be formatted to be
    skipped by the parser when reading the file. This is useful for comments
    that should be recalculated and refreshed during saving/serialization,
    like weight percentages or summaries.

    Args:
        key:
            attribute to attach the calculated comment to. Comments appear
            above their attached attributes in FOS format.
        comment:
            comment text without comment formatting (don't include // or !)
        calc_id:
            unique identifier for the calculated comment. If it matches an
            existing comment (like when refreshing a value), the comment is
            overwritten

    """
    calc_comments = self._calc_comments.get(key, {})
    self._calc_comments[key] = calc_comments
    self._calc_comments[key][calc_id]=comment

add_calc_routine(path, **kwargs)

Schedules a calculated comment.

Appends a _calc_routine()-decorated function to self._calc_routines to be run at serialization.

Used to add calculated comments that should be refreshed during serialization.

Parameters:

Name Type Description Default
path str

a relative path string that can be resolved into a _calc_routine()-decorated function

required
**kwargs any

optional key word arguments to be passed to the function at path.

{}

Raises:

Type Description
TypeError

the attr or method at path is not registered as a _calc_routine

Example:

    mySyn.add_calc_routine("materials.add_weight_pcts", typ="reagent")
    ## mySyn.materials.add_weight_pcts(typ="reagent") is now scheduled
    ## to run at serialization

Source code in FoSpy/blocks/blocks.py
def add_calc_routine(self, path:str, **kwargs):
    """
    Schedules a calculated comment.

    Appends a
    [`_calc_routine()`][FoSpy.blocks._blockUtils._calc_routine]-decorated
    function to `self._calc_routines` to be run at
    [serialization][FoSpy.blocks.blocks.SingleBlock.serialize].

    Used to add calculated comments that should be refreshed during
    serialization.

    Args:
        path:
            a relative path string that can be resolved into a
            `_calc_routine()`-decorated function
        **kwargs (any):
            optional key word arguments to be passed to the function at
            path.

    Raises:
        TypeError:
            the attr or method at path is not registered as a
            _calc_routine

    Example:
    ```
        mySyn.add_calc_routine("materials.add_weight_pcts", typ="reagent")
        ## mySyn.materials.add_weight_pcts(typ="reagent") is now scheduled
        ## to run at serialization
    ```
    """

    func = self._resolve_relative_path(path)
    if not getattr(func, "_is_calc_routine", False):
        raise TypeError(f"'{path}' is not a registered calc routine.")

    self._meta.routine_paths.append(path)

    def wrapped(f=func, k=kwargs):
        return f(**k)

    self._calc_routines.append(wrapped)

add_comments(*comments)

Default behavior to be overwritten when attached to a parent block.

If a SingleBlock is stored as an attribute of another SingleBlock, this method will be overwritten by the parent's __setattr__.

Source code in FoSpy/blocks/blocks.py
def add_comments(self, *comments):
    """
    Default behavior to be overwritten when attached to a parent block.

    If a `SingleBlock` is stored as an attribute of another `SingleBlock`,
    this method will be overwritten by the parent's `__setattr__`.
    """
    keys = list(self.get_req_validators())

    keys = [k for k in keys if k != "metadata"]
    fallback = [k for k in self._key_order if k != "metadata"]
    if not (keys or fallback):
        raise ValueError("This object has not been correctly attached to a parent block "
                         "and could not identify a required key to attach to.")

    first = keys[0] if keys else fallback[0]

    self._meta.comments.setdefault(first, [])
    for comment in comments:
        self._meta.comments[first].append(comment)

add_dispatch(blockDict, dispatch_key, **kwargs) classmethod

Source code in FoSpy/blocks/attachments.py
@classmethod
def add_dispatch(cls, blockDict, dispatch_key, **kwargs):
    from .. import _errors as err

    # make sure wrapped
    _ = SingleBlock.add_dispatch(blockDict, dispatch_key, **kwargs)

    if "file_name" not in blockDict:
        raise err.MissingPropertyError("file_name", cls, blockDict=blockDict)

    _, ext = cls._validate_filename(blockDict["file_name"])

    return {dispatch_key: ext}

build_req_validators() classmethod

Builds required keys and validators mapped to subclass.

Walks all parent classes and builds a map of all keys that are required during __init__, and their respective validation routines. Subclasses are mapped to expected keys and validations in parsing.validation. Subclass validations override parent classes when applicable.

Returns:

Name Type Description
merged dict

Maps required keys to validation routines. Routines may be a class constructor or a func taking one arg.

Example:

>>> SingleBlock.build_req_validators()
{
    "name": str,
    "type": str,
    "formula": ChemFormula, # class constructor
    "supplier": str,
    "cas": str,
    "form": str,
    "env": str,
    "ratio": validators.material.ratio # validator function
}

Source code in FoSpy/blocks/blocks.py
@classmethod
def build_req_validators(cls):
    """
    Builds required keys and validators mapped to subclass.

    Walks all parent classes and builds a map of all keys that are required
    during `__init__`, and their respective validation routines. Subclasses
    are mapped to expected keys and validations in
    [`parsing.validation`][FoSpy.parsing.validation]. Subclass validations
    override parent classes when applicable.

    Returns:
        merged (dict):
            Maps required keys to validation routines. Routines may be
            a class constructor or a func taking one arg.
    Example:
        ``` 
        >>> SingleBlock.build_req_validators()
        {
            "name": str,
            "type": str,
            "formula": ChemFormula, # class constructor
            "supplier": str,
            "cas": str,
            "form": str,
            "env": str,
            "ratio": validators.material.ratio # validator function
        }
        ```
    """
    from ..parsing.validation import required_keys
    from ._blockUtils import _get_prop_mro, _merge_vals
    merged = {}
    # mro = list(reversed(cls.__mro__))
    # for i, base in enumerate(mro):
    #     base_reqs = required_keys.get(base,{})
    #     for key, validator in base_reqs.items():
    #         # allow subclasses to remove parent requirements.
    #         if not validator:
    #             merged.pop(key, None)
    #         else:
    #             merged[key] = validator

    req_mro = _get_prop_mro(cls, required_keys)
    for i in range(len(req_mro)):
        merged = _merge_vals(merged, req_mro, i)

    merged.pop("__all__")

    return merged

build_validators() classmethod

Builds expected keys and validators mapped to subclass.

Walks all parent classes and builds a map of all keys that are expected (required or optional), and their respective validation routines. Subclasses are mapped to keys and validations in parsing.validation. Subclass validations override parent classes when applicable.

See build_req_validators

Source code in FoSpy/blocks/blocks.py
@classmethod
def build_validators(cls):
    """
    Builds expected keys and validators mapped to subclass.

    Walks all parent classes and builds a map of all keys that are expected
    (required or optional), and their respective validation routines.
    Subclasses are mapped to keys and validations in
    [`parsing.validation`][FoSpy.parsing.validation]. Subclass validations
    override parent classes when applicable.

    See
    [`build_req_validators`][FoSpy.blocks.blocks.SingleBlock.build_req_validators]
    """
    from ..parsing.validation import required_keys, optional_keys
    from ._blockUtils import _merge_vals, _get_prop_mro
    from .._docs.properties import _validator_rules
    merged = {}
    # for base in reversed(cls.__mro__):
    #     for key_set in (required_keys, optional_keys):
    #         base_reqs = key_set.get(base,{})
    #         for key, validator in base_reqs.items():
    #             # allow subclasses to remove parent requirements.
    #             if validator is False:
    #                 merged.pop(key, None)
    #             else:
    #                 merged[key] = validator
    req_mro = _get_prop_mro(cls, required_keys)
    opt_mro = _get_prop_mro(cls, optional_keys)

    for i in range(len(req_mro)): # req_mro and opt_mro are the same length
        merged = _merge_vals(merged, req_mro, i)
        merged = _merge_vals(merged, opt_mro, i)

    universal_val = merged.pop("__all__")

    @_validator_rules(inherit_from=universal_val)
    def universal_val_method(cls, *_, _m=universal_val, **__):
        return _m(*_, **__)

    cls.universal_val = universal_val_method

    return merged

clear_all_comments()

Source code in FoSpy/blocks/blocks.py
def clear_all_comments(self):
    self._meta.comments = {}
    for attr, val in self.__dict__.items():
        if attr.startswith("_") or attr in self._reserved:
            continue
        if hasattr(val, "clear_all_comments"):
            val.clear_all_comments()

clear_comments()

Clear comments attached to top-level attributes only.

Source code in FoSpy/blocks/blocks.py
def clear_comments(self):
    """
    Clear comments attached to top-level attributes only.
    """
    self._meta.comments = {}

copy()

Returns a deep-copy of self by serializing and reconstructing.

_calc_comments are not preserved during copy, but _calc_routines are. This prevents mutation of the comments when reconstructing.

Source code in FoSpy/blocks/blocks.py
def copy(self):
    """
    Returns a deep-copy of `self` by serializing and reconstructing.

    _calc_comments are not preserved during copy, but _calc_routines are.
    This prevents mutation of the comments when reconstructing.
    """
    cls = type(self)
    # cache calculated comments before serializing
    c_cmts = self._calc_comments.copy()

    serial = self.serialize(keepListType=True)

    new_obj =  cls(serial)

    # restore cached calc comments
    self._calc_comments = c_cmts

    return new_obj

default_key_order(deep=False)

Set to default attribute order for serialization.

Rearrange attribute order to the default order assigned by build_validators

Parameters:

Name Type Description Default
deep bool

When true, recursively calls default_key_order on any other SingleBlock objects stored in attributes.

False
Source code in FoSpy/blocks/blocks.py
def default_key_order(self, deep:bool=False):
    """
    Set to default attribute order for serialization.

    Rearrange attribute order to the default order assigned by
    [`build_validators`][FoSpy.blocks.blocks.SingleBlock.build_validators]

    Args:
        deep:
            When true, recursively calls `default_key_order` on any other
            `SingleBlock` objects stored in attributes.
    """
    new_order = []
    for key in self.get_validators():
        if key != "ext" and key in self.serialize(shallow=True):
            new_order.append(key)
    for key in self._key_order:
        if key not in new_order:
            new_order.append(key)
    self._key_order = new_order
    self._meta_to_front()

    if deep:
        for name, obj in self.__dict__.items():
            if not name.startswith("_") and hasattr(obj, "default_key_order"):
                obj.default_key_order(deep=True)

dispatch_subclass(*args, **kwargs) classmethod

Source code in FoSpy/blocks/blocks.py
@classmethod
def dispatch_subclass(cls, *args, **kwargs):
    # fallback.
    # overridden by setup_dispatch decorator
    return cls

dispatcher(dispatch_method) staticmethod

Decorate a classmethod to dispatch to other subclasses.

Not normally used directly. See setup_dispatch decorator.

Source code in FoSpy/blocks/blocks.py
@staticmethod
def dispatcher(dispatch_method: Callable[..., dict])->classmethod:
    """
    Decorate a classmethod to dispatch to other subclasses.

    Not normally used directly. See
    [`setup_dispatch`][FoSpy.blocks.blocks.SingleBlock.setup_dispatch]
    decorator.
    """

    @classmethod
    def dispatch_subclass(cls:type[BlockType], blockDict, _add_defaults=False, **kwargs):
        from .. import _errors as err
        for_template = kwargs.get("for_template", False)

        block_dispatch = blockDict.setdefault("__dispatch__", {})
        visited = block_dispatch.setdefault("visited", [])
        cls_dispatch = getattr(cls, "__dispatch__", None)
        # shorthand for keying dispatch parameters
        d=cls_dispatch

        if d['dispatch_from'] in visited and (
            cls in visited or
            d is None or
            d['from_key'] is None):
            return cls
        try:
            blockDict = dispatch_method(cls, blockDict, add_defaults=_add_defaults, **kwargs)
        except Exception as e:
            if not for_template:
                raise e

        visited.append(cls)

        if d['dispatch_from'] not in visited:
            blockDict.pop("__dispatch__", None)
            return d['dispatch_from'].dispatch_subclass(blockDict, _add_defaults=_add_defaults, **kwargs)


        dispatch_val = block_dispatch.get(d['from_key'],
                            blockDict.get(d['from_key'], None))

        dispatched_cls = d['registry'].get(dispatch_val, d['registry'].get(None, cls))

        if dispatched_cls is cls and not d['allow_self']:
            if not for_template:
                raise err.BlockDispatchError(
                    f"The following blockDict was dispatched to {cls.__name__} "
                    "but could not be dispatched further. "
                    f"{cls.__name__} blocks are not allowed without a subclass.")
            return cls

        return dispatched_cls.dispatch_subclass(blockDict, **kwargs)
    return dispatch_subclass

enforce_subtype(subcls, **kwargs) classmethod

Source code in FoSpy/blocks/attachments.py
@classmethod
def enforce_subtype(cls, subcls, **kwargs):
    raise DeprecationWarning("Attachments no longer enforce subtype through this method. "
                             "Simply spec the validator as the enforced subtype instead.")

fill_staged_template(prop_name, **kwargs)

Source code in FoSpy/blocks/blocks.py
def fill_staged_template(self, prop_name, **kwargs):
    from .template import TemplateBlock

    prop_key = prop_name.split("$")[0] if "$" in prop_name else prop_name

    template = self._staged_templates.pop(prop_key, None)
    if template is None:
        prop_name, _ = self.stage_template(prop_name)
        return self.fill_staged_template(prop_name, **kwargs)

    prop_name = prop_key

    filled = template.fill(staged=True,**kwargs)

    if isinstance(filled, TemplateBlock):
        return self.stage_template(prop_name, filled)

    try:
        setattr(self, prop_name, filled)
    except Exception as e:
        raise Exception(f"Template was filled but could not be assigned {prop_name}") from e

    filled = getattr(self, prop_name)

    if isinstance(self, TemplateBlock):
        self.fill()

    return prop_name, filled

find_attachments()

Source code in FoSpy/blocks/attachments.py
def find_attachments(self):
    attachments = super().find_attachments()
    if self not in attachments:
        attachments.append(self)

    return attachments

find_fileblock()

Finds the parent file object.

Walks upward through _parent_block attributes until a FileBlock instance is found and returns that instance.

Source code in FoSpy/blocks/blocks.py
def find_fileblock(self):
    """
    Finds the parent file object.

    Walks upward through `_parent_block` attributes until a
    [`FileBlock`][FoSpy.blocks.files.FileBlock] instance is found and
    returns that instance.
    """
    from .files import FileBlock
    from .._errors import FileBlockNotFoundError

    blk = self
    while blk is not None:
        if isinstance(blk, FileBlock):
            return blk
        if hasattr(blk,"_parent_block"):
            blk = blk._parent_block
        else:
            blk = None
    raise FileBlockNotFoundError("Could not find a FileBlock containing the current object")

find_tempdir()

Find the parent file object's temporary directory.

Finds the temporary directory created by the FileBlock instance containing this block as one of its attributes.

Returns:

Name Type Description
tempdir tempfile.TemporaryDirectory

The temporary directory created by the parent file object

Source code in FoSpy/blocks/blocks.py
def find_tempdir(self):
    """
    Find the parent file object's temporary directory.

    Finds the temporary directory created by the
    [`FileBlock`][FoSpy.blocks.files.FileBlock] instance containing this
    block as one of its attributes. 

    Returns:
        tempdir (tempfile.TemporaryDirectory):
            The temporary directory created by the parent file object
    """
    fileblock = self.find_fileblock()
    if hasattr(fileblock, "_tempdir"):
        return fileblock._tempdir
    else:
        raise AttributeError("Could not find a temporary directory attached to this object's FileBlock")

find_temppath()

Find the parent file object's temporary directory path.

Similar to find_tempdir but returns the corresponding pathlib.Path object instead.

Source code in FoSpy/blocks/blocks.py
def find_temppath(self):
    """
    Find the parent file object's temporary directory path.

    Similar to [`find_tempdir`][FoSpy.blocks.blocks.Block.find_tempdir] but
    returns the corresponding `pathlib.Path` object instead.
    """
    fileblock = self.find_fileblock()
    if hasattr(fileblock, "_temppath"):
        return fileblock._temppath
    if hasattr(fileblock, "_temppdir"):
        raise AttributeError("This object's FileBlock has a temporary directory but no path mapped to it. "
                             "Use obj.find_tempdir() instead")
    raise AttributeError("Could not find a temporary directory object or path "
                         "attached to this object's FileBlock.")

get_id()

Returns an easily recognizable identifier for self. Non-unique.

Source code in FoSpy/blocks/blocks.py
def get_id(self):
    """Returns an easily recognizable identifier for self. Non-unique."""
    id_txt = str(getattr(self, self._id_key)) if self._id_key is not None else type(self).__name__
    return self._id_key, id_txt

get_parent_prop()

Source code in FoSpy/blocks/blocks.py
def get_parent_prop(self):
    if not hasattr(self, "_parent_block"):
        return None
    parent_blk = self._parent_block

    if isinstance(parent_blk, SingleBlock):
        for prop, val in parent_blk.get_prop_dict().items():
            if val is self:
                return prop

        raise err.FoSpyStructureError(f"Block {self} points to a parent block {parent_blk} that does not contain it as a property.")

    elif isinstance(parent_blk, ListBlock):
        return f"[{parent_blk.get_idx(self)}]"

    raise err.FoSpyStructureError(f"Block {self} has an unknown parent block type: {type(parent_blk)}")

get_prop_dict()

Returns a dictionary mapping property names to their live object values.

Source code in FoSpy/blocks/blocks.py
def get_prop_dict(self):
    """Returns a dictionary mapping property names to their live object values."""
    serial = self.serialize(shallow=True, clean=True)
    out = {}
    for prop in serial:
        if "$" in prop:
            prop = prop.split("$")[0]

        # guard for when templateblocks add staged templates to their serial
        if hasattr(self, prop):
            out[prop] = getattr(self, prop)

    return out

get_prop_path()

Source code in FoSpy/blocks/blocks.py
def get_prop_path(self):
    from .files import FileBlock

    if not hasattr(self, "_parent_block"):
        if isinstance(self, FileBlock):
            root_path = f"<{str(self.get_file_name())}>"
        else:
            root_path = f"<Root {type(self).__name__}"
            if isinstance(self, SingleBlock):
                id_key, id_txt = self.get_id()
                if id_key is not None:
                    root_path += f" ({id_key}={id_txt})"
            root_path += ">"
        return root_path

    parent_path = self._parent_block.get_prop_path()
    parent_prop = self.get_parent_prop()

    if "[" not in parent_prop:
        return parent_path + "." + parent_prop

    return parent_path + parent_prop

get_req_validators()

Overrides class validators with any renamed properties.

Similar to class method: build_req_validators, but uses _rename_validators to align any renamed properties with their original validators.

Source code in FoSpy/blocks/blocks.py
def get_req_validators(self):
    """
    Overrides class validators with any renamed properties.

    Similar to class method:
    [`build_req_validators`][FoSpy.blocks.blocks.SingleBlock.build_req_validators],
    but uses
    [`_rename_validators`][FoSpy.blocks.blocks.SingleBlock._rename_validators]
    to align any renamed properties with their original validators.
    """
    return self._rename_validators(self.build_req_validators())

get_validators()

Overrides class validators with any renamed properties.

Similar to class method: build_validators, but uses _rename_validators to align any renamed properties with their original validators. Also adds any optional key overrides added by key$alias syntax.

Returns:

Name Type Description
vals dict

maps expected keys to validation routines.

Source code in FoSpy/blocks/blocks.py
def get_validators(self):
    """
    Overrides class validators with any renamed properties.

    Similar to class method:
    [`build_validators`][FoSpy.blocks.blocks.SingleBlock.build_validators],
    but uses
    [`_rename_validators`][FoSpy.blocks.blocks.SingleBlock._rename_validators]
    to align any renamed properties with their original validators. Also
    adds any optional key overrides added by key$alias syntax.

    Returns:
        vals (dict): maps expected keys to validation routines.
    """
    vals = self._rename_validators(self.build_validators())
    if hasattr(self, "_key_overrides"):
        for key, val in self._key_overrides.items():
            vals[key] = val
    return vals

has_staged()

Source code in FoSpy/blocks/blocks.py
def has_staged(self):
    if len(self._staged_templates) > 0:
        return True

    for val in self.get_prop_dict().values():
        if hasattr(val, "has_staged") and val.has_staged():
            return True

    return False

inject_defaults(blockDict, *args, **kwargs) classmethod

Source code in FoSpy/blocks/blocks.py
@classmethod
def inject_defaults(cls, blockDict, *args, **kwargs):
    # fallback.
    # overridden by setup_dispatch decorator
    return blockDict

inspect() classmethod

Source code in FoSpy/blocks/blocks.py
@classmethod
def inspect(self):
    # for breaking to debugger from gui
    raise Exception("put a break point here")

key_to_idx(key, idx)

Reorder attributes for serialization.

Move any attribute name to a specific index in _key_order for serialization order. The invisible "metadata" key is always refreshed to the front of the list, so indices are effectively 1-based.

Parameters:

Name Type Description Default
key str

name of attribute to reorder

required
idx int

new index in _key_order

required
Source code in FoSpy/blocks/blocks.py
def key_to_idx(self, key:str, idx:int):
    """
    Reorder attributes for serialization.

    Move any attribute name to a specific index in `_key_order` for
    serialization order. The invisible `"metadata"` key is always refreshed
    to the front of the list, so indices are effectively 1-based.

    Args:
        key: name of attribute to reorder
        idx: new index in _key_order
    """
    self._meta_to_front()
    try:
        old_idx = self._key_order.index(key)
        self._key_order.pop(old_idx)
    # TODO: Better handling
    except Exception:
        pass
    self._key_order.insert(idx, key)

keys_to_end(*args)

Reorder attributes for serialization.

Move any attribute names in *args to the end of _key_order to be serialized last. Order within *args is maintained in result.

Source code in FoSpy/blocks/blocks.py
def keys_to_end(self, *args):
    """
    Reorder attributes for serialization.

    Move any attribute names in `*args` to the end of _key_order to be
    serialized last. Order within `*args` is maintained in result.
    """
    def remove_alias(key):
        return key.split("$")[0] if "$" in key else key
    for key in self.serialize(shallow=True):
        if not key.startswith("_") and remove_alias(key) not in self._key_order:
            self._key_order.append(remove_alias(key))
    for key in args:
        try:
            idx = self._key_order.index(key)
            self._key_order.pop(idx)
        # TODO: Better handling
        except Exception:
            pass
        self._key_order.append(key)
    self._meta_to_front()

keys_to_front(*args)

Reorder attributes for serialization.

Move any attribute names in *args to the front of _key_order to be serialized first. Order within *args is maintained in result.

Source code in FoSpy/blocks/blocks.py
def keys_to_front(self,*args):
    """
    Reorder attributes for serialization.

    Move any attribute names in `*args` to the front of _key_order to be
    serialized first. Order within `*args` is maintained in result.
    """
    try:
        meta_idx = args.index("metadata")
        args.pop(meta_idx)
    # TODO: Better handling
    except Exception:
        pass

    new_order = []
    for key in args:
        new_order.append(key)
    for key in self._key_order:
        if key not in new_order:
            new_order.append(key)
    self._key_order = new_order
    self._meta_to_front()

list_avail_routines(recursive=False, prefix='', abbreviated=False)

Lists all calc routines available to be added to self._calc_routines.

Non-abbreviated calc routine strings can be passed directly to self.add_calc_routine()

Parameters:

Name Type Description Default
recursive bool

If True, recursively walks all attributes and appends results from self.attr.list_avail_routines() to result. Otherwise only identifies methods of self.

False
prefix str

Used during recursion to build relative paths

''
abbreviated bool

optionally abbreviate recursively repeated routines for similar objects into one line. This line cannot be passed to self.add_calc_routine()

False

Returns:

Name Type Description
routines list

list of strings describing _calc_routine-decorated methods. Non-abbreviated calc routine strings can be passed directly to self.add_calc_routine()

Example:

    mySyn.list_avail_routines()
    ## returns []
    mySyn.list_avail_routines(recursive=True)
    ## returns [
    ##     'reaction.add_nom_MW',
    ##     'materials.add_weight_pcts',
    ##     'materials[0].add_MW',
    ##     'materials[1].add_MW',
    ##     ... 6 total materials with the same calc_routine
    ##     'materials[5].add_MW'
    ## ]
    mySyn.list_avail_routines(recursive=True, abbreviated=True)
    ## returns [
    ##     'reaction.add_nom_MW',
    ##     'materials.add_weight_pcts',
    ##     'materials[i].add_MW; i = [0, 1, 2, 3, 4, 5]'
    ## ]

Source code in FoSpy/blocks/blocks.py
def list_avail_routines(self, recursive:bool=False, prefix:str="", abbreviated:bool=False):
    """
    Lists all calc routines available to be added to `self._calc_routines`.

    Non-abbreviated calc routine strings can be passed directly to
    `self.add_calc_routine()`

    Args:
        recursive:
            If True, recursively walks all attributes and appends results
            from `self.attr.list_avail_routines()` to result. Otherwise only
            identifies methods of `self`.

        prefix: Used during recursion to build relative paths
        abbreviated:
            optionally abbreviate recursively repeated routines for similar
            objects into one line. This line cannot be passed to
            `self.add_calc_routine()`

    Returns:
        routines (list): 
            list of strings describing _calc_routine-decorated methods.
            Non-abbreviated calc routine strings can be passed directly to
            `self.add_calc_routine()`

    Example:
    ```
        mySyn.list_avail_routines()
        ## returns []
        mySyn.list_avail_routines(recursive=True)
        ## returns [
        ##     'reaction.add_nom_MW',
        ##     'materials.add_weight_pcts',
        ##     'materials[0].add_MW',
        ##     'materials[1].add_MW',
        ##     ... 6 total materials with the same calc_routine
        ##     'materials[5].add_MW'
        ## ]
        mySyn.list_avail_routines(recursive=True, abbreviated=True)
        ## returns [
        ##     'reaction.add_nom_MW',
        ##     'materials.add_weight_pcts',
        ##     'materials[i].add_MW; i = [0, 1, 2, 3, 4, 5]'
        ## ]
    ```
    """
    routines = []

    # Local routines
    for name in dir(self):
        attr = getattr(self, name)
        if callable(attr) and getattr(attr, "_is_calc_routine", False):
            routines.append(prefix + name)

    if recursive:
        for attr, val in self.__dict__.items():
            if attr.startswith("_"):
                continue

            # Recurse into child blocks
            if hasattr(val, "list_avail_routines"):
                child_prefix = f"{prefix}{attr}."
                routines.extend(val.list_avail_routines(True, child_prefix, abbreviated))

    return routines

make_template(template_name, *args)

Converts self into a template of its original subclass.

Returns a copy of self as a template of its original subclass, with specified fields replaced with template types. See TemplateClass for more information on template generation.

Parameters:

Name Type Description Default
template_name str

All templates require an identifying name.

required
*args str

properties to clear and replace with template types.

()
Source code in FoSpy/blocks/blocks.py
def make_template(self,template_name:str,*args:str):
    """
    Converts `self` into a template of its original subclass.

    Returns a copy of `self` as a template of its original subclass, with
    specified fields replaced with template types. See
    [`TemplateClass`][FoSpy.blocks.blocks.SingleBlock.TemplateClass] for
    more information on template generation.

    Args:
        template_name: All templates require an identifying name.
        *args: properties to clear and replace with template types.
    """

    from ..parsing.format_fos import format_field

    serial = self.serialize(keepListType=True)
    validators = self.get_validators()
    for key in args:
        val = validators.get(key, None)
        if isinstance(val,type) and (issubclass(val, SingleBlock) or issubclass(val, ListBlock)):
            serial.setdefault(key, [{}])
        else:
            serial[key] = format_field("template")
    serial["template_name"] = template_name
    return self.TemplateClass(*args)(serial)

print_summary(mode='cli') classmethod

Source code in FoSpy/blocks/blocks.py
@classmethod
def print_summary(cls, mode="cli"):
    from .._docs.properties import get_summary

    print(get_summary(cls, mode=mode))

reflex(serialize=True, clean=False, **kwargs) classmethod

Source code in FoSpy/blocks/attachments.py
@classmethod
def reflex(cls, serialize=True, clean=False, **kwargs:dict):
    from .template import TemplateField
    if "file_name" not in kwargs:
        kwargs["file_name"] = TemplateField.serialize()
        kwargs.pop("path", None)
        add_embedded = "embedded" not in kwargs

    elif not any(k in kwargs for k in ("path", "embedded")):
        add_embedded = True

    if add_embedded:
        kwargs["embedded"] = TemplateField.serialize()

    return super().reflex(serialize=serialize, clean=clean, **kwargs)

refresh_attachments(new_copy=None, overwrite=None, **kwargs)

Source code in FoSpy/blocks/blocks.py
def refresh_attachments(self, new_copy=None, overwrite=None, **kwargs):
    from .attachments import Attachment

    if new_copy is None:
        new_copy = self._att_new_copy
    if overwrite is None:
        overwrite = self._att_overwrite

    for propDict in self.__dict__, self.ext.__dict__:
        for key, val in propDict.items():
            if key.startswith("_") or key in self._reserved:
                continue
            if hasattr(val, "refresh_attachments"):
                val.refresh_attachments(new_copy=new_copy, overwrite=overwrite, **kwargs)
            elif isinstance(val, Attachment) and hasattr(val, "refresh"):
                val.refresh(new_copy=new_copy, overwrite=overwrite, **kwargs)

register_dispatch(registry_val, **kwargs) classmethod

Source code in FoSpy/blocks/attachments.py
@classmethod
def register_dispatch(cls, registry_val, **kwargs):
    extension = registry_val or ".txt"
    fn = "attachment"+extension
    return super().register_dispatch(registry_val, setup_from_key="_location",
                                     setup_allow_self=False, defaults={"file_name":fn},
                                     inherit_dispatch=True,
                                     **kwargs)

rename_block(old, new)

Source code in FoSpy/blocks/blocks.py
def rename_block(self, old, new):
    validators = self.get_validators()
    req = self.get_req_validators()
    if any(name.startswith("_") for name in (old, new)):
        raise ValueError("You cannot set private attributes (starting with '_') using obj.rename_block()")

    if old in req and new in validators:
        raise ValueError(f"You cannot rename '{old}' to '{new}'. '{old}' is a required property that "
                            f"can only be renamed to an unregistered key; '{new}' is already registered "
                            "as an expected property.")

    if hasattr(self, new):
        raise ValueError(f"'{new}' is already a property for this object, you cannot overwrite it with "
                         "obj.rename_block()")

    if "rename" in (old, new):
        raise ValueError("obj.rename property cannot be set or changed by obj.rename_block()")

    if hasattr(self, "rename") and hasattr(self.rename, old):
        old = getattr(self.rename, old)()

    if old in self._key_overrides:
        val = self._key_overrides.pop(old)
        self._key_overrides[new] = val

    else:
        if not hasattr(self,"rename"):
            self.rename = {}

        rename_dict = self.rename_dict()
        rename_from = {v:k for k,v in rename_dict.items()}
        if old in rename_from:
            base = rename_from[old]
        else:
            base = old

        _debug.msg(f"Registering '{base}':'{new}' into rename block")
        setattr(self.rename, base, new)
    _debug.msg(f"Moving '{old}' over to '{new}'.")
    setattr(self,new,getattr(self, old))
    delattr(self,old)

    try:
        idx = self._key_order.index(old)
        self._key_order[idx] = new
    # TODO: Better handling
    except Exception:
        self._key_order.append(new)

rename_dict()

Source code in FoSpy/blocks/blocks.py
def rename_dict(self):
    if not hasattr(self, "rename"):
        return {}
    return self.rename.serialize(shallow=True, clean=True)

serialize(**kwargs)

Performs the default SingleBlock serialization, but restores the "embedded" key to the full list of embedded lines instead of a string.

Source code in FoSpy/blocks/attachments.py
def serialize(self,**kwargs):
    """
    Performs the default `SingleBlock` serialization, but restores the
    "embedded" key to the full list of embedded lines instead of a string.
    """
    serial = super().serialize(**kwargs)
    #serial["embedded"] = self.embedded.copy()
    return serial

set_dispatch(value=None, from_parent=None, from_key=None, allow_self=None) classmethod

Source code in FoSpy/blocks/blocks.py
@classmethod
def set_dispatch(cls, value=None, from_parent=None, from_key=None, allow_self=None):

    # Abstract classes are sometimes made without SingleBlock in MRO
    if from_parent is not None:
        target_cls = from_parent
    else:
        target_cls = cls

    if "dispatch" not in target_cls.__dict__:
        target_cls.dispatch = {}

    if from_key is not None:
        target_cls.dispatch_key = from_key

    if allow_self is not None:
        target_cls.dispatch_allow_self = allow_self

    def dispatched_cls(subcls, v=value, _cls=target_cls):
        subcls.dispatch_from = _cls
        _cls.dispatch[v] = subcls
        return subcls
    return dispatched_cls

setup_dispatch(from_key=None, allow_self=True, _dispatch_from=None, _defaults={}) staticmethod

Decorate a class to dispatch to other classes during construction.

The decorated class's add_dispatch method will be wrapped into a new method, dispatch_subclass, which is decorator as a dispatcher. The parent class's from_key is found in the blockDict passed to the constructor, and the value mapped to from_key is mapped to dispatchable subclasses in the registry.

add_dispatch returns a dictionary of values that are injected into the blockDict, either to be detected by dispatch, or to be delegated to the constructor.

This decorator should only be used directly for the start of a dispatch chain. For later dispatches, use register_dispatch

Parameters:

Name Type Description Default
cls SingleBlock subclass

The class to be decorated. If provided, the decorator is most likely being called as a bare decorator. Otherwise, the decorators is being called with other keyword arguments and returns the modified decorator.

None
from_key str

The key to be located in the blockDict after optional injection by add_dispatch. Private from_keys will be injected and located under the __dispatch__ key which is popped before final construction.

None
allow_self bool

When True, the decorated class will dispatch to itself if no subclasses can be found. When False, error is raised during construction if dispatchable subclass is not found.

True
_dispatch_from SingleBlock subclass

To be passed only by register_dispatch, which decorates subclasses to populate this class's registry. Identifies the parent class that the constructor must start at. If not provided, the decorated class is assumed to be the start of a dispatch chain.

None
_defaults dict

To be passed only by register_dispatch, which decorates subclasses to populat this class's registry. Provides default values that should be injected into the blockDict when trying to guarantee dispatching to the decorated class (usually by a template constructor).

{}
Source code in FoSpy/blocks/blocks.py
@staticmethod
def setup_dispatch(cls:type[BlockType]=None,
    from_key=None,
    allow_self=True,
    _dispatch_from=None,
    _defaults={}
):
    """
    Decorate a class to dispatch to other classes during construction.

    The decorated class's [`add_dispatch`
    method][FoSpy.blocks.blocks.SingleBlock.add_dispatch] will be wrapped
    into a new method,
    [`dispatch_subclass`][FoSpy.blocks.blocks.SingleBlock.dispatch_subclass],
    which is decorator as a
    [`dispatcher`][FoSpy.blocks.blocks.SingleBlock.dispatcher]. The parent
    class's `from_key` is found in the blockDict passed to the constructor,
    and the value mapped to `from_key` is mapped to dispatchable subclasses
    in the `registry`.

    `add_dispatch` returns a dictionary of values that are injected into the
    blockDict, either to be detected by dispatch, or to be delegated to the
    constructor.

    This decorator should only be used directly for the start of a dispatch
    chain. For later dispatches, use
    [`register_dispatch`][FoSpy.blocks.blocks.SingleBlock.register_dispatch]

    Args:
        cls (SingleBlock subclass):
            The class to be decorated. If provided, the decorator is most
            likely being called as a bare decorator. Otherwise, the
            decorators is being called with other keyword arguments and
            returns the modified decorator.

        from_key (str):
            The key to be located in the blockDict after optional injection
            by `add_dispatch`. Private `from_key`s will be injected and
            located under the `__dispatch__` key which is popped before
            final construction.

        allow_self (bool):
            When True, the decorated class will dispatch to itself if no
            subclasses can be found. When False, error is raised during
            construction if dispatchable subclass is not found.

        _dispatch_from (SingleBlock subclass):
            To be passed only by `register_dispatch`, which decorates
            subclasses to populate this class's registry. Identifies the
            parent class that the constructor must start at. If not
            provided, the decorated class is assumed to be the start of a
            dispatch chain. 
        _defaults (dict):
            To be passed only by `register_dispatch`, which decorates
            subclasses to populat this class's registry. Provides default
            values that should be injected into the blockDict when trying to
            guarantee dispatching to the decorated class (usually by a
            template constructor).
        """
    if cls is not None and not isinstance(cls, type):
        raise Exception("@setup_dispatch must be used as a bare decorator, or with "
                        "a class as the first positional argument. You may have tried "
                        "to decorate a class with positional args instead of keywords.")

    def decorator(_cls:type[BlockType], _fk=from_key, _as=allow_self, _df=_dispatch_from, _def=_defaults):
        _cls.__dispatch__ = {
            "from_key": _fk,
            "allow_self": _as,
            "dispatch_from": _df or _cls,
            "registry": {}
        }

        def inject(bD, k, v, is_default=False):
            target_dict = bD["__dispatch__"] if k.startswith("_") else bD

            if is_default and v is None and k in target_dict:
                return bD

            target_dict[k] = v

            return bD

        @classmethod
        def inject_defaults(current_cls, blockDict, _d=_def):
            d = current_cls.__dispatch__
            blk_d = blockDict.setdefault("__dispatch__", {})
            if (not d['allow_self'] and
                None not in d['registry'] and
                blockDict.get(d['from_key'],blk_d.get(d['from_key'], None)) is None):
                default_dispatch = next(iter(d['registry'].values()))
                blockDict = default_dispatch.inject_defaults(blockDict)

            for k, v in _d.items():
                blockDict = inject(blockDict, k, v, is_default=True)
            return blockDict

        @SingleBlock.dispatcher
        def dispatch_subclass(current_cls:type[BlockType], blockDict:dict, _dispatch_key=_fk, **kwargs):
            injection = current_cls.add_dispatch(blockDict, _dispatch_key, _wrapped=True, **kwargs)

            for k, v in injection.items():
                blockDict = inject(blockDict, k, v)

            return blockDict

        # inject methods
        _cls.inject_defaults = inject_defaults
        _cls.dispatch_subclass = dispatch_subclass

        return _cls

    if cls is not None:
        return decorator(cls)

    return decorator

stage_template(prop_name, template=None)

Source code in FoSpy/blocks/blocks.py
def stage_template(self, prop_name, template:Block|dict=None):
    from .template import TemplateBlock
    if template is None:
        template = {}

    if not isinstance(template, (TemplateBlock, dict)):
        raise ValueError("Template must be a TemplateBlock or dictionary. To 'stage' a ListBlock, "
                         "you can stage a SingleBlock template with a ListBlock alias. This creates "
                         "a non-template ListBlock with the template staged as its first entry.")

    alias_validator = None
    if "$" in prop_name:
        prop_name, alias = prop_name.split("$",1)
        try:
            alias_validator = self._aliases[alias]
        except KeyError as e:
            raise err.PropertyAliasError(prop_name, self, blockDict={prop_name: "<staged template>"},
                                         hint=f"Unrecognized block alias: '{alias}' assigned to property: ",
                                         posthint=f"Valid aliases: {list(self._aliases.keys())}") from e

    if hasattr(self, prop_name):
        raise ValueError(f"Property {prop_name} already exists. You cannot stage a template for a property that already exists.")

    validators = self.build_validators()
    validator = validators.get(prop_name, None)

    if validator is not None:
        alias = None

    if alias_validator is not validator and None not in (validator, alias_validator):
        raise ValueError(f"Property {prop_name} already has a validator. You cannot alias a different validator for the same property.")

    validator = next(v for v in (validator, alias_validator) if v is not None)

    if validator is None:
        try:
            if not isinstance(template, TemplateBlock):
                raise TypeError("Dictionary templates must be staged with an alias.")

            alias = next(k for k, v in self._aliases.items() if isinstance(template, v))
            validator = self._aliases[alias]
        except (TypeError,StopIteration) as e:
            raise ValueError(f"Property {prop_name} is unexpected. In order to stage a template "
                            "for and unexpected property, you must specify the validator with a '$' alias "
                            "in the property name, or stage a pre-constructed template of an aliasable validator."
                            ) from e

    if isinstance(validator, type) and issubclass(validator, ListBlock):
        # let setattr handle ListBlock construction using alias
        # this creates an empty ListBlock under self.prop_name
        # (alias stripped during setattr)
        setattr(self, prop_name+"$"+alias if alias is not None else prop_name, [])
        empty_lb = getattr(self, prop_name)
        return empty_lb.stage_template("entry0", template)

    if isinstance(template, dict):
        # reflex returns a TemplateBlock subclassed from the validator
        template = validator.reflex(serialize=False, include_temp_names=True, clean=False, **template)
        template.template_name = prop_name

    elif not isinstance(template, validator):
        val_nm = validator.__name__
        if alias is None:
            error_msg = f"The provided template is not compatible with the validator expected for property '{prop_name}' ({val_nm})."
        else:
            error_msg = f"The provided template is not compatible with the validator specified by alias '{alias}' ({val_nm})."
        raise ValueError(error_msg)


    template._staged_parent = self

    if alias is not None:
        self._key_overrides[prop_name] = validator

    self._staged_templates[prop_name] = template

    return prop_name, template

to_json(filepath=None, clean=True, indent=4, **kwargs)

Converts self into a JSON-formatted string or file.

Serializes and either returns as a JSON-formatted string or saves to a JSON file.

Parameters:

Name Type Description Default
filepath pathlike

JSON file save destination. If None, returns JSON-formatted string instead.

None
clean bool

When True, no FOS format read/write metadata is included in the serial. FOS metadata has no impact on JSON format but may be useful to view in JSON for troubleshooting.

True
indent int

indent value passed to json.dump for file saving.

4
**kwargs any

other arguments passed to json.dump for file saving.

{}
Source code in FoSpy/blocks/blocks.py
def to_json(self, filepath=None, clean:bool=True, indent:int=4, **kwargs):
    """
    Converts `self` into a JSON-formatted string or file.

    [Serializes][FoSpy.blocks.blocks.SingleBlock.serialize] and either
    returns as a JSON-formatted string or saves to a JSON file.

    Args:
        filepath (pathlike):
            JSON file save destination. If `None`, returns JSON-formatted
            string instead.

        clean:
            When True, no FOS format read/write metadata is included in the
            serial. FOS metadata has no impact on JSON format but may be
            useful to view in JSON for troubleshooting.

        indent:
            `indent` value passed to `json.dump` for file saving.

        **kwargs (any):
            other arguments passed to `json.dump` for file saving.
    """
    import json
    serial = self.serialize(clean=clean)

    if filepath is None:
        return json.dumps(serial)

    with open(filepath, "w") as f:
        json.dump(serial, f, indent=indent, **kwargs)

track_attachments(new_copy='prompt', overwrite='prompt', **kwargs)

Source code in FoSpy/blocks/blocks.py
def track_attachments(self, new_copy="prompt",overwrite="prompt", **kwargs):
    self._att_new_copy = new_copy
    self._att_overwrite = overwrite

PathFile

Bases: Attachment

Methods:

Name Description
TemplateClass

Create a template for a subclass of SingleBlock.

__delattr__
__eq__

Check equality of two SingleBlock objects.

__getattr__

Check both self and self.ext for attribute before returning.

__hash__
__init__
__new__
__setattr__
_assign_and_inject

Attaches attributes and methods to any value before assigning it as an

_get_abspath
_get_filedir
_get_filepath
_meta_to_front

Moves metadata to the front of _key_order. Metadata will always be

_rename_validators

Realigns any renamed

_resolve_relative_path

Resolves a relative object path string into an object or function.

_subprocess
_update_src
_validate_filename
add_all_calc_routines

Schedule all available calculation routines.

add_block

Adds an unexpected attribute with a validator mapped by type_alias.

add_calc_comment

Add a calculated comment to be injected during serialization.

add_calc_routine

Schedules a calculated comment.

add_comments

Default behavior to be overwritten when attached to a parent block.

add_dispatch
build_req_validators

Builds required keys and validators mapped to subclass.

build_validators

Builds expected keys and validators mapped to subclass.

change_path
clear_all_comments
clear_comments

Clear comments attached to top-level attributes only.

copy
default_key_order

Set to default attribute order for serialization.

dispatch_subclass
dispatcher

Decorate a classmethod to dispatch to other subclasses.

enforce_subtype
exists
fill_staged_template
find_attachments
find_fileblock

Finds the parent file object.

find_tempdir

Find the parent file object's temporary directory.

find_temppath

Find the parent file object's temporary directory path.

get_id

Returns an easily recognizable identifier for self. Non-unique.

get_parent_prop
get_prop_dict

Returns a dictionary mapping property names to their live object values.

get_prop_path
get_req_validators

Overrides class validators with any renamed properties.

get_validators

Overrides class validators with any renamed properties.

has_staged
inject_defaults
inspect
key_to_idx

Reorder attributes for serialization.

keys_to_end

Reorder attributes for serialization.

keys_to_front

Reorder attributes for serialization.

list_avail_routines

Lists all calc routines available to be added to self._calc_routines.

make_template

Converts self into a template of its original subclass.

print_summary
reflex
refresh
refresh_attachments
register_dispatch
rename_block
rename_dict
serialize

Return a recursively serialized dict representation of self.

set_dispatch
setup_dispatch

Decorate a class to dispatch to other classes during construction.

stage_template
to_json

Converts self into a JSON-formatted string or file.

track_attachments
Source code in FoSpy/blocks/attachments.py
@AnyFile.register_dispatch("path")
class PathFile(Attachment):
    def __init__(self, blockDict, **kwargs):
        super().__init__(blockDict, **kwargs)
    def _get_abspath(self):
        filedir = self._get_filedir().resolve()

        return (filedir / str(self.path) / self.file_name()).resolve()

    def _get_filedir(self):
        from pathlib import Path
        fileblock = self.find_fileblock()
        return Path(fileblock._sourceFile).parent.resolve()

    def exists(self):
        return self._filepath.is_file() if self._filepath is not None else False

    def refresh(self, new_copy="prompt",overwrite="prompt", **kwargs):
        from .. import cfg
        try:
            if self._filepath is None:
                self._get_filepath(rf_new_copy=new_copy, rf_overwrite=overwrite, **kwargs)
                return
            if not self.exists():
                raise ValueError(f"Cannot find file at last known location of attachment: {self._filepath}")

            new_path = self._get_abspath()
            if self._filepath != new_path:
                printmsg = f"Attachment file path has changed: {self._filepath} -> {new_path}"
                prompted=False
                if new_copy == "prompt":
                    print(printmsg)
                    prompted=True

                if new_copy == "prompt":
                    new_copy = input("You can copy the file to the new location, "
                                    "or modify the path value to match the old location. "
                                    "Copy? (y/n): ").lower() == "y"

                if new_copy:
                    if new_path.is_file() and overwrite == "prompt":
                        if not prompted:
                            print(printmsg)
                        overwrite = input("File already exists at new location. "
                                        "Overwrite? (y/n): ").lower() == "y"
                    if overwrite or not new_path.is_file():
                        import shutil
                        shutil.copyfile(self._filepath, new_path)
                        self._filepath = new_path

                else:
                    new_path = self._filepath.parent.resolve().relative_to(self._get_filedir(),walk_up=True)
                    self.path = str(new_path)

            checkpath = self._get_abspath()
            check = checkpath.is_file()
            if not check:  
                raise ValueError(f"Could not successfully refresh attachment to location: {checkpath}.")

            self._filepath = checkpath

        except Exception as e:
            if not cfg.track_attachments.ignore:
                raise e
            else:
                _debug.msg(f"Could not refresh attachment: {e}. Configured to ignore.")

    def change_path(self, new_path):
        from pathlib import Path
        import warnings
        abspath = Path(new_path).resolve()

        parent = abspath.parent
        filename = abspath.name
        ext = self._extension if hasattr(self, "_extension") else None

        warnings.simplefilter("always")
        filename, ext = self._validate_filename(filename, ext, warn=False) 
        abspath = parent / filename

        if not abspath.is_file():
            raise ValueError(f"Cannot change path to file that does not exist: {abspath}")

        self.file_name = filename
        self.path = str(abspath.parent.relative_to(self._get_filedir(),walk_up=True))
        self._filepath = abspath
        self.refresh()

    def _get_filepath(self, rf_new_copy=False, rf_overwrite=False, **kwargs):
        if self._filepath is None:
            try:
                self._filepath = self._get_abspath()
            except FileBlockNotFoundError:
                self._filepath = None
        else:
            self.refresh(new_copy=rf_new_copy, overwrite=rf_overwrite, **kwargs)
        return self._filepath

    def copy(self):
        copy = super().copy()
        copy._filepath = self._filepath
        return copy

_aliases = new_als class-attribute instance-attribute

_calc_comments = {} instance-attribute

_calc_routines = [] instance-attribute

_constructed = True instance-attribute

_filepath = None instance-attribute

_id_key = 'file_name' class-attribute instance-attribute

_key_order = [] instance-attribute

_key_overrides = {} instance-attribute

_meta = SubContainer() instance-attribute

_reserved = ['ext'] instance-attribute

_sourceDict = blockDict.copy() instance-attribute

_staged_templates = {} instance-attribute

dispatch = {} class-attribute instance-attribute

dispatch_allow_self = True class-attribute instance-attribute

dispatch_default = None class-attribute instance-attribute

dispatch_key = None class-attribute instance-attribute

ext = SubContainer() instance-attribute

rename = rename instance-attribute

TemplateClass(*args) classmethod

Create a template for a subclass of SingleBlock.

Generates a hybridized subclass of the current block class and TemplateBlock. Template subclasses override original expected validators with either a TemplateField, TemplateBlock, or TemplateList depending on the type of the original validator.

Parameters:

Name Type Description Default
*args str

A list of properties to override as template types.

()
Source code in FoSpy/blocks/blocks.py
@classmethod
def TemplateClass(cls,*args:str):
    """
    Create a template for a subclass of `SingleBlock`.

    Generates a hybridized subclass of the current block class and
    [`TemplateBlock`][FoSpy.blocks.template.TemplateBlock]. Template
    subclasses override original expected validators with either a
    [`TemplateField`][FoSpy.blocks.template.TemplateField],
    [`TemplateBlock`][FoSpy.blocks.template.TemplateBlock], or
    [`TemplateList`][FoSpy.blocks.template.TemplateList] depending on the
    type of the original validator.

    Args:
        *args: A list of properties to override as template types.
    """
    from .template import TemplateBlock, FlexTemplate

    cls_registry = TemplateBlock.__dispatch__["registry"]

    if cls not in cls_registry:

        @TemplateBlock.register_dispatch(cls, setup_from_key="_fields", setup_allow_self=True, inherit_dispatch=True)
        class TemplateLocator(FlexTemplate, TemplateBlock, cls):
            _full_class = cls

        TemplateLocator.__name__ = f"{cls.__name__}TemplateLocator"
        TemplateLocator.__qualname__ = f"{cls.__name__}.TemplateClass.Locator"
        TemplateLocator.__module__ = cls.__module__

    fields = tuple(sorted(args))

    # construct a proxy dictionary that will correctly dispatch to the right
    # template class in TemplateBlock's dispatch chain.
    proxy_dict = {
        "__dispatch__": {
            "_full_class": cls,
            "_fields": fields
        }
    }

    return TemplateBlock.dispatch_subclass(proxy_dict)

__delattr__(attr)

Source code in FoSpy/blocks/blocks.py
def __delattr__(self, attr):
    if attr in self.get_req_validators():
        raise AttributeError(f"Cannot delete property: '{attr}'. It is registered as a required property for this object.")
    return super().__delattr__(attr)

__eq__(other, suppress_routine_paths=False)

Check equality of two SingleBlock objects.

Equality is checked by a deep difference of their serialized dictionaries.

Parameters:

Name Type Description Default
suppress_routine_paths bool

Optional flag to still return true if the only differences found are in calculation routine metadata. Calculation routines are for user information only and may not be relevant for equality.

False
Source code in FoSpy/blocks/blocks.py
def __eq__(self, other, suppress_routine_paths:bool=False):
    """
    Check equality of two `SingleBlock` objects.

    Equality is checked by a deep difference of their
    [serialized][FoSpy.blocks.blocks.SingleBlock.serialize] dictionaries.

    Args:
        suppress_routine_paths:
            Optional flag to still return true if the only differences found
            are in [calculation
            routine][FoSpy.blocks.blocks.SingleBlock.add_calc_routine]
            metadata. Calculation routines are for user information only and
            may not be relevant for equality.
    """
    from .._debug import deep_diff as dd, _debug as db
    try:
        db.msg("Serializing Blocks to check equality:", module = "SingleBlock.__eq__()")
        diffs = dd(self.serialize(), other.serialize(), suppress_routine_paths=suppress_routine_paths)
        passed = len(diffs) == 0
        if not passed:
            db.pmsg(diffs,module = "SingleBlock.__eq__()")
        return passed
    except Exception as e:
        db.msg(f"Equality failed by exception: {e}",module = "SingleBlock.__eq__()")
        return False

__getattr__(name)

Check both self and self.ext for attribute before returning.

A matching attribute of self will be returned first, but if self has no matching attribute, a matching attribute of self.ext can be returned instead.

Source code in FoSpy/blocks/blocks.py
def __getattr__(self, name:str):
    """
    Check both `self` and `self.ext` for attribute before returning.

    A matching attribute of `self` will be returned first, but if `self` has
    no matching attribute, a matching attribute of `self.ext` can be
    returned instead.
    """

    try:
        if name not in ("rename", "ext") and hasattr(self, "rename"):
            rename_dict = self.rename.serialize(shallow=True, clean=True)
            if name in rename_dict:
                return getattr(self, rename_dict[name])

        if name != 'ext':
            return getattr(self.ext, name)

        raise AttributeError()
    except AttributeError:
        raise AttributeError(
            f"{type(self).__name__} object "
            f"has no attribute {name!r}."
        )

__hash__()

Source code in FoSpy/blocks/blocks.py
def __hash__(self):
    return id(self)

__init__(blockDict, **kwargs)

Source code in FoSpy/blocks/attachments.py
def __init__(self, blockDict, **kwargs):
    super().__init__(blockDict, **kwargs)

__new__(blockDict, *args, **kwargs)

Source code in FoSpy/blocks/blocks.py
def __new__(cls, blockDict, *args, **kwargs):
    _dispatched = kwargs.pop("_dispatched", False)
    if _dispatched:
        # blockDict should always be dict after dispatch. I want to see attributeerror if not.
        blockDict.pop("__dispatch__",None)
        return super().__new__(cls)

    blockDict = _unwrap_block(blockDict)

    dispatched_cls = cls.dispatch_subclass(blockDict, *args, **kwargs)

    if issubclass(dispatched_cls, cls):
        return dispatched_cls(blockDict, *args, _dispatched=True, **kwargs)

    dispatch = blockDict.pop("__dispatch__")
    raise err.BlockDispatchError(
        f"Attempted to construct the following dictionary as a {cls.__name__} block, "
        f"but it was dispatched to a {dispatched_cls.__name__} instead."
        f"\n\nINPUT:\n{blockDict}"
        f"\n\nDISPATCH:\n{dispatch}")

__setattr__(name, value)

Source code in FoSpy/blocks/attachments.py
def __setattr__(self, name, value):
    if name == "_extension":
        if value is None:
            return
        if hasattr(self, "_extension") and value != self._extension:
            from warnings import warn
            warn("You cannot change the extension of an attachment after construction. Skipping change.", RuntimeWarning)
            return

    if name == "file_name":
        old_ext = self._extension if hasattr(self, "_extension") else None
        value, new_ext = self._validate_filename(value, old_ext)
        self._extension = new_ext

    return super().__setattr__(name, value)

_assign_and_inject(name, value, extended=False)

Attaches attributes and methods to any value before assigning it as an attribute of self or self.ext.

Attributes Attached to Object

_parent_block: refers to self

Methods Attached to Object

add_comments_to_parent clear_comments_from_parent

Source code in FoSpy/blocks/blocks.py
def _assign_and_inject(self, name, value, extended=False):
    """
    Attaches attributes and methods to any value before assigning it as an
    attribute of `self` or `self.ext`.

    Attributes Attached to Object:
        `_parent_block`: refers to `self`

    Methods Attached to Object:
        [`add_comments_to_parent`][FoSpy.blocks.blocks._add_comments_to_parent]
        [`clear_comments_from_parent`][FoSpy.blocks.blocks._clear_comments_from_parent]
    """
    from .attachments import Attachment

    if name == 'ext':
        return super().__setattr__('ext', value)
    if not hasattr(value, "__dict__"):
        value = SimpleWrapper(value)

    if extended:
        setattr(self.ext, name, value)
    else:
        super().__setattr__(name, value)

    attr_obj = getattr(self.ext if extended else self, name)

    setattr(attr_obj, "_parent_block", self)

    if isinstance(attr_obj, Attachment):
        attr_obj._get_filepath()
    elif hasattr(attr_obj, "refresh_attachments"):
        attr_obj.refresh_attachments()

    methods = ((_add_comments_to_parent(name), "add_comments"),
            (_clear_comments_from_parent(name), "clear_comments"))

    attr_obj._reserved = ['ext'] if not hasattr(attr_obj,"_reserved") else attr_obj._reserved
    for method, method_name in methods:
        attr_obj._reserved.append(method_name)
        bound = method.__get__(attr_obj, type(attr_obj))
        setattr(attr_obj, method_name, bound)

    self._props_changed = True

_get_abspath()

Source code in FoSpy/blocks/attachments.py
def _get_abspath(self):
    filedir = self._get_filedir().resolve()

    return (filedir / str(self.path) / self.file_name()).resolve()

_get_filedir()

Source code in FoSpy/blocks/attachments.py
def _get_filedir(self):
    from pathlib import Path
    fileblock = self.find_fileblock()
    return Path(fileblock._sourceFile).parent.resolve()

_get_filepath(rf_new_copy=False, rf_overwrite=False, **kwargs)

Source code in FoSpy/blocks/attachments.py
def _get_filepath(self, rf_new_copy=False, rf_overwrite=False, **kwargs):
    if self._filepath is None:
        try:
            self._filepath = self._get_abspath()
        except FileBlockNotFoundError:
            self._filepath = None
    else:
        self.refresh(new_copy=rf_new_copy, overwrite=rf_overwrite, **kwargs)
    return self._filepath

_meta_to_front()

Moves metadata to the front of _key_order. Metadata will always be serialized first, but being elsewhere in the order leads to unexpected results when moving other keys to desired indices.

Source code in FoSpy/blocks/blocks.py
def _meta_to_front(self):
    """
    Moves metadata to the front of `_key_order`. Metadata will always be
    serialized first, but being elsewhere in the order leads to unexpected
    results when moving other keys to desired indices.
    """
    try:
        meta_idx =self._key_order.index("metadata")
        self._key_order.pop(meta_idx)
    # TODO: Better handling
    except Exception:
        pass
    self._key_order.insert(0,"metadata")

_rename_validators(validators)

Realigns any renamed attributes with their expected validator.

Parameters:

Name Type Description Default
validators dict

A dictionary mapping attribute names to validators, returned by either build_validators or build_req_validators

required
Source code in FoSpy/blocks/blocks.py
def _rename_validators(self, validators:dict):
    """
    Realigns any [renamed][FoSpy.blocks.blocks.SingleBlock.rename_block]
    attributes with their expected validator.

    Args:
        validators:
            A dictionary mapping attribute names to validators, returned by
            either
            [`build_validators`][FoSpy.blocks.blocks.SingleBlock.build_validators]
            or
            [`build_req_validators`][FoSpy.blocks.blocks.SingleBlock.build_req_validators]
    """
    if hasattr(self, "rename"):
        for name, rename in self.rename.serialize(shallow=True, clean=True).items():
            if name in validators and rename not in validators:
                val = validators.pop(name)
                validators[rename] = val
    return validators

_resolve_relative_path(path)

Resolves a relative object path string into an object or function.

Example:

    mySyn._resolve_relative_path("materials[1].ratio")
    ## returns mySyn.materials[1].ratio

Source code in FoSpy/blocks/blocks.py
def _resolve_relative_path(self, path: str):
    """
    Resolves a relative object path string into an object or function.

    Example:
    ```
        mySyn._resolve_relative_path("materials[1].ratio")
        ## returns mySyn.materials[1].ratio
    ```
    """
    import re

    _index_re = re.compile(r"^([A-Za-z_]\w*)\[(\d+)\]$")
    obj = self

    for part in path.split("."):

        # Case: attr[index]
        m = _index_re.match(part)
        if m:
            attr_name, idx_str = m.groups()
            idx = int(idx_str)

            # Get the ListBlock
            obj = getattr(obj, attr_name)

            # Index into its _objs
            obj = obj._objs[idx]
            continue

        # Case: simple attribute
        obj = getattr(obj, part)

    return obj

_subprocess(target, args=(), **kwargs)

Source code in FoSpy/blocks/blocks.py
def _subprocess(self, target, args=(), **kwargs):
    from multiprocessing import Process

    if kwargs is None:
        kwargs={}

    p = Process(target=target, args=args, kwargs=kwargs)
    p.start()

_update_src()

Source code in FoSpy/blocks/blocks.py
def _update_src(self):
    if self._constructed and self._props_changed:
        self._sourceDict = self.serialize(clean=True)
        self._props_changed = False

    return self._sourceDict   

_validate_filename(filename, ext=None, warn=True) classmethod

Source code in FoSpy/blocks/attachments.py
@classmethod
def _validate_filename(cls, filename:str, ext:str=None, warn=True):
    filename = str(filename)
    if ext is None:
        ext = f".{filename.rsplit('.')[-1]}" if "." in filename else ""
        # delegate to base validator routine to verify extension
        return filename, ext

    if "." not in filename:
        new_ext = ext
    else:
        new_ext = f".{filename.rsplit('.')[-1]}"

    if new_ext != ext:
        if warn:
            filename = filename + ext
            from warnings import warn
            warn(f"New filename contains a different extension: '{new_ext}'. Extensions cannot "
                f"be changed after construction. The current extension ('{ext}') "
                f"will be appended to the new filename to form: '{filename}'.", RuntimeWarning)
        else:
            raise ValueError(f"New filename contains a different extension: '{new_ext}'. Extensions cannot "
                             "be changed after attachment construction.")

    return filename, new_ext

add_all_calc_routines(recursive=False)

Schedule all available calculation routines.

Adds all available calc_routines to self._calc_routines using list_avail_routines() and add_calc_routine().

Parameters:

Name Type Description Default
recursive bool

Optional recursion. See SingleBlock.list_avail_routines()

False
Source code in FoSpy/blocks/blocks.py
def add_all_calc_routines(self, recursive:bool=False):
    """
    Schedule all available calculation routines.

    Adds all available calc_routines to `self._calc_routines` using
    [`list_avail_routines()`][FoSpy.blocks.blocks.SingleBlock.list_avail_routines]
    and
    [`add_calc_routine()`][FoSpy.blocks.blocks.SingleBlock.add_calc_routine].

    Args:
        recursive:
            Optional recursion. See `SingleBlock.list_avail_routines()`
    """
    for path in self.list_avail_routines(recursive=recursive, abbreviated=False):
        self.add_calc_routine(path)

add_block(block_name, type_alias, value=[])

Adds an unexpected attribute with a validator mapped by type_alias. Unexpected attributes not requiring a validator can be set directly without using this method.

Parameters:

Name Type Description Default
block_name str

new unexpected attribute name

required
type_alias str

Alias mapped to the desired validator in parsing.validation.aliases. For more information on how aliases are used, see __setattr__.

required
Source code in FoSpy/blocks/blocks.py
def add_block(self, block_name:str, type_alias:str, value=[]):
    """
    Adds an unexpected attribute with a validator mapped by `type_alias`.
    Unexpected attributes not requiring a validator can be set directly
    without using this method.

    Args:
        block_name: new unexpected attribute name
        type_alias:
            Alias mapped to the desired validator in
            [`parsing.validation.aliases`][FoSpy.parsing.validation.aliases].
            For more information on how aliases are used, see
            [`__setattr__`][FoSpy.blocks.blocks.SingleBlock.__setattr__].
    """
    if hasattr(self,block_name):
        raise ValueError(f"This object already has attribute: '{block_name}'.")
    return setattr(self, f"{block_name}${type_alias}", value)

add_calc_comment(key, comment, calc_id)

Add a calculated comment to be injected during serialization.

WARNING: This function can leave outdated calculations in comments after serialization. Recommended to use add_calc_routine() instead.

Calculated comments are for user information and will be formatted to be skipped by the parser when reading the file. This is useful for comments that should be recalculated and refreshed during saving/serialization, like weight percentages or summaries.

Parameters:

Name Type Description Default
key str

attribute to attach the calculated comment to. Comments appear above their attached attributes in FOS format.

required
comment str

comment text without comment formatting (don't include // or !)

required
calc_id str

unique identifier for the calculated comment. If it matches an existing comment (like when refreshing a value), the comment is overwritten

required
Source code in FoSpy/blocks/blocks.py
def add_calc_comment(self, key:str, comment:str, calc_id:str):
    """
    Add a calculated comment to be injected during serialization.

    WARNING: This function can leave outdated calculations in comments after
    serialization. Recommended to use `add_calc_routine()` instead.

    Calculated comments are for user information and will be formatted to be
    skipped by the parser when reading the file. This is useful for comments
    that should be recalculated and refreshed during saving/serialization,
    like weight percentages or summaries.

    Args:
        key:
            attribute to attach the calculated comment to. Comments appear
            above their attached attributes in FOS format.
        comment:
            comment text without comment formatting (don't include // or !)
        calc_id:
            unique identifier for the calculated comment. If it matches an
            existing comment (like when refreshing a value), the comment is
            overwritten

    """
    calc_comments = self._calc_comments.get(key, {})
    self._calc_comments[key] = calc_comments
    self._calc_comments[key][calc_id]=comment

add_calc_routine(path, **kwargs)

Schedules a calculated comment.

Appends a _calc_routine()-decorated function to self._calc_routines to be run at serialization.

Used to add calculated comments that should be refreshed during serialization.

Parameters:

Name Type Description Default
path str

a relative path string that can be resolved into a _calc_routine()-decorated function

required
**kwargs any

optional key word arguments to be passed to the function at path.

{}

Raises:

Type Description
TypeError

the attr or method at path is not registered as a _calc_routine

Example:

    mySyn.add_calc_routine("materials.add_weight_pcts", typ="reagent")
    ## mySyn.materials.add_weight_pcts(typ="reagent") is now scheduled
    ## to run at serialization

Source code in FoSpy/blocks/blocks.py
def add_calc_routine(self, path:str, **kwargs):
    """
    Schedules a calculated comment.

    Appends a
    [`_calc_routine()`][FoSpy.blocks._blockUtils._calc_routine]-decorated
    function to `self._calc_routines` to be run at
    [serialization][FoSpy.blocks.blocks.SingleBlock.serialize].

    Used to add calculated comments that should be refreshed during
    serialization.

    Args:
        path:
            a relative path string that can be resolved into a
            `_calc_routine()`-decorated function
        **kwargs (any):
            optional key word arguments to be passed to the function at
            path.

    Raises:
        TypeError:
            the attr or method at path is not registered as a
            _calc_routine

    Example:
    ```
        mySyn.add_calc_routine("materials.add_weight_pcts", typ="reagent")
        ## mySyn.materials.add_weight_pcts(typ="reagent") is now scheduled
        ## to run at serialization
    ```
    """

    func = self._resolve_relative_path(path)
    if not getattr(func, "_is_calc_routine", False):
        raise TypeError(f"'{path}' is not a registered calc routine.")

    self._meta.routine_paths.append(path)

    def wrapped(f=func, k=kwargs):
        return f(**k)

    self._calc_routines.append(wrapped)

add_comments(*comments)

Default behavior to be overwritten when attached to a parent block.

If a SingleBlock is stored as an attribute of another SingleBlock, this method will be overwritten by the parent's __setattr__.

Source code in FoSpy/blocks/blocks.py
def add_comments(self, *comments):
    """
    Default behavior to be overwritten when attached to a parent block.

    If a `SingleBlock` is stored as an attribute of another `SingleBlock`,
    this method will be overwritten by the parent's `__setattr__`.
    """
    keys = list(self.get_req_validators())

    keys = [k for k in keys if k != "metadata"]
    fallback = [k for k in self._key_order if k != "metadata"]
    if not (keys or fallback):
        raise ValueError("This object has not been correctly attached to a parent block "
                         "and could not identify a required key to attach to.")

    first = keys[0] if keys else fallback[0]

    self._meta.comments.setdefault(first, [])
    for comment in comments:
        self._meta.comments[first].append(comment)

add_dispatch(blockDict, dispatch_key, **kwargs) classmethod

Source code in FoSpy/blocks/attachments.py
@classmethod
def add_dispatch(cls, blockDict, dispatch_key, **kwargs):
    from .. import _errors as err

    # make sure wrapped
    _ = SingleBlock.add_dispatch(blockDict, dispatch_key, **kwargs)

    if "file_name" not in blockDict:
        raise err.MissingPropertyError("file_name", cls, blockDict=blockDict)

    _, ext = cls._validate_filename(blockDict["file_name"])

    return {dispatch_key: ext}

build_req_validators() classmethod

Builds required keys and validators mapped to subclass.

Walks all parent classes and builds a map of all keys that are required during __init__, and their respective validation routines. Subclasses are mapped to expected keys and validations in parsing.validation. Subclass validations override parent classes when applicable.

Returns:

Name Type Description
merged dict

Maps required keys to validation routines. Routines may be a class constructor or a func taking one arg.

Example:

>>> SingleBlock.build_req_validators()
{
    "name": str,
    "type": str,
    "formula": ChemFormula, # class constructor
    "supplier": str,
    "cas": str,
    "form": str,
    "env": str,
    "ratio": validators.material.ratio # validator function
}

Source code in FoSpy/blocks/blocks.py
@classmethod
def build_req_validators(cls):
    """
    Builds required keys and validators mapped to subclass.

    Walks all parent classes and builds a map of all keys that are required
    during `__init__`, and their respective validation routines. Subclasses
    are mapped to expected keys and validations in
    [`parsing.validation`][FoSpy.parsing.validation]. Subclass validations
    override parent classes when applicable.

    Returns:
        merged (dict):
            Maps required keys to validation routines. Routines may be
            a class constructor or a func taking one arg.
    Example:
        ``` 
        >>> SingleBlock.build_req_validators()
        {
            "name": str,
            "type": str,
            "formula": ChemFormula, # class constructor
            "supplier": str,
            "cas": str,
            "form": str,
            "env": str,
            "ratio": validators.material.ratio # validator function
        }
        ```
    """
    from ..parsing.validation import required_keys
    from ._blockUtils import _get_prop_mro, _merge_vals
    merged = {}
    # mro = list(reversed(cls.__mro__))
    # for i, base in enumerate(mro):
    #     base_reqs = required_keys.get(base,{})
    #     for key, validator in base_reqs.items():
    #         # allow subclasses to remove parent requirements.
    #         if not validator:
    #             merged.pop(key, None)
    #         else:
    #             merged[key] = validator

    req_mro = _get_prop_mro(cls, required_keys)
    for i in range(len(req_mro)):
        merged = _merge_vals(merged, req_mro, i)

    merged.pop("__all__")

    return merged

build_validators() classmethod

Builds expected keys and validators mapped to subclass.

Walks all parent classes and builds a map of all keys that are expected (required or optional), and their respective validation routines. Subclasses are mapped to keys and validations in parsing.validation. Subclass validations override parent classes when applicable.

See build_req_validators

Source code in FoSpy/blocks/blocks.py
@classmethod
def build_validators(cls):
    """
    Builds expected keys and validators mapped to subclass.

    Walks all parent classes and builds a map of all keys that are expected
    (required or optional), and their respective validation routines.
    Subclasses are mapped to keys and validations in
    [`parsing.validation`][FoSpy.parsing.validation]. Subclass validations
    override parent classes when applicable.

    See
    [`build_req_validators`][FoSpy.blocks.blocks.SingleBlock.build_req_validators]
    """
    from ..parsing.validation import required_keys, optional_keys
    from ._blockUtils import _merge_vals, _get_prop_mro
    from .._docs.properties import _validator_rules
    merged = {}
    # for base in reversed(cls.__mro__):
    #     for key_set in (required_keys, optional_keys):
    #         base_reqs = key_set.get(base,{})
    #         for key, validator in base_reqs.items():
    #             # allow subclasses to remove parent requirements.
    #             if validator is False:
    #                 merged.pop(key, None)
    #             else:
    #                 merged[key] = validator
    req_mro = _get_prop_mro(cls, required_keys)
    opt_mro = _get_prop_mro(cls, optional_keys)

    for i in range(len(req_mro)): # req_mro and opt_mro are the same length
        merged = _merge_vals(merged, req_mro, i)
        merged = _merge_vals(merged, opt_mro, i)

    universal_val = merged.pop("__all__")

    @_validator_rules(inherit_from=universal_val)
    def universal_val_method(cls, *_, _m=universal_val, **__):
        return _m(*_, **__)

    cls.universal_val = universal_val_method

    return merged

change_path(new_path)

Source code in FoSpy/blocks/attachments.py
def change_path(self, new_path):
    from pathlib import Path
    import warnings
    abspath = Path(new_path).resolve()

    parent = abspath.parent
    filename = abspath.name
    ext = self._extension if hasattr(self, "_extension") else None

    warnings.simplefilter("always")
    filename, ext = self._validate_filename(filename, ext, warn=False) 
    abspath = parent / filename

    if not abspath.is_file():
        raise ValueError(f"Cannot change path to file that does not exist: {abspath}")

    self.file_name = filename
    self.path = str(abspath.parent.relative_to(self._get_filedir(),walk_up=True))
    self._filepath = abspath
    self.refresh()

clear_all_comments()

Source code in FoSpy/blocks/blocks.py
def clear_all_comments(self):
    self._meta.comments = {}
    for attr, val in self.__dict__.items():
        if attr.startswith("_") or attr in self._reserved:
            continue
        if hasattr(val, "clear_all_comments"):
            val.clear_all_comments()

clear_comments()

Clear comments attached to top-level attributes only.

Source code in FoSpy/blocks/blocks.py
def clear_comments(self):
    """
    Clear comments attached to top-level attributes only.
    """
    self._meta.comments = {}

copy()

Source code in FoSpy/blocks/attachments.py
def copy(self):
    copy = super().copy()
    copy._filepath = self._filepath
    return copy

default_key_order(deep=False)

Set to default attribute order for serialization.

Rearrange attribute order to the default order assigned by build_validators

Parameters:

Name Type Description Default
deep bool

When true, recursively calls default_key_order on any other SingleBlock objects stored in attributes.

False
Source code in FoSpy/blocks/blocks.py
def default_key_order(self, deep:bool=False):
    """
    Set to default attribute order for serialization.

    Rearrange attribute order to the default order assigned by
    [`build_validators`][FoSpy.blocks.blocks.SingleBlock.build_validators]

    Args:
        deep:
            When true, recursively calls `default_key_order` on any other
            `SingleBlock` objects stored in attributes.
    """
    new_order = []
    for key in self.get_validators():
        if key != "ext" and key in self.serialize(shallow=True):
            new_order.append(key)
    for key in self._key_order:
        if key not in new_order:
            new_order.append(key)
    self._key_order = new_order
    self._meta_to_front()

    if deep:
        for name, obj in self.__dict__.items():
            if not name.startswith("_") and hasattr(obj, "default_key_order"):
                obj.default_key_order(deep=True)

dispatch_subclass(*args, **kwargs) classmethod

Source code in FoSpy/blocks/blocks.py
@classmethod
def dispatch_subclass(cls, *args, **kwargs):
    # fallback.
    # overridden by setup_dispatch decorator
    return cls

dispatcher(dispatch_method) staticmethod

Decorate a classmethod to dispatch to other subclasses.

Not normally used directly. See setup_dispatch decorator.

Source code in FoSpy/blocks/blocks.py
@staticmethod
def dispatcher(dispatch_method: Callable[..., dict])->classmethod:
    """
    Decorate a classmethod to dispatch to other subclasses.

    Not normally used directly. See
    [`setup_dispatch`][FoSpy.blocks.blocks.SingleBlock.setup_dispatch]
    decorator.
    """

    @classmethod
    def dispatch_subclass(cls:type[BlockType], blockDict, _add_defaults=False, **kwargs):
        from .. import _errors as err
        for_template = kwargs.get("for_template", False)

        block_dispatch = blockDict.setdefault("__dispatch__", {})
        visited = block_dispatch.setdefault("visited", [])
        cls_dispatch = getattr(cls, "__dispatch__", None)
        # shorthand for keying dispatch parameters
        d=cls_dispatch

        if d['dispatch_from'] in visited and (
            cls in visited or
            d is None or
            d['from_key'] is None):
            return cls
        try:
            blockDict = dispatch_method(cls, blockDict, add_defaults=_add_defaults, **kwargs)
        except Exception as e:
            if not for_template:
                raise e

        visited.append(cls)

        if d['dispatch_from'] not in visited:
            blockDict.pop("__dispatch__", None)
            return d['dispatch_from'].dispatch_subclass(blockDict, _add_defaults=_add_defaults, **kwargs)


        dispatch_val = block_dispatch.get(d['from_key'],
                            blockDict.get(d['from_key'], None))

        dispatched_cls = d['registry'].get(dispatch_val, d['registry'].get(None, cls))

        if dispatched_cls is cls and not d['allow_self']:
            if not for_template:
                raise err.BlockDispatchError(
                    f"The following blockDict was dispatched to {cls.__name__} "
                    "but could not be dispatched further. "
                    f"{cls.__name__} blocks are not allowed without a subclass.")
            return cls

        return dispatched_cls.dispatch_subclass(blockDict, **kwargs)
    return dispatch_subclass

enforce_subtype(subcls, **kwargs) classmethod

Source code in FoSpy/blocks/attachments.py
@classmethod
def enforce_subtype(cls, subcls, **kwargs):
    raise DeprecationWarning("Attachments no longer enforce subtype through this method. "
                             "Simply spec the validator as the enforced subtype instead.")

exists()

Source code in FoSpy/blocks/attachments.py
def exists(self):
    return self._filepath.is_file() if self._filepath is not None else False

fill_staged_template(prop_name, **kwargs)

Source code in FoSpy/blocks/blocks.py
def fill_staged_template(self, prop_name, **kwargs):
    from .template import TemplateBlock

    prop_key = prop_name.split("$")[0] if "$" in prop_name else prop_name

    template = self._staged_templates.pop(prop_key, None)
    if template is None:
        prop_name, _ = self.stage_template(prop_name)
        return self.fill_staged_template(prop_name, **kwargs)

    prop_name = prop_key

    filled = template.fill(staged=True,**kwargs)

    if isinstance(filled, TemplateBlock):
        return self.stage_template(prop_name, filled)

    try:
        setattr(self, prop_name, filled)
    except Exception as e:
        raise Exception(f"Template was filled but could not be assigned {prop_name}") from e

    filled = getattr(self, prop_name)

    if isinstance(self, TemplateBlock):
        self.fill()

    return prop_name, filled

find_attachments()

Source code in FoSpy/blocks/attachments.py
def find_attachments(self):
    attachments = super().find_attachments()
    if self not in attachments:
        attachments.append(self)

    return attachments

find_fileblock()

Finds the parent file object.

Walks upward through _parent_block attributes until a FileBlock instance is found and returns that instance.

Source code in FoSpy/blocks/blocks.py
def find_fileblock(self):
    """
    Finds the parent file object.

    Walks upward through `_parent_block` attributes until a
    [`FileBlock`][FoSpy.blocks.files.FileBlock] instance is found and
    returns that instance.
    """
    from .files import FileBlock
    from .._errors import FileBlockNotFoundError

    blk = self
    while blk is not None:
        if isinstance(blk, FileBlock):
            return blk
        if hasattr(blk,"_parent_block"):
            blk = blk._parent_block
        else:
            blk = None
    raise FileBlockNotFoundError("Could not find a FileBlock containing the current object")

find_tempdir()

Find the parent file object's temporary directory.

Finds the temporary directory created by the FileBlock instance containing this block as one of its attributes.

Returns:

Name Type Description
tempdir tempfile.TemporaryDirectory

The temporary directory created by the parent file object

Source code in FoSpy/blocks/blocks.py
def find_tempdir(self):
    """
    Find the parent file object's temporary directory.

    Finds the temporary directory created by the
    [`FileBlock`][FoSpy.blocks.files.FileBlock] instance containing this
    block as one of its attributes. 

    Returns:
        tempdir (tempfile.TemporaryDirectory):
            The temporary directory created by the parent file object
    """
    fileblock = self.find_fileblock()
    if hasattr(fileblock, "_tempdir"):
        return fileblock._tempdir
    else:
        raise AttributeError("Could not find a temporary directory attached to this object's FileBlock")

find_temppath()

Find the parent file object's temporary directory path.

Similar to find_tempdir but returns the corresponding pathlib.Path object instead.

Source code in FoSpy/blocks/blocks.py
def find_temppath(self):
    """
    Find the parent file object's temporary directory path.

    Similar to [`find_tempdir`][FoSpy.blocks.blocks.Block.find_tempdir] but
    returns the corresponding `pathlib.Path` object instead.
    """
    fileblock = self.find_fileblock()
    if hasattr(fileblock, "_temppath"):
        return fileblock._temppath
    if hasattr(fileblock, "_temppdir"):
        raise AttributeError("This object's FileBlock has a temporary directory but no path mapped to it. "
                             "Use obj.find_tempdir() instead")
    raise AttributeError("Could not find a temporary directory object or path "
                         "attached to this object's FileBlock.")

get_id()

Returns an easily recognizable identifier for self. Non-unique.

Source code in FoSpy/blocks/blocks.py
def get_id(self):
    """Returns an easily recognizable identifier for self. Non-unique."""
    id_txt = str(getattr(self, self._id_key)) if self._id_key is not None else type(self).__name__
    return self._id_key, id_txt

get_parent_prop()

Source code in FoSpy/blocks/blocks.py
def get_parent_prop(self):
    if not hasattr(self, "_parent_block"):
        return None
    parent_blk = self._parent_block

    if isinstance(parent_blk, SingleBlock):
        for prop, val in parent_blk.get_prop_dict().items():
            if val is self:
                return prop

        raise err.FoSpyStructureError(f"Block {self} points to a parent block {parent_blk} that does not contain it as a property.")

    elif isinstance(parent_blk, ListBlock):
        return f"[{parent_blk.get_idx(self)}]"

    raise err.FoSpyStructureError(f"Block {self} has an unknown parent block type: {type(parent_blk)}")

get_prop_dict()

Returns a dictionary mapping property names to their live object values.

Source code in FoSpy/blocks/blocks.py
def get_prop_dict(self):
    """Returns a dictionary mapping property names to their live object values."""
    serial = self.serialize(shallow=True, clean=True)
    out = {}
    for prop in serial:
        if "$" in prop:
            prop = prop.split("$")[0]

        # guard for when templateblocks add staged templates to their serial
        if hasattr(self, prop):
            out[prop] = getattr(self, prop)

    return out

get_prop_path()

Source code in FoSpy/blocks/blocks.py
def get_prop_path(self):
    from .files import FileBlock

    if not hasattr(self, "_parent_block"):
        if isinstance(self, FileBlock):
            root_path = f"<{str(self.get_file_name())}>"
        else:
            root_path = f"<Root {type(self).__name__}"
            if isinstance(self, SingleBlock):
                id_key, id_txt = self.get_id()
                if id_key is not None:
                    root_path += f" ({id_key}={id_txt})"
            root_path += ">"
        return root_path

    parent_path = self._parent_block.get_prop_path()
    parent_prop = self.get_parent_prop()

    if "[" not in parent_prop:
        return parent_path + "." + parent_prop

    return parent_path + parent_prop

get_req_validators()

Overrides class validators with any renamed properties.

Similar to class method: build_req_validators, but uses _rename_validators to align any renamed properties with their original validators.

Source code in FoSpy/blocks/blocks.py
def get_req_validators(self):
    """
    Overrides class validators with any renamed properties.

    Similar to class method:
    [`build_req_validators`][FoSpy.blocks.blocks.SingleBlock.build_req_validators],
    but uses
    [`_rename_validators`][FoSpy.blocks.blocks.SingleBlock._rename_validators]
    to align any renamed properties with their original validators.
    """
    return self._rename_validators(self.build_req_validators())

get_validators()

Overrides class validators with any renamed properties.

Similar to class method: build_validators, but uses _rename_validators to align any renamed properties with their original validators. Also adds any optional key overrides added by key$alias syntax.

Returns:

Name Type Description
vals dict

maps expected keys to validation routines.

Source code in FoSpy/blocks/blocks.py
def get_validators(self):
    """
    Overrides class validators with any renamed properties.

    Similar to class method:
    [`build_validators`][FoSpy.blocks.blocks.SingleBlock.build_validators],
    but uses
    [`_rename_validators`][FoSpy.blocks.blocks.SingleBlock._rename_validators]
    to align any renamed properties with their original validators. Also
    adds any optional key overrides added by key$alias syntax.

    Returns:
        vals (dict): maps expected keys to validation routines.
    """
    vals = self._rename_validators(self.build_validators())
    if hasattr(self, "_key_overrides"):
        for key, val in self._key_overrides.items():
            vals[key] = val
    return vals

has_staged()

Source code in FoSpy/blocks/blocks.py
def has_staged(self):
    if len(self._staged_templates) > 0:
        return True

    for val in self.get_prop_dict().values():
        if hasattr(val, "has_staged") and val.has_staged():
            return True

    return False

inject_defaults(blockDict, *args, **kwargs) classmethod

Source code in FoSpy/blocks/blocks.py
@classmethod
def inject_defaults(cls, blockDict, *args, **kwargs):
    # fallback.
    # overridden by setup_dispatch decorator
    return blockDict

inspect() classmethod

Source code in FoSpy/blocks/blocks.py
@classmethod
def inspect(self):
    # for breaking to debugger from gui
    raise Exception("put a break point here")

key_to_idx(key, idx)

Reorder attributes for serialization.

Move any attribute name to a specific index in _key_order for serialization order. The invisible "metadata" key is always refreshed to the front of the list, so indices are effectively 1-based.

Parameters:

Name Type Description Default
key str

name of attribute to reorder

required
idx int

new index in _key_order

required
Source code in FoSpy/blocks/blocks.py
def key_to_idx(self, key:str, idx:int):
    """
    Reorder attributes for serialization.

    Move any attribute name to a specific index in `_key_order` for
    serialization order. The invisible `"metadata"` key is always refreshed
    to the front of the list, so indices are effectively 1-based.

    Args:
        key: name of attribute to reorder
        idx: new index in _key_order
    """
    self._meta_to_front()
    try:
        old_idx = self._key_order.index(key)
        self._key_order.pop(old_idx)
    # TODO: Better handling
    except Exception:
        pass
    self._key_order.insert(idx, key)

keys_to_end(*args)

Reorder attributes for serialization.

Move any attribute names in *args to the end of _key_order to be serialized last. Order within *args is maintained in result.

Source code in FoSpy/blocks/blocks.py
def keys_to_end(self, *args):
    """
    Reorder attributes for serialization.

    Move any attribute names in `*args` to the end of _key_order to be
    serialized last. Order within `*args` is maintained in result.
    """
    def remove_alias(key):
        return key.split("$")[0] if "$" in key else key
    for key in self.serialize(shallow=True):
        if not key.startswith("_") and remove_alias(key) not in self._key_order:
            self._key_order.append(remove_alias(key))
    for key in args:
        try:
            idx = self._key_order.index(key)
            self._key_order.pop(idx)
        # TODO: Better handling
        except Exception:
            pass
        self._key_order.append(key)
    self._meta_to_front()

keys_to_front(*args)

Reorder attributes for serialization.

Move any attribute names in *args to the front of _key_order to be serialized first. Order within *args is maintained in result.

Source code in FoSpy/blocks/blocks.py
def keys_to_front(self,*args):
    """
    Reorder attributes for serialization.

    Move any attribute names in `*args` to the front of _key_order to be
    serialized first. Order within `*args` is maintained in result.
    """
    try:
        meta_idx = args.index("metadata")
        args.pop(meta_idx)
    # TODO: Better handling
    except Exception:
        pass

    new_order = []
    for key in args:
        new_order.append(key)
    for key in self._key_order:
        if key not in new_order:
            new_order.append(key)
    self._key_order = new_order
    self._meta_to_front()

list_avail_routines(recursive=False, prefix='', abbreviated=False)

Lists all calc routines available to be added to self._calc_routines.

Non-abbreviated calc routine strings can be passed directly to self.add_calc_routine()

Parameters:

Name Type Description Default
recursive bool

If True, recursively walks all attributes and appends results from self.attr.list_avail_routines() to result. Otherwise only identifies methods of self.

False
prefix str

Used during recursion to build relative paths

''
abbreviated bool

optionally abbreviate recursively repeated routines for similar objects into one line. This line cannot be passed to self.add_calc_routine()

False

Returns:

Name Type Description
routines list

list of strings describing _calc_routine-decorated methods. Non-abbreviated calc routine strings can be passed directly to self.add_calc_routine()

Example:

    mySyn.list_avail_routines()
    ## returns []
    mySyn.list_avail_routines(recursive=True)
    ## returns [
    ##     'reaction.add_nom_MW',
    ##     'materials.add_weight_pcts',
    ##     'materials[0].add_MW',
    ##     'materials[1].add_MW',
    ##     ... 6 total materials with the same calc_routine
    ##     'materials[5].add_MW'
    ## ]
    mySyn.list_avail_routines(recursive=True, abbreviated=True)
    ## returns [
    ##     'reaction.add_nom_MW',
    ##     'materials.add_weight_pcts',
    ##     'materials[i].add_MW; i = [0, 1, 2, 3, 4, 5]'
    ## ]

Source code in FoSpy/blocks/blocks.py
def list_avail_routines(self, recursive:bool=False, prefix:str="", abbreviated:bool=False):
    """
    Lists all calc routines available to be added to `self._calc_routines`.

    Non-abbreviated calc routine strings can be passed directly to
    `self.add_calc_routine()`

    Args:
        recursive:
            If True, recursively walks all attributes and appends results
            from `self.attr.list_avail_routines()` to result. Otherwise only
            identifies methods of `self`.

        prefix: Used during recursion to build relative paths
        abbreviated:
            optionally abbreviate recursively repeated routines for similar
            objects into one line. This line cannot be passed to
            `self.add_calc_routine()`

    Returns:
        routines (list): 
            list of strings describing _calc_routine-decorated methods.
            Non-abbreviated calc routine strings can be passed directly to
            `self.add_calc_routine()`

    Example:
    ```
        mySyn.list_avail_routines()
        ## returns []
        mySyn.list_avail_routines(recursive=True)
        ## returns [
        ##     'reaction.add_nom_MW',
        ##     'materials.add_weight_pcts',
        ##     'materials[0].add_MW',
        ##     'materials[1].add_MW',
        ##     ... 6 total materials with the same calc_routine
        ##     'materials[5].add_MW'
        ## ]
        mySyn.list_avail_routines(recursive=True, abbreviated=True)
        ## returns [
        ##     'reaction.add_nom_MW',
        ##     'materials.add_weight_pcts',
        ##     'materials[i].add_MW; i = [0, 1, 2, 3, 4, 5]'
        ## ]
    ```
    """
    routines = []

    # Local routines
    for name in dir(self):
        attr = getattr(self, name)
        if callable(attr) and getattr(attr, "_is_calc_routine", False):
            routines.append(prefix + name)

    if recursive:
        for attr, val in self.__dict__.items():
            if attr.startswith("_"):
                continue

            # Recurse into child blocks
            if hasattr(val, "list_avail_routines"):
                child_prefix = f"{prefix}{attr}."
                routines.extend(val.list_avail_routines(True, child_prefix, abbreviated))

    return routines

make_template(template_name, *args)

Converts self into a template of its original subclass.

Returns a copy of self as a template of its original subclass, with specified fields replaced with template types. See TemplateClass for more information on template generation.

Parameters:

Name Type Description Default
template_name str

All templates require an identifying name.

required
*args str

properties to clear and replace with template types.

()
Source code in FoSpy/blocks/blocks.py
def make_template(self,template_name:str,*args:str):
    """
    Converts `self` into a template of its original subclass.

    Returns a copy of `self` as a template of its original subclass, with
    specified fields replaced with template types. See
    [`TemplateClass`][FoSpy.blocks.blocks.SingleBlock.TemplateClass] for
    more information on template generation.

    Args:
        template_name: All templates require an identifying name.
        *args: properties to clear and replace with template types.
    """

    from ..parsing.format_fos import format_field

    serial = self.serialize(keepListType=True)
    validators = self.get_validators()
    for key in args:
        val = validators.get(key, None)
        if isinstance(val,type) and (issubclass(val, SingleBlock) or issubclass(val, ListBlock)):
            serial.setdefault(key, [{}])
        else:
            serial[key] = format_field("template")
    serial["template_name"] = template_name
    return self.TemplateClass(*args)(serial)

print_summary(mode='cli') classmethod

Source code in FoSpy/blocks/blocks.py
@classmethod
def print_summary(cls, mode="cli"):
    from .._docs.properties import get_summary

    print(get_summary(cls, mode=mode))

reflex(serialize=True, clean=False, **kwargs) classmethod

Source code in FoSpy/blocks/attachments.py
@classmethod
def reflex(cls, serialize=True, clean=False, **kwargs:dict):
    from .template import TemplateField
    if "file_name" not in kwargs:
        kwargs["file_name"] = TemplateField.serialize()
        kwargs.pop("path", None)
        add_embedded = "embedded" not in kwargs

    elif not any(k in kwargs for k in ("path", "embedded")):
        add_embedded = True

    if add_embedded:
        kwargs["embedded"] = TemplateField.serialize()

    return super().reflex(serialize=serialize, clean=clean, **kwargs)

refresh(new_copy='prompt', overwrite='prompt', **kwargs)

Source code in FoSpy/blocks/attachments.py
def refresh(self, new_copy="prompt",overwrite="prompt", **kwargs):
    from .. import cfg
    try:
        if self._filepath is None:
            self._get_filepath(rf_new_copy=new_copy, rf_overwrite=overwrite, **kwargs)
            return
        if not self.exists():
            raise ValueError(f"Cannot find file at last known location of attachment: {self._filepath}")

        new_path = self._get_abspath()
        if self._filepath != new_path:
            printmsg = f"Attachment file path has changed: {self._filepath} -> {new_path}"
            prompted=False
            if new_copy == "prompt":
                print(printmsg)
                prompted=True

            if new_copy == "prompt":
                new_copy = input("You can copy the file to the new location, "
                                "or modify the path value to match the old location. "
                                "Copy? (y/n): ").lower() == "y"

            if new_copy:
                if new_path.is_file() and overwrite == "prompt":
                    if not prompted:
                        print(printmsg)
                    overwrite = input("File already exists at new location. "
                                    "Overwrite? (y/n): ").lower() == "y"
                if overwrite or not new_path.is_file():
                    import shutil
                    shutil.copyfile(self._filepath, new_path)
                    self._filepath = new_path

            else:
                new_path = self._filepath.parent.resolve().relative_to(self._get_filedir(),walk_up=True)
                self.path = str(new_path)

        checkpath = self._get_abspath()
        check = checkpath.is_file()
        if not check:  
            raise ValueError(f"Could not successfully refresh attachment to location: {checkpath}.")

        self._filepath = checkpath

    except Exception as e:
        if not cfg.track_attachments.ignore:
            raise e
        else:
            _debug.msg(f"Could not refresh attachment: {e}. Configured to ignore.")

refresh_attachments(new_copy=None, overwrite=None, **kwargs)

Source code in FoSpy/blocks/blocks.py
def refresh_attachments(self, new_copy=None, overwrite=None, **kwargs):
    from .attachments import Attachment

    if new_copy is None:
        new_copy = self._att_new_copy
    if overwrite is None:
        overwrite = self._att_overwrite

    for propDict in self.__dict__, self.ext.__dict__:
        for key, val in propDict.items():
            if key.startswith("_") or key in self._reserved:
                continue
            if hasattr(val, "refresh_attachments"):
                val.refresh_attachments(new_copy=new_copy, overwrite=overwrite, **kwargs)
            elif isinstance(val, Attachment) and hasattr(val, "refresh"):
                val.refresh(new_copy=new_copy, overwrite=overwrite, **kwargs)

register_dispatch(registry_val, **kwargs) classmethod

Source code in FoSpy/blocks/attachments.py
@classmethod
def register_dispatch(cls, registry_val, **kwargs):
    extension = registry_val or ".txt"
    fn = "attachment"+extension
    return super().register_dispatch(registry_val, setup_from_key="_location",
                                     setup_allow_self=False, defaults={"file_name":fn},
                                     inherit_dispatch=True,
                                     **kwargs)

rename_block(old, new)

Source code in FoSpy/blocks/blocks.py
def rename_block(self, old, new):
    validators = self.get_validators()
    req = self.get_req_validators()
    if any(name.startswith("_") for name in (old, new)):
        raise ValueError("You cannot set private attributes (starting with '_') using obj.rename_block()")

    if old in req and new in validators:
        raise ValueError(f"You cannot rename '{old}' to '{new}'. '{old}' is a required property that "
                            f"can only be renamed to an unregistered key; '{new}' is already registered "
                            "as an expected property.")

    if hasattr(self, new):
        raise ValueError(f"'{new}' is already a property for this object, you cannot overwrite it with "
                         "obj.rename_block()")

    if "rename" in (old, new):
        raise ValueError("obj.rename property cannot be set or changed by obj.rename_block()")

    if hasattr(self, "rename") and hasattr(self.rename, old):
        old = getattr(self.rename, old)()

    if old in self._key_overrides:
        val = self._key_overrides.pop(old)
        self._key_overrides[new] = val

    else:
        if not hasattr(self,"rename"):
            self.rename = {}

        rename_dict = self.rename_dict()
        rename_from = {v:k for k,v in rename_dict.items()}
        if old in rename_from:
            base = rename_from[old]
        else:
            base = old

        _debug.msg(f"Registering '{base}':'{new}' into rename block")
        setattr(self.rename, base, new)
    _debug.msg(f"Moving '{old}' over to '{new}'.")
    setattr(self,new,getattr(self, old))
    delattr(self,old)

    try:
        idx = self._key_order.index(old)
        self._key_order[idx] = new
    # TODO: Better handling
    except Exception:
        self._key_order.append(new)

rename_dict()

Source code in FoSpy/blocks/blocks.py
def rename_dict(self):
    if not hasattr(self, "rename"):
        return {}
    return self.rename.serialize(shallow=True, clean=True)

serialize(keepListType=False, shallow=False, clean=False, **kwargs)

Return a recursively serialized dict representation of self.

Fully serialized SingleBlocks are a single dict that can be passed to another constructor or emitted into lines for a FOS file. Serialized values at any nest level are either dicts, lists, or strings to allow full type-coersion when reconstructing or simplified emission when writing files.

Serialized dict is deep copied to prevent object mutation.

Parameters:

Name Type Description Default
keepListType bool

When True, maintains its current FOS printing mode (looped keys or explicit key:value lines), instead of explicit default

False
shallow bool

When True, no recursive serialization occurs. Recommended when serialization is used only to inspect top-level keys.

False
clean bool

When True, no FOS format read/write metadata is included in the serial. Recommended for sending output to other formats like JSON.

False

Private attributes starting with "_" are either skipped or unpacked in special cases:

  • _key_order: attributes are added to the serialized dict in the order they appear in this list.

  • _calc_comments: calculated comments are attached to their mapped attribute after serialization to avoid mutation of object comments

  • _calc_routines: A list of functions scheduled to be called right before serialization to update _calc_comments. Scheduling calc routines ensures that their calculated values are up-to-date.

  • _meta: attributes of this container are given their own private _keys mapped by FoSpy.parsing.syntax.meta_keys in the serialized dict.

  • _key_overrides: per-instance override mapping that tracks which unexpected attributes require $alias suffixes.

  • _aliases: maps attribute names to alias tags used to emit $alias suffixed keys.

  • _reserved: attribute names in reserved are non-private attributes which should not be serialized. This usually applies to the ext attribute or methods attached after construction.

Source code in FoSpy/blocks/blocks.py
def serialize(self, keepListType:bool=False, shallow:bool=False, clean:bool=False, **kwargs):
    """
    Return a recursively serialized `dict` representation of `self`.

    Fully serialized `SingleBlock`s are a single dict that can be passed to
    another constructor or emitted into lines for a FOS file. Serialized
    values at any nest level are either dicts, lists, or strings to allow
    full type-coersion when reconstructing or simplified emission when
    writing files.

    Serialized dict is deep copied to prevent object mutation.

    Args:
        keepListType:
            When True, maintains its current FOS printing mode (looped keys
            or explicit key:value lines), instead of explicit default

        shallow:
            When True, no recursive serialization occurs. Recommended when
            serialization is used only to inspect top-level keys.

        clean:
            When True, no FOS format read/write metadata is included in the
            serial. Recommended for sending output to other formats like
            JSON.

    Private attributes starting with "_" are either skipped or unpacked in
    special cases:

    * `_key_order`:
        attributes are added to the serialized dict in the order they
        appear in this list.

    * `_calc_comments`:
        calculated comments are attached to their mapped attribute after
        serialization to avoid mutation of object comments

    * `_calc_routines`:
        A list of functions scheduled to be called right before
        serialization to update _calc_comments. Scheduling calc routines
        ensures that their calculated values are up-to-date.

    * `_meta`:
        attributes of this container are given their own private `_key`s
        mapped by `FoSpy.parsing.syntax.meta_keys` in the serialized
        dict.

    * `_key_overrides`:
        per-instance override mapping that tracks which unexpected
        attributes require $alias suffixes.

    * `_aliases`:
        maps attribute names to alias tags used to emit $alias suffixed
        keys.

    * `_reserved`:
        attribute names in reserved are non-private attributes which
        should *not* be serialized. This usually applies to the `ext`
        attribute or methods attached after construction.
    """
    from copy import deepcopy
    from ..parsing.format_fos import format_calc_comment
    from .template import TemplateBlock

    val_to_alias = {v:k for k,v in self._aliases.items()}

    all_attrs = {}
    out = {}

    for routine in self._calc_routines:
        routine()

    def add_alias(key):
        if key in self._key_overrides:
            alias = val_to_alias[self._key_overrides[key]]
            return f"{key}${alias}"
        return key


    def try_serial(obj):
        if isinstance(obj, SimpleWrapper):
            obj = obj()
        serialize = getattr(obj, "serialize", None)
        if callable(serialize) and not shallow:
            return obj.serialize(clean=clean)
        if isinstance(obj, list):
            return [try_serial(item) for item in obj]
        if isinstance(obj, dict):
            return {k:try_serial(v) for k,v in obj.items()}
        return str(obj)

    for attr,val in self.__dict__.items():
        if attr == "ext" and val is not None:
            for ext_attr, ext_val in val.__dict__.items():
                all_attrs[ext_attr] = ext_val
        elif not (attr.startswith("_") or attr in self._reserved):
            all_attrs[attr] = val


    for key in self._key_order:
        if key in all_attrs:
            val = all_attrs.pop(key)
            out[add_alias(key)] = try_serial(val)

    for key, val in all_attrs.items():
        out[add_alias(key)] = try_serial(val)

    for attr, key in mk.items():
        try:
            k = md[key].copy()
        except AttributeError:
            k = md[key]
        val = getattr(self._meta,attr,k)
        out[key] = val

    comments = {}
    for key, comment_list in out[mk["comments"]].items():
        comments[add_alias(key)] = comment_list
    out[mk["comments"]] = comments

    out = deepcopy(out)

    # _debug.pmsg(self._calc_comments)
    for key, comments in self._calc_comments.items():
        for comment in comments.values():
            out[mk["comments"]].setdefault(add_alias(key),[])
            out[mk["comments"]][add_alias(key)].append(format_calc_comment(comment))

    if not keepListType:
        out[mk["list_type"]] = "explicit"

    if "template_name" in out and not isinstance(self, TemplateBlock):
        out.pop("template_name")

    if clean:
        scan = out.copy()
        for key, val in scan.items():
            if key.startswith("_") or val is None:
                out.pop(key)

    if not any(k for k in out.get("rename", {}) if not k.startswith("_")):
        out.pop("rename", None)

    return out

set_dispatch(value=None, from_parent=None, from_key=None, allow_self=None) classmethod

Source code in FoSpy/blocks/blocks.py
@classmethod
def set_dispatch(cls, value=None, from_parent=None, from_key=None, allow_self=None):

    # Abstract classes are sometimes made without SingleBlock in MRO
    if from_parent is not None:
        target_cls = from_parent
    else:
        target_cls = cls

    if "dispatch" not in target_cls.__dict__:
        target_cls.dispatch = {}

    if from_key is not None:
        target_cls.dispatch_key = from_key

    if allow_self is not None:
        target_cls.dispatch_allow_self = allow_self

    def dispatched_cls(subcls, v=value, _cls=target_cls):
        subcls.dispatch_from = _cls
        _cls.dispatch[v] = subcls
        return subcls
    return dispatched_cls

setup_dispatch(from_key=None, allow_self=True, _dispatch_from=None, _defaults={}) staticmethod

Decorate a class to dispatch to other classes during construction.

The decorated class's add_dispatch method will be wrapped into a new method, dispatch_subclass, which is decorator as a dispatcher. The parent class's from_key is found in the blockDict passed to the constructor, and the value mapped to from_key is mapped to dispatchable subclasses in the registry.

add_dispatch returns a dictionary of values that are injected into the blockDict, either to be detected by dispatch, or to be delegated to the constructor.

This decorator should only be used directly for the start of a dispatch chain. For later dispatches, use register_dispatch

Parameters:

Name Type Description Default
cls SingleBlock subclass

The class to be decorated. If provided, the decorator is most likely being called as a bare decorator. Otherwise, the decorators is being called with other keyword arguments and returns the modified decorator.

None
from_key str

The key to be located in the blockDict after optional injection by add_dispatch. Private from_keys will be injected and located under the __dispatch__ key which is popped before final construction.

None
allow_self bool

When True, the decorated class will dispatch to itself if no subclasses can be found. When False, error is raised during construction if dispatchable subclass is not found.

True
_dispatch_from SingleBlock subclass

To be passed only by register_dispatch, which decorates subclasses to populate this class's registry. Identifies the parent class that the constructor must start at. If not provided, the decorated class is assumed to be the start of a dispatch chain.

None
_defaults dict

To be passed only by register_dispatch, which decorates subclasses to populat this class's registry. Provides default values that should be injected into the blockDict when trying to guarantee dispatching to the decorated class (usually by a template constructor).

{}
Source code in FoSpy/blocks/blocks.py
@staticmethod
def setup_dispatch(cls:type[BlockType]=None,
    from_key=None,
    allow_self=True,
    _dispatch_from=None,
    _defaults={}
):
    """
    Decorate a class to dispatch to other classes during construction.

    The decorated class's [`add_dispatch`
    method][FoSpy.blocks.blocks.SingleBlock.add_dispatch] will be wrapped
    into a new method,
    [`dispatch_subclass`][FoSpy.blocks.blocks.SingleBlock.dispatch_subclass],
    which is decorator as a
    [`dispatcher`][FoSpy.blocks.blocks.SingleBlock.dispatcher]. The parent
    class's `from_key` is found in the blockDict passed to the constructor,
    and the value mapped to `from_key` is mapped to dispatchable subclasses
    in the `registry`.

    `add_dispatch` returns a dictionary of values that are injected into the
    blockDict, either to be detected by dispatch, or to be delegated to the
    constructor.

    This decorator should only be used directly for the start of a dispatch
    chain. For later dispatches, use
    [`register_dispatch`][FoSpy.blocks.blocks.SingleBlock.register_dispatch]

    Args:
        cls (SingleBlock subclass):
            The class to be decorated. If provided, the decorator is most
            likely being called as a bare decorator. Otherwise, the
            decorators is being called with other keyword arguments and
            returns the modified decorator.

        from_key (str):
            The key to be located in the blockDict after optional injection
            by `add_dispatch`. Private `from_key`s will be injected and
            located under the `__dispatch__` key which is popped before
            final construction.

        allow_self (bool):
            When True, the decorated class will dispatch to itself if no
            subclasses can be found. When False, error is raised during
            construction if dispatchable subclass is not found.

        _dispatch_from (SingleBlock subclass):
            To be passed only by `register_dispatch`, which decorates
            subclasses to populate this class's registry. Identifies the
            parent class that the constructor must start at. If not
            provided, the decorated class is assumed to be the start of a
            dispatch chain. 
        _defaults (dict):
            To be passed only by `register_dispatch`, which decorates
            subclasses to populat this class's registry. Provides default
            values that should be injected into the blockDict when trying to
            guarantee dispatching to the decorated class (usually by a
            template constructor).
        """
    if cls is not None and not isinstance(cls, type):
        raise Exception("@setup_dispatch must be used as a bare decorator, or with "
                        "a class as the first positional argument. You may have tried "
                        "to decorate a class with positional args instead of keywords.")

    def decorator(_cls:type[BlockType], _fk=from_key, _as=allow_self, _df=_dispatch_from, _def=_defaults):
        _cls.__dispatch__ = {
            "from_key": _fk,
            "allow_self": _as,
            "dispatch_from": _df or _cls,
            "registry": {}
        }

        def inject(bD, k, v, is_default=False):
            target_dict = bD["__dispatch__"] if k.startswith("_") else bD

            if is_default and v is None and k in target_dict:
                return bD

            target_dict[k] = v

            return bD

        @classmethod
        def inject_defaults(current_cls, blockDict, _d=_def):
            d = current_cls.__dispatch__
            blk_d = blockDict.setdefault("__dispatch__", {})
            if (not d['allow_self'] and
                None not in d['registry'] and
                blockDict.get(d['from_key'],blk_d.get(d['from_key'], None)) is None):
                default_dispatch = next(iter(d['registry'].values()))
                blockDict = default_dispatch.inject_defaults(blockDict)

            for k, v in _d.items():
                blockDict = inject(blockDict, k, v, is_default=True)
            return blockDict

        @SingleBlock.dispatcher
        def dispatch_subclass(current_cls:type[BlockType], blockDict:dict, _dispatch_key=_fk, **kwargs):
            injection = current_cls.add_dispatch(blockDict, _dispatch_key, _wrapped=True, **kwargs)

            for k, v in injection.items():
                blockDict = inject(blockDict, k, v)

            return blockDict

        # inject methods
        _cls.inject_defaults = inject_defaults
        _cls.dispatch_subclass = dispatch_subclass

        return _cls

    if cls is not None:
        return decorator(cls)

    return decorator

stage_template(prop_name, template=None)

Source code in FoSpy/blocks/blocks.py
def stage_template(self, prop_name, template:Block|dict=None):
    from .template import TemplateBlock
    if template is None:
        template = {}

    if not isinstance(template, (TemplateBlock, dict)):
        raise ValueError("Template must be a TemplateBlock or dictionary. To 'stage' a ListBlock, "
                         "you can stage a SingleBlock template with a ListBlock alias. This creates "
                         "a non-template ListBlock with the template staged as its first entry.")

    alias_validator = None
    if "$" in prop_name:
        prop_name, alias = prop_name.split("$",1)
        try:
            alias_validator = self._aliases[alias]
        except KeyError as e:
            raise err.PropertyAliasError(prop_name, self, blockDict={prop_name: "<staged template>"},
                                         hint=f"Unrecognized block alias: '{alias}' assigned to property: ",
                                         posthint=f"Valid aliases: {list(self._aliases.keys())}") from e

    if hasattr(self, prop_name):
        raise ValueError(f"Property {prop_name} already exists. You cannot stage a template for a property that already exists.")

    validators = self.build_validators()
    validator = validators.get(prop_name, None)

    if validator is not None:
        alias = None

    if alias_validator is not validator and None not in (validator, alias_validator):
        raise ValueError(f"Property {prop_name} already has a validator. You cannot alias a different validator for the same property.")

    validator = next(v for v in (validator, alias_validator) if v is not None)

    if validator is None:
        try:
            if not isinstance(template, TemplateBlock):
                raise TypeError("Dictionary templates must be staged with an alias.")

            alias = next(k for k, v in self._aliases.items() if isinstance(template, v))
            validator = self._aliases[alias]
        except (TypeError,StopIteration) as e:
            raise ValueError(f"Property {prop_name} is unexpected. In order to stage a template "
                            "for and unexpected property, you must specify the validator with a '$' alias "
                            "in the property name, or stage a pre-constructed template of an aliasable validator."
                            ) from e

    if isinstance(validator, type) and issubclass(validator, ListBlock):
        # let setattr handle ListBlock construction using alias
        # this creates an empty ListBlock under self.prop_name
        # (alias stripped during setattr)
        setattr(self, prop_name+"$"+alias if alias is not None else prop_name, [])
        empty_lb = getattr(self, prop_name)
        return empty_lb.stage_template("entry0", template)

    if isinstance(template, dict):
        # reflex returns a TemplateBlock subclassed from the validator
        template = validator.reflex(serialize=False, include_temp_names=True, clean=False, **template)
        template.template_name = prop_name

    elif not isinstance(template, validator):
        val_nm = validator.__name__
        if alias is None:
            error_msg = f"The provided template is not compatible with the validator expected for property '{prop_name}' ({val_nm})."
        else:
            error_msg = f"The provided template is not compatible with the validator specified by alias '{alias}' ({val_nm})."
        raise ValueError(error_msg)


    template._staged_parent = self

    if alias is not None:
        self._key_overrides[prop_name] = validator

    self._staged_templates[prop_name] = template

    return prop_name, template

to_json(filepath=None, clean=True, indent=4, **kwargs)

Converts self into a JSON-formatted string or file.

Serializes and either returns as a JSON-formatted string or saves to a JSON file.

Parameters:

Name Type Description Default
filepath pathlike

JSON file save destination. If None, returns JSON-formatted string instead.

None
clean bool

When True, no FOS format read/write metadata is included in the serial. FOS metadata has no impact on JSON format but may be useful to view in JSON for troubleshooting.

True
indent int

indent value passed to json.dump for file saving.

4
**kwargs any

other arguments passed to json.dump for file saving.

{}
Source code in FoSpy/blocks/blocks.py
def to_json(self, filepath=None, clean:bool=True, indent:int=4, **kwargs):
    """
    Converts `self` into a JSON-formatted string or file.

    [Serializes][FoSpy.blocks.blocks.SingleBlock.serialize] and either
    returns as a JSON-formatted string or saves to a JSON file.

    Args:
        filepath (pathlike):
            JSON file save destination. If `None`, returns JSON-formatted
            string instead.

        clean:
            When True, no FOS format read/write metadata is included in the
            serial. FOS metadata has no impact on JSON format but may be
            useful to view in JSON for troubleshooting.

        indent:
            `indent` value passed to `json.dump` for file saving.

        **kwargs (any):
            other arguments passed to `json.dump` for file saving.
    """
    import json
    serial = self.serialize(clean=clean)

    if filepath is None:
        return json.dumps(serial)

    with open(filepath, "w") as f:
        json.dump(serial, f, indent=indent, **kwargs)

track_attachments(new_copy='prompt', overwrite='prompt', **kwargs)

Source code in FoSpy/blocks/blocks.py
def track_attachments(self, new_copy="prompt",overwrite="prompt", **kwargs):
    self._att_new_copy = new_copy
    self._att_overwrite = overwrite