Skip to content

materials

Full documentation pages are generated for docstring reference only and may contain symbols imported from other modules. Imported symbols are not distinguished from locally defined symbols and will appear in any module that they are imported into. For better information on where symbols should be imported from, review the sourcecode on the github.

FoSpy.blocks.materials

_debug module-attribute

_debug = Debug()

Chemical

Bases: SingleBlock

An abbreviated reference to a material, product, or chemical composition.

This class is the bare minimum for identifying a chemical composition. Additional requirements are enforced for various subclasses, including Material and Product.

Source code in FoSpy/blocks/chemicals.py
class Chemical(SingleBlock):
    """
    An abbreviated reference to a material, product, or chemical composition.

    This class is the bare minimum for identifying a chemical composition.
    Additional requirements are enforced for various subclasses, including
    `Material` and `Product`.
    """
    _id_key = "formula"

    @_calc_routine
    def add_MW(self):
        """
        Attach a comment to the formula with molecular weight.
        """
        _debug.msg(f"Adding molecular weight to chemical: {self.formula}")
        mw = self.formula.formula_weight
        self.add_calc_comment("formula",f"Molecular Weight: {mw:.2f} g/mol", "add_MW")

_id_key class-attribute instance-attribute

_id_key = 'formula'

add_MW

add_MW()

Attach a comment to the formula with molecular weight.

Source code in FoSpy/blocks/chemicals.py
@_calc_routine
def add_MW(self):
    """
    Attach a comment to the formula with molecular weight.
    """
    _debug.msg(f"Adding molecular weight to chemical: {self.formula}")
    mw = self.formula.formula_weight
    self.add_calc_comment("formula",f"Molecular Weight: {mw:.2f} g/mol", "add_MW")

Debug

Source code in FoSpy/_debug.py
class Debug:
    def __init__(self):
        self.on = False

        frame = inspect.currentframe().f_back
        self.module_name = frame.f_globals.get("__name__", "<unknown>")
        self.label = f"|(Debug message from {self.module_name})"
        self.label_width = len(self.label)

    def _get_text_width(self, module=None):
        if module:
            label = f"|(Debug message from {module} via {self.module_name})"
            label_width = len(label)
        else:
            label = self.label
            label_width = self.label_width

        text_width = DEBUG_WIDTH - label_width
        return text_width, label, label_width


    def msg(self,msg, module=None):
        if not self.on:
            return

        text_width, label, label_width = self._get_text_width(module)

        wrapped = textwrap.fill(str(msg), width=text_width)

        for line in wrapped.splitlines():
            print(f'{line:<{text_width}}{label:>{label_width}}')

    def pmsg(self,msg,module=None,**kwargs):
        if not self.on:
            return

        text_width, label, label_width = self._get_text_width(module)

        buf = io.StringIO()
        pprint(msg,stream=buf, width=text_width,**kwargs)
        txt = buf.getvalue()
        for line in txt.splitlines():
            print(f'{line:<{text_width}}{label:>{label_width}}')

label instance-attribute

label = f'|(Debug message from {self.module_name})'

label_width instance-attribute

label_width = len(self.label)

module_name instance-attribute

module_name = frame.f_globals.get('__name__', '<unknown>')

on instance-attribute

on = False

__init__

__init__()
Source code in FoSpy/_debug.py
def __init__(self):
    self.on = False

    frame = inspect.currentframe().f_back
    self.module_name = frame.f_globals.get("__name__", "<unknown>")
    self.label = f"|(Debug message from {self.module_name})"
    self.label_width = len(self.label)

_get_text_width

_get_text_width(module=None)
Source code in FoSpy/_debug.py
def _get_text_width(self, module=None):
    if module:
        label = f"|(Debug message from {module} via {self.module_name})"
        label_width = len(label)
    else:
        label = self.label
        label_width = self.label_width

    text_width = DEBUG_WIDTH - label_width
    return text_width, label, label_width

msg

msg(msg, module=None)
Source code in FoSpy/_debug.py
def msg(self,msg, module=None):
    if not self.on:
        return

    text_width, label, label_width = self._get_text_width(module)

    wrapped = textwrap.fill(str(msg), width=text_width)

    for line in wrapped.splitlines():
        print(f'{line:<{text_width}}{label:>{label_width}}')

pmsg

pmsg(msg, module=None, **kwargs)
Source code in FoSpy/_debug.py
def pmsg(self,msg,module=None,**kwargs):
    if not self.on:
        return

    text_width, label, label_width = self._get_text_width(module)

    buf = io.StringIO()
    pprint(msg,stream=buf, width=text_width,**kwargs)
    txt = buf.getvalue()
    for line in txt.splitlines():
        print(f'{line:<{text_width}}{label:>{label_width}}')

ListBlock

Bases: Block

Represents multiple similar blocks of key:value pairs parsed from a FOS File

ListBlocks are used to group multiple SingleBlocks of the same subclass together and define methods that modify or access information from multiple SingleBlocks at once. SingleBlocks contained within a ListBlock can be indexed and iterated over directly instead of calling ListBlock._objs

Attributes:

Name Type Description
_objs

List containing the stored SingleBlock objects.

_reqCls type[SingleBlock]

Specifies which SingleBlock subclass the objects in _objs must belong to.

Notable Subclasses:

MaterialList(ListBlock) # Contains Material(SingleBlock) objects
TreamentList(ListBlock) # Contains Treatment(SingleBlock) objects

Source code in FoSpy/blocks/blocks.py
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
@SingleBlock.setup_dispatch(from_key="_reqCls", allow_self=False)
class ListBlock(Block):
    """
    Represents multiple similar blocks of key:value pairs parsed from a FOS File

    `ListBlock`s are used to group multiple `SingleBlock`s of the same subclass
    together and define methods that modify or access information from multiple
    `SingleBlock`s at once. `SingleBlocks` contained within a `ListBlock` can be
    indexed and iterated over directly instead of calling `ListBlock._objs`

    Attributes:
        _objs: List containing the stored `SingleBlock` objects.
        _reqCls:
            Specifies which `SingleBlock` subclass the objects in `_objs` must belong to.

    Notable Subclasses:
    ```python
    MaterialList(ListBlock) # Contains Material(SingleBlock) objects
    TreamentList(ListBlock) # Contains Treatment(SingleBlock) objects
    ```
    """
    _reqCls: type[SingleBlock] = None
    simple_lists = {}
    def __init__(self, blockList:list):
        """
        Constructs a `ListBlock` from a list of objects or serialized dictionaries.

        Each item in blockList is checked against the `SingleBlock` subclass
        specified for the `ListBlock` subclass. If the item is not the correct
        subclass, it is passed to the `SingleBlock` subclass's
        `dispatch_subclass` method for coersion.

        Args:
            blockList:
                A list containing either `dicts` or `SingleBlock` objects (Mixing
                is allowed).
        Raises:
            TypeError:
                `ListBlock` instances can only be constructed from subclasses with an assigned _reqCls, not the parent `ListBlock` class.
        """
        #self._objs = []
        if not (isinstance(self._reqCls, type) and issubclass(self._reqCls, SingleBlock)):
            raise TypeError(f"ListBlock instances can only be constructed from subclasses with an assigned _reqCls. {self.__class__} has no _reqCls.")
        self.track_attachments(**cfg.track_attachments())
        self._temp_id_gen = self.temp_id_gen()
        if not isinstance(blockList, list):
            blockList = [blockList]
        self._objs = blockList
        self._staged_templates = {}

    @classmethod
    def add_dispatch(cls, blockDict, dispatch_key, **kwargs):
        _ = SingleBlock.add_dispatch(blockDict, dispatch_key, **kwargs)

        from .template import TemplateList
        from .._docs.properties import _validator_rules
        from ._blockUtils import _get_docs_link

        block_dispatch = blockDict.setdefault("__dispatch__", {})
        reqCls = block_dispatch.setdefault("_reqCls", cls._reqCls)

        if reqCls is None:
            return {}

        if getattr(reqCls, "_full_class", None) is not None:
            return TemplateList.Simple(reqCls)

        registry = ListBlock.__dispatch__['registry']

        if reqCls not in registry:
            link = _get_docs_link(reqCls)

            @_validator_rules(
                f"A [simple `ListBlock`](#listblock-and-simple-lists) of [`{reqCls.__name__}` objects.]{link}"
            )
            @SingleBlock.register_dispatch(reqCls, from_parent=ListBlock)
            class SimpleList(ListBlock):
                _reqCls = reqCls

            SimpleList.__name__ = f"{reqCls.__name__}SimpleList"
            SimpleList.__qualname__ = f"ListBlock.Simple.{reqCls.__name__}List"
            SimpleList.__module__ = reqCls.__module__

        return {dispatch_key: reqCls}


    @classmethod
    def Simple(cls, reqCls=SingleBlock):
        """
        Creates a simple subclass of `ListBlock`

        Creates a subclass of `ListBlock` that only accepts objects of the
        specified `SingleBlock` subclass.

        Simple ListBlocks are used when no specialized methods or attributes are
        needed.

        Args:
            reqCls:
                The subclass of `SingleBlock` that this `ListBlock` subclass
                accepts.
        """
        if not issubclass(reqCls, SingleBlock):
            raise TypeError("reqCls must be a subclass of SingleBlock")

        proxy_dict = {
            "__dispatch__": {
                "_reqCls": reqCls,
            }
        }

        return ListBlock.dispatch_subclass(proxy_dict)

    def TemplateClass(cls):
        from .template import TemplateList
        return TemplateList.simple(cls._reqCls)


    def __setattr__(self, name, value):
        """
        Only private attributes starting with "_" can be set.

        Items in self._objs can be edited/replaced individually by indexing with
        self[i], or self._objs can be replaced with a new list, which is
        re-validated and coerced to the correct `SingleBlock` subclass specified
        by _reqCls

        Args:
            name:
                The name of the attribute to set.
            value:
                The value to set the attribute to.
        Raises:
            AttributeError:
                Only private attributes starting with "_" can be set.
            TypeError:
                self._objs must be a list of objects which can be coerced to the
                correct `SingleBlock` subclass specified by _reqCls
        """
        from .attachments import Attachment
        from ._blockUtils import _unwrap_listblock
        from .template import TemplateBlock, TemplateList

        if name == "_objs":


            if hasattr(self, "_reqCls"):
                errors = []
                typ = self._reqCls
                if getattr(typ, "_full_class", None) is not None:
                    check_typ = typ._full_class
                else:
                    check_typ = typ
                value = _unwrap_listblock(value, typ=check_typ)
                new_list = []
                for idx, obj in enumerate(value):
                    if isinstance(obj, dict) and obj == {}:
                        continue
                    if not isinstance(obj, check_typ):
                        try:
                            new_obj = typ(obj)
                        except Exception as e:
                            errors.append(err.ListBlockMismatchError(self, obj, idx, cause=e))
                            continue  
                        if isinstance(obj, Attachment) and hasattr(obj, "_filepath"):
                            new_obj._filepath = obj._filepath
                        obj=new_obj
                    elif isinstance(obj, TemplateBlock) and not isinstance(self, TemplateList):
                        try:
                            obj = obj.fill()
                            if isinstance(obj, TemplateBlock):
                                raise err.FoSpyStructureError("Could not fill a template into a complete block before adding to this ListBlock")
                        except Exception as e:
                            errors.append(err.ListBlockMismatchError(self, obj, idx, cause=e))
                    obj._parent_block = self
                    if hasattr(obj, "refresh") and isinstance(obj, Attachment):
                        obj.refresh(new_copy=self._att_new_copy, overwrite=self._att_overwrite)
                    new_list.append(obj)
                if errors:
                    raise err.ListBlockErrorGroup(self, errors)
                return super().__setattr__(name, new_list)

        elif name.startswith("_") or name in self._reserved:
            return super().__setattr__(name,value)
        else:
            raise AttributeError(
                f"{type(self).__name__} does not allow setting attribute '{name}'. "
                f"Only private names starting with '_' can be used. "
                f"Each list item is an item in {type(self).__name__}._objs which can be edited individually, "
                f"Or you can replace {type(self).__name__}._objs with a new list of objects."
            )

    def has_staged(self):
        return len(self._staged_templates) > 0 or any(blk.has_staged() for blk in self)

    def temp_id_gen(self):
        i = 0
        while True:
            yield "template_" + str(i)
            i += 1

    def stage_template(self, temp_id=None, template:Block|dict=None):
        from .template import TemplateBlock
        if template is None:
            template = {}

        if isinstance(template, TemplateBlock):
            temp_id = temp_id or template.template_name
            if temp_id in self._staged_templates:
                temp_id += f" ({next(self._temp_id_gen)})"
        elif isinstance(template, dict):
            temp_id = temp_id or template.get("template_name", next(self._temp_id_gen))
        else:
            raise ValueError("Template must be a TemplateBlock or dictionary.")

        if isinstance(template, dict):
            template = self._reqCls.reflex(serialize=False,**template)
            template.template_name = temp_id
        elif not isinstance(template, self._reqCls):
            raise ValueError("Template must be a TemplateBlock subclass of the same type as this ListBlock.")

        if temp_id in self._staged_templates:
            raise ValueError(f"A Template has already been staged for {temp_id}.")

        template._staged_parent = self
        self._staged_templates[temp_id] = template

        return temp_id, template

    def fill_staged_template(self, temp_id, idx=None, **kwargs):
        from .template import TemplateBlock

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

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

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

        if idx is None:
            self.append(filled)
        else:
            self.insert(idx, filled)

        return temp_id, filled

    def block_to_idx(self, blk, idx):
        objs = self._objs.copy()
        current_idx = None
        if blk in objs:
            current_idx = objs.index(blk)
            if idx <= current_idx:
                current_idx += 1
        objs.insert(idx, blk)
        objs.pop(current_idx)
        self._objs = objs

    def order_up(self, blk):
        idx = self.get_idx(blk)
        if idx == 0:
            return
        self.block_to_idx(blk, idx-1)

    def order_down(self, blk):
        idx = self.get_idx(blk)
        if idx == len(self)-1:
            return
        self.block_to_idx(blk, idx+2)

    def get_idx(self, blk):
        try:
            return self._objs.index(blk)
        except ValueError:
            raise err.FoSpyStructureError(f"Block {blk} is not in {self}")

    def append(self, obj:SingleBlock):
        """
        Append a `SingleBlock`-coercable object to this `ListBlock`

        Appends the object to this `ListBlock`'s `_objs` list, and passes the
        entire list back to
        [`__setattr__`][FoSpy.blocks.blocks.ListBlock.__setattr__] for
        validation.

        Args:
            obj:
                The object to append
        """
        objs = self._objs.copy()
        objs.append(obj)
        self._objs = objs

    def insert(self, idx, obj:SingleBlock):
        """
        Insert a `SingleBlock`-coercable object into this `ListBlock`.

        Inserts the object into this `ListBlock`'s `_objs` list, and passes the
        entire list back to
        [`__setattr__`][FoSpy.blocks.blocks.ListBlock.__setattr__] for
        validation.

        Args:
            idx:
                The index to insert the object at
            obj:
                The object to insert
        """
        objs = self._objs.copy()
        objs.insert(idx,obj)
        self._objs = objs

    def remove_idx(self, from_idx:int=None, to_idx:int=None):
        """
        Remove a range of items from this `ListBlock`

        Removes a range of items from this `ListBlock`'s `_objs` list, and
        passes the entire list back to
        [`__setattr__`][FoSpy.blocks.blocks.ListBlock.__setattr__] for
        validation.

        `from_idx` is inclusive, and `to_idx` is non-inclusive. i.e., if
        `from_idx` is 0 and `to_idx` is 1, then the first item in the list will
        be removed, but not the second.

        If `from_idx` is None, then all items starting at and including `to_idx`
        will be removed. If `to_idx` is None, then all items up to and **not**
        including `from_idx` will be removed.

        Args:
            from_idx:
                The index of the first item to remove
            to_idx:
                The non-inclusive index to stop removing
        """
        if from_idx is None and to_idx is None:
            self._objs = []

        objs = self._objs.copy()

        if from_idx is None:
            objs = objs[to_idx:]
        elif to_idx is None:
            objs = objs[:from_idx]
        else:
            objs = objs[:from_idx] + objs[to_idx:]

        self._objs = objs   

    def __getitem__(self, idx:int):
        """
        Get an item from this `ListBlock` by index.

        Args:
            idx:
                The index of the item
        """
        return self._objs[idx]

    def __setitem__(self, idx, val):
        """
        Set an item to this `ListBlock` by index.

        After setting, all items in this `ListBlock`'s `_objs` list are passed
        back to [`__setattr__`][FoSpy.blocks.blocks.ListBlock.__setattr__] for
        validation.

        Args:
            idx:
                The index of the item
            val:
                The new value for the item
        """
        new_objs = self._objs.copy()
        new_objs[idx] = val
        self._objs = new_objs

    def __len__(self):
        """
        Get the number of items in this `ListBlock`
        """
        return len(self._objs)

    def __iter__(self):
        """
        Iterate over the items in this `ListBlock`
        """
        return iter(self._objs)

    def __eq__(self, other, suppress_routine_paths=False):
        """
        Check equality of two `ListBlock` objects.

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

        Args:
            other:
                The other `ListBlock` object to check equality with
            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
        try:
            return len(deep_diff(self.serialize(), other.serialize(), suppress_routine_paths=suppress_routine_paths))==0
        except Exception:
            return False

    def __hash__(self):
        """
        Get the hash of this `ListBlock` object
        """
        return id(self)

    def set_list_type(self,typ="explicit"):
        """
        Set FOS list formatting (explicit or looped).

        Sets metadata for all items in this `ListBlock` to the specified type.

        List Types:
            - "explicit": Each object declares its own keys.
            - "looped": 
                Common keys are declared once at the beginning of a list. Each
                object specifies values for those keys in the declared order.
                Anomalous keys are still printed as key:value pairs.

        Args:
            typ:
                The type to set
        """
        if typ not in ("explicit", "looped"):
            raise ValueError("List type must be 'single' or 'looped'.")
        for obj in self:
            obj._meta.list_type = typ

    def serialize(self, clean=False, shallow=False, override_list_type:str|bool=None):
        """
        Serialize this `ListBlock` as a list of dictionaries.

        Overriding list type is only skipped when all objects in the list have
        the same list type. To prevent mutation, list type override is performed
        by calling
        [`set_list_type`][FoSpy.blocks.blocks.ListBlock.set_list_type] on a copy
        of this `ListBlock` and returning the serialized copy.

        `ListBlock`s of length one are always overridden to "explicit".

        List Types:
            - "explicit": Each object declares its own keys.
            - "looped": 
                Common keys are declared once at the beginning of a list. Each
                object specifies values for those keys in the declared order.
                Anomalous keys are still printed as key:value pairs.

        Args:
            clean:
                When True, no FOS format read/write metadata is included in the
                serial. Recommended for sending output to other formats like
                JSON.
            shallow:
                When True, no recursive serialization occurs. Recommended when
                serialization is used only to inspect top-level keys for object
                dictionaries.
            override_list_type:
                - When `None` (default): Checks for mixed list types and
                  recurses with override set to "explicit" if found.
                - When `False`: Does not override any list type. This should be
                  avoided for FOS-formatted output unless you know that all
                  objects in the list have the same list type.
                - When `str`: Copies this `ListBlock` and passes override to
                  [`copy.set_list_type`][FoSpy.blocks.blocks.ListBlock.set_list_type]
                  before returning the serialized copy
        """
        if override_list_type is None:
            for obj in self:
                if obj._meta.list_type == "explicit":
                    return self.serialize(clean=clean, shallow=shallow, override_list_type="explicit")
            return self.serialize(clean=clean, shallow=shallow, override_list_type="looped")
        elif not override_list_type:
            keepListType = len(self)>1
            lst = [obj.serialize(clean=clean, shallow=shallow, keepListType=keepListType) for obj in self]
            return lst
        else:
            copy = self.copy()
            copy.set_list_type(override_list_type)
            return copy.serialize(clean=clean, shallow=shallow, override_list_type=False)


    def list_avail_routines(self, recursive=False, prefix="", abbreviated=False):
        """
        Lists all methods decorated as calc routines.

        Methods are resolved as path strings relative to self, including
        indexing for recursively searched methods within self._objs. When
        returned back to a parent `ListBlock` object's call, these strings
        produce paths that can be resolved back into function calls relative to
        the parent object. See `SingleBlock.list_avail_routines()`.

        This method is usually only used in a recursive call from a
        `SingleBlock` object where one of its attributes is a `ListBlock`.

        Example:
        ```
        mySyn.materals.list_avail_routines(recursive=True)
        ## returns [
        ##     'add_weight_pcts',
        ##     '[0].add_MW',
        ##     '[1].add_MW',
        ##     ... 6 total materials with the same calc_routine
        ##     '[5].add_MW'
        ## ]

        # Resursive call from `SingleBlock` mySyn object:
        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'
        ## ]
        ```
        """
        routines = []

        # Local routines on the ListBlock itself
        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:
            if abbreviated:
                obj_routines = {}
                idx_str = "i"
                idx_num = 0
                while f"[{idx_str}{idx_num if idx_num > 0 else ''}]" in prefix:
                    if idx_str == "z":
                        idx_str = "i"
                        idx_num += 1
                    else:
                        idx_str = chr(ord(idx_str)+1)

                idx_str = f"{idx_str}{idx_num if idx_num > 0 else ''}"

                for i, obj in enumerate(self._objs):
                    if hasattr(obj, "list_avail_routines"):
                        rtns = obj.list_avail_routines(True,f"{prefix[:-1]}[{idx_str}].",abbreviated=True)
                        for routine in rtns:
                            if routine not in obj_routines:
                                obj_routines[routine] = []
                            obj_routines[routine].append(i)
                for routine, i_list in obj_routines.items():
                    routines.append(f"{routine}; {idx_str} = {i_list}")
            else:
                for i, obj in enumerate(self._objs):
                    if hasattr(obj, "list_avail_routines"):
                        child_prefix = f"{prefix[:-1]}[{i}]."
                        routines.extend(obj.list_avail_routines(
                            recursive=True,
                            prefix=child_prefix,
                            abbreviated=False
                        ))

        return routines

    def copy(self):
        """Returns a deep copy by serializing and then reconstructing."""
        cls = type(self)
        return cls(self.serialize(override_list_type=False))

    def remove_block(self, blk):
        if blk in self._objs:
            self._objs.remove(blk)

        if blk in self._staged_templates.values():
            temp_id = next(k for k,v in self._staged_templates.items() if v is blk)
            self._staged_templates.pop(temp_id)


    def remove_any(self, **kwargs):
        """
        Remove any objects from self._objs with attributes matching `kwargs`

        Args:
            **kwargs:
                A single keyword argument can be passed. Any objects with
                attr:value matching kw:arg are removed.

        Raises:
            TypeError: Exactly one keyword argument is required.

        Example:
        ```
        mySyn.materials.remove_any(supplier="sigma")
        ## removes any obj from mySyn.materials._objs where
        ## obj.supplier == "sigma"
        ```
        """
        if len(kwargs) != 1:
            raise TypeError("Exactly one keyword argument is required")

        key, val = next(iter(kwargs.items()))

        objs = list(iter(self)).copy()
        removed = 0
        for obj in self:
            if getattr(obj, key, None) == val:
                for i, existing in enumerate(objs):
                    if existing is obj:
                        del objs[i]
                        removed += 1
                        break

        self._objs = objs
        _debug.msg(f"Removed {removed} {self._reqCls.__name__} objects matching {key} = {val}.")

    def get_any(self, **kwargs):
        if len(kwargs) != 1:
            raise TypeError("Exactly one keyword argument is required")

        key, val = next(iter(kwargs.items()))
        found = []
        for obj in self:
            if getattr(obj, key, None) == val:
                found.append(obj)
        return found

    def get_first(self, **kwargs):
        return self.get_any(**kwargs)[0]

    def clear_all_comments(self):
        for obj in self:
            if hasattr(obj, "clear_all_comments"):
                obj.clear_all_comments()

    def default_key_order(self, deep=False):
        for obj in self:
            if hasattr(obj, "default_key_order"):
                obj.default_key_order(deep=deep)

    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 val in self:
            if hasattr(val, "refresh_attachments"):
                val.refresh_attachments(new_copy=new_copy, overwrite=overwrite, **kwargs)
            if isinstance(val, Attachment) and hasattr(val, "refresh"):
                val.refresh(new_copy=new_copy, overwrite=overwrite, **kwargs)

_objs instance-attribute

_objs = blockList

_reqCls class-attribute instance-attribute

_reqCls = None

_staged_templates instance-attribute

_staged_templates = {}

_temp_id_gen instance-attribute

_temp_id_gen = self.temp_id_gen()

simple_lists class-attribute instance-attribute

simple_lists = {}

Simple classmethod

Simple(reqCls=SingleBlock)

Creates a simple subclass of ListBlock

Creates a subclass of ListBlock that only accepts objects of the specified SingleBlock subclass.

Simple ListBlocks are used when no specialized methods or attributes are needed.

Parameters:

Name Type Description Default
reqCls

The subclass of SingleBlock that this ListBlock subclass accepts.

SingleBlock
Source code in FoSpy/blocks/blocks.py
@classmethod
def Simple(cls, reqCls=SingleBlock):
    """
    Creates a simple subclass of `ListBlock`

    Creates a subclass of `ListBlock` that only accepts objects of the
    specified `SingleBlock` subclass.

    Simple ListBlocks are used when no specialized methods or attributes are
    needed.

    Args:
        reqCls:
            The subclass of `SingleBlock` that this `ListBlock` subclass
            accepts.
    """
    if not issubclass(reqCls, SingleBlock):
        raise TypeError("reqCls must be a subclass of SingleBlock")

    proxy_dict = {
        "__dispatch__": {
            "_reqCls": reqCls,
        }
    }

    return ListBlock.dispatch_subclass(proxy_dict)

TemplateClass

TemplateClass()
Source code in FoSpy/blocks/blocks.py
def TemplateClass(cls):
    from .template import TemplateList
    return TemplateList.simple(cls._reqCls)

__eq__

__eq__(other, suppress_routine_paths=False)

Check equality of two ListBlock objects.

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

Parameters:

Name Type Description Default
other

The other ListBlock object to check equality with

required
suppress_routine_paths

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=False):
    """
    Check equality of two `ListBlock` objects.

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

    Args:
        other:
            The other `ListBlock` object to check equality with
        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
    try:
        return len(deep_diff(self.serialize(), other.serialize(), suppress_routine_paths=suppress_routine_paths))==0
    except Exception:
        return False

__getitem__

__getitem__(idx)

Get an item from this ListBlock by index.

Parameters:

Name Type Description Default
idx int

The index of the item

required
Source code in FoSpy/blocks/blocks.py
def __getitem__(self, idx:int):
    """
    Get an item from this `ListBlock` by index.

    Args:
        idx:
            The index of the item
    """
    return self._objs[idx]

__hash__

__hash__()

Get the hash of this ListBlock object

Source code in FoSpy/blocks/blocks.py
def __hash__(self):
    """
    Get the hash of this `ListBlock` object
    """
    return id(self)

__init__

__init__(blockList)

Constructs a ListBlock from a list of objects or serialized dictionaries.

Each item in blockList is checked against the SingleBlock subclass specified for the ListBlock subclass. If the item is not the correct subclass, it is passed to the SingleBlock subclass's dispatch_subclass method for coersion.

Parameters:

Name Type Description Default
blockList list

A list containing either dicts or SingleBlock objects (Mixing is allowed).

required

Raises: TypeError: ListBlock instances can only be constructed from subclasses with an assigned _reqCls, not the parent ListBlock class.

Source code in FoSpy/blocks/blocks.py
def __init__(self, blockList:list):
    """
    Constructs a `ListBlock` from a list of objects or serialized dictionaries.

    Each item in blockList is checked against the `SingleBlock` subclass
    specified for the `ListBlock` subclass. If the item is not the correct
    subclass, it is passed to the `SingleBlock` subclass's
    `dispatch_subclass` method for coersion.

    Args:
        blockList:
            A list containing either `dicts` or `SingleBlock` objects (Mixing
            is allowed).
    Raises:
        TypeError:
            `ListBlock` instances can only be constructed from subclasses with an assigned _reqCls, not the parent `ListBlock` class.
    """
    #self._objs = []
    if not (isinstance(self._reqCls, type) and issubclass(self._reqCls, SingleBlock)):
        raise TypeError(f"ListBlock instances can only be constructed from subclasses with an assigned _reqCls. {self.__class__} has no _reqCls.")
    self.track_attachments(**cfg.track_attachments())
    self._temp_id_gen = self.temp_id_gen()
    if not isinstance(blockList, list):
        blockList = [blockList]
    self._objs = blockList
    self._staged_templates = {}

__iter__

__iter__()

Iterate over the items in this ListBlock

Source code in FoSpy/blocks/blocks.py
def __iter__(self):
    """
    Iterate over the items in this `ListBlock`
    """
    return iter(self._objs)

__len__

__len__()

Get the number of items in this ListBlock

Source code in FoSpy/blocks/blocks.py
def __len__(self):
    """
    Get the number of items in this `ListBlock`
    """
    return len(self._objs)

__setattr__

__setattr__(name, value)

Only private attributes starting with "_" can be set.

Items in self._objs can be edited/replaced individually by indexing with self[i], or self._objs can be replaced with a new list, which is re-validated and coerced to the correct SingleBlock subclass specified by _reqCls

Parameters:

Name Type Description Default
name

The name of the attribute to set.

required
value

The value to set the attribute to.

required

Raises: AttributeError: Only private attributes starting with "_" can be set. TypeError: self._objs must be a list of objects which can be coerced to the correct SingleBlock subclass specified by _reqCls

Source code in FoSpy/blocks/blocks.py
def __setattr__(self, name, value):
    """
    Only private attributes starting with "_" can be set.

    Items in self._objs can be edited/replaced individually by indexing with
    self[i], or self._objs can be replaced with a new list, which is
    re-validated and coerced to the correct `SingleBlock` subclass specified
    by _reqCls

    Args:
        name:
            The name of the attribute to set.
        value:
            The value to set the attribute to.
    Raises:
        AttributeError:
            Only private attributes starting with "_" can be set.
        TypeError:
            self._objs must be a list of objects which can be coerced to the
            correct `SingleBlock` subclass specified by _reqCls
    """
    from .attachments import Attachment
    from ._blockUtils import _unwrap_listblock
    from .template import TemplateBlock, TemplateList

    if name == "_objs":


        if hasattr(self, "_reqCls"):
            errors = []
            typ = self._reqCls
            if getattr(typ, "_full_class", None) is not None:
                check_typ = typ._full_class
            else:
                check_typ = typ
            value = _unwrap_listblock(value, typ=check_typ)
            new_list = []
            for idx, obj in enumerate(value):
                if isinstance(obj, dict) and obj == {}:
                    continue
                if not isinstance(obj, check_typ):
                    try:
                        new_obj = typ(obj)
                    except Exception as e:
                        errors.append(err.ListBlockMismatchError(self, obj, idx, cause=e))
                        continue  
                    if isinstance(obj, Attachment) and hasattr(obj, "_filepath"):
                        new_obj._filepath = obj._filepath
                    obj=new_obj
                elif isinstance(obj, TemplateBlock) and not isinstance(self, TemplateList):
                    try:
                        obj = obj.fill()
                        if isinstance(obj, TemplateBlock):
                            raise err.FoSpyStructureError("Could not fill a template into a complete block before adding to this ListBlock")
                    except Exception as e:
                        errors.append(err.ListBlockMismatchError(self, obj, idx, cause=e))
                obj._parent_block = self
                if hasattr(obj, "refresh") and isinstance(obj, Attachment):
                    obj.refresh(new_copy=self._att_new_copy, overwrite=self._att_overwrite)
                new_list.append(obj)
            if errors:
                raise err.ListBlockErrorGroup(self, errors)
            return super().__setattr__(name, new_list)

    elif name.startswith("_") or name in self._reserved:
        return super().__setattr__(name,value)
    else:
        raise AttributeError(
            f"{type(self).__name__} does not allow setting attribute '{name}'. "
            f"Only private names starting with '_' can be used. "
            f"Each list item is an item in {type(self).__name__}._objs which can be edited individually, "
            f"Or you can replace {type(self).__name__}._objs with a new list of objects."
        )

__setitem__

__setitem__(idx, val)

Set an item to this ListBlock by index.

After setting, all items in this ListBlock's _objs list are passed back to __setattr__ for validation.

Parameters:

Name Type Description Default
idx

The index of the item

required
val

The new value for the item

required
Source code in FoSpy/blocks/blocks.py
def __setitem__(self, idx, val):
    """
    Set an item to this `ListBlock` by index.

    After setting, all items in this `ListBlock`'s `_objs` list are passed
    back to [`__setattr__`][FoSpy.blocks.blocks.ListBlock.__setattr__] for
    validation.

    Args:
        idx:
            The index of the item
        val:
            The new value for the item
    """
    new_objs = self._objs.copy()
    new_objs[idx] = val
    self._objs = new_objs

add_dispatch classmethod

add_dispatch(blockDict, dispatch_key, **kwargs)
Source code in FoSpy/blocks/blocks.py
@classmethod
def add_dispatch(cls, blockDict, dispatch_key, **kwargs):
    _ = SingleBlock.add_dispatch(blockDict, dispatch_key, **kwargs)

    from .template import TemplateList
    from .._docs.properties import _validator_rules
    from ._blockUtils import _get_docs_link

    block_dispatch = blockDict.setdefault("__dispatch__", {})
    reqCls = block_dispatch.setdefault("_reqCls", cls._reqCls)

    if reqCls is None:
        return {}

    if getattr(reqCls, "_full_class", None) is not None:
        return TemplateList.Simple(reqCls)

    registry = ListBlock.__dispatch__['registry']

    if reqCls not in registry:
        link = _get_docs_link(reqCls)

        @_validator_rules(
            f"A [simple `ListBlock`](#listblock-and-simple-lists) of [`{reqCls.__name__}` objects.]{link}"
        )
        @SingleBlock.register_dispatch(reqCls, from_parent=ListBlock)
        class SimpleList(ListBlock):
            _reqCls = reqCls

        SimpleList.__name__ = f"{reqCls.__name__}SimpleList"
        SimpleList.__qualname__ = f"ListBlock.Simple.{reqCls.__name__}List"
        SimpleList.__module__ = reqCls.__module__

    return {dispatch_key: reqCls}

append

append(obj)

Append a SingleBlock-coercable object to this ListBlock

Appends the object to this ListBlock's _objs list, and passes the entire list back to __setattr__ for validation.

Parameters:

Name Type Description Default
obj SingleBlock

The object to append

required
Source code in FoSpy/blocks/blocks.py
def append(self, obj:SingleBlock):
    """
    Append a `SingleBlock`-coercable object to this `ListBlock`

    Appends the object to this `ListBlock`'s `_objs` list, and passes the
    entire list back to
    [`__setattr__`][FoSpy.blocks.blocks.ListBlock.__setattr__] for
    validation.

    Args:
        obj:
            The object to append
    """
    objs = self._objs.copy()
    objs.append(obj)
    self._objs = objs

block_to_idx

block_to_idx(blk, idx)
Source code in FoSpy/blocks/blocks.py
def block_to_idx(self, blk, idx):
    objs = self._objs.copy()
    current_idx = None
    if blk in objs:
        current_idx = objs.index(blk)
        if idx <= current_idx:
            current_idx += 1
    objs.insert(idx, blk)
    objs.pop(current_idx)
    self._objs = objs

clear_all_comments

clear_all_comments()
Source code in FoSpy/blocks/blocks.py
def clear_all_comments(self):
    for obj in self:
        if hasattr(obj, "clear_all_comments"):
            obj.clear_all_comments()

copy

copy()

Returns a deep copy by serializing and then reconstructing.

Source code in FoSpy/blocks/blocks.py
def copy(self):
    """Returns a deep copy by serializing and then reconstructing."""
    cls = type(self)
    return cls(self.serialize(override_list_type=False))

default_key_order

default_key_order(deep=False)
Source code in FoSpy/blocks/blocks.py
def default_key_order(self, deep=False):
    for obj in self:
        if hasattr(obj, "default_key_order"):
            obj.default_key_order(deep=deep)

fill_staged_template

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

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

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

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

    if idx is None:
        self.append(filled)
    else:
        self.insert(idx, filled)

    return temp_id, filled

get_any

get_any(**kwargs)
Source code in FoSpy/blocks/blocks.py
def get_any(self, **kwargs):
    if len(kwargs) != 1:
        raise TypeError("Exactly one keyword argument is required")

    key, val = next(iter(kwargs.items()))
    found = []
    for obj in self:
        if getattr(obj, key, None) == val:
            found.append(obj)
    return found

get_first

get_first(**kwargs)
Source code in FoSpy/blocks/blocks.py
def get_first(self, **kwargs):
    return self.get_any(**kwargs)[0]

get_idx

get_idx(blk)
Source code in FoSpy/blocks/blocks.py
def get_idx(self, blk):
    try:
        return self._objs.index(blk)
    except ValueError:
        raise err.FoSpyStructureError(f"Block {blk} is not in {self}")

has_staged

has_staged()
Source code in FoSpy/blocks/blocks.py
def has_staged(self):
    return len(self._staged_templates) > 0 or any(blk.has_staged() for blk in self)

insert

insert(idx, obj)

Insert a SingleBlock-coercable object into this ListBlock.

Inserts the object into this ListBlock's _objs list, and passes the entire list back to __setattr__ for validation.

Parameters:

Name Type Description Default
idx

The index to insert the object at

required
obj SingleBlock

The object to insert

required
Source code in FoSpy/blocks/blocks.py
def insert(self, idx, obj:SingleBlock):
    """
    Insert a `SingleBlock`-coercable object into this `ListBlock`.

    Inserts the object into this `ListBlock`'s `_objs` list, and passes the
    entire list back to
    [`__setattr__`][FoSpy.blocks.blocks.ListBlock.__setattr__] for
    validation.

    Args:
        idx:
            The index to insert the object at
        obj:
            The object to insert
    """
    objs = self._objs.copy()
    objs.insert(idx,obj)
    self._objs = objs

list_avail_routines

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

Lists all methods decorated as calc routines.

Methods are resolved as path strings relative to self, including indexing for recursively searched methods within self._objs. When returned back to a parent ListBlock object's call, these strings produce paths that can be resolved back into function calls relative to the parent object. See SingleBlock.list_avail_routines().

This method is usually only used in a recursive call from a SingleBlock object where one of its attributes is a ListBlock.

Example:

mySyn.materals.list_avail_routines(recursive=True)
## returns [
##     'add_weight_pcts',
##     '[0].add_MW',
##     '[1].add_MW',
##     ... 6 total materials with the same calc_routine
##     '[5].add_MW'
## ]

# Resursive call from `SingleBlock` mySyn object:
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'
## ]

Source code in FoSpy/blocks/blocks.py
def list_avail_routines(self, recursive=False, prefix="", abbreviated=False):
    """
    Lists all methods decorated as calc routines.

    Methods are resolved as path strings relative to self, including
    indexing for recursively searched methods within self._objs. When
    returned back to a parent `ListBlock` object's call, these strings
    produce paths that can be resolved back into function calls relative to
    the parent object. See `SingleBlock.list_avail_routines()`.

    This method is usually only used in a recursive call from a
    `SingleBlock` object where one of its attributes is a `ListBlock`.

    Example:
    ```
    mySyn.materals.list_avail_routines(recursive=True)
    ## returns [
    ##     'add_weight_pcts',
    ##     '[0].add_MW',
    ##     '[1].add_MW',
    ##     ... 6 total materials with the same calc_routine
    ##     '[5].add_MW'
    ## ]

    # Resursive call from `SingleBlock` mySyn object:
    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'
    ## ]
    ```
    """
    routines = []

    # Local routines on the ListBlock itself
    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:
        if abbreviated:
            obj_routines = {}
            idx_str = "i"
            idx_num = 0
            while f"[{idx_str}{idx_num if idx_num > 0 else ''}]" in prefix:
                if idx_str == "z":
                    idx_str = "i"
                    idx_num += 1
                else:
                    idx_str = chr(ord(idx_str)+1)

            idx_str = f"{idx_str}{idx_num if idx_num > 0 else ''}"

            for i, obj in enumerate(self._objs):
                if hasattr(obj, "list_avail_routines"):
                    rtns = obj.list_avail_routines(True,f"{prefix[:-1]}[{idx_str}].",abbreviated=True)
                    for routine in rtns:
                        if routine not in obj_routines:
                            obj_routines[routine] = []
                        obj_routines[routine].append(i)
            for routine, i_list in obj_routines.items():
                routines.append(f"{routine}; {idx_str} = {i_list}")
        else:
            for i, obj in enumerate(self._objs):
                if hasattr(obj, "list_avail_routines"):
                    child_prefix = f"{prefix[:-1]}[{i}]."
                    routines.extend(obj.list_avail_routines(
                        recursive=True,
                        prefix=child_prefix,
                        abbreviated=False
                    ))

    return routines

order_down

order_down(blk)
Source code in FoSpy/blocks/blocks.py
def order_down(self, blk):
    idx = self.get_idx(blk)
    if idx == len(self)-1:
        return
    self.block_to_idx(blk, idx+2)

order_up

order_up(blk)
Source code in FoSpy/blocks/blocks.py
def order_up(self, blk):
    idx = self.get_idx(blk)
    if idx == 0:
        return
    self.block_to_idx(blk, idx-1)

refresh_attachments

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 val in self:
        if hasattr(val, "refresh_attachments"):
            val.refresh_attachments(new_copy=new_copy, overwrite=overwrite, **kwargs)
        if isinstance(val, Attachment) and hasattr(val, "refresh"):
            val.refresh(new_copy=new_copy, overwrite=overwrite, **kwargs)

remove_any

remove_any(**kwargs)

Remove any objects from self._objs with attributes matching kwargs

Parameters:

Name Type Description Default
**kwargs

A single keyword argument can be passed. Any objects with attr:value matching kw:arg are removed.

{}

Raises:

Type Description
TypeError

Exactly one keyword argument is required.

Example:

mySyn.materials.remove_any(supplier="sigma")
## removes any obj from mySyn.materials._objs where
## obj.supplier == "sigma"

Source code in FoSpy/blocks/blocks.py
def remove_any(self, **kwargs):
    """
    Remove any objects from self._objs with attributes matching `kwargs`

    Args:
        **kwargs:
            A single keyword argument can be passed. Any objects with
            attr:value matching kw:arg are removed.

    Raises:
        TypeError: Exactly one keyword argument is required.

    Example:
    ```
    mySyn.materials.remove_any(supplier="sigma")
    ## removes any obj from mySyn.materials._objs where
    ## obj.supplier == "sigma"
    ```
    """
    if len(kwargs) != 1:
        raise TypeError("Exactly one keyword argument is required")

    key, val = next(iter(kwargs.items()))

    objs = list(iter(self)).copy()
    removed = 0
    for obj in self:
        if getattr(obj, key, None) == val:
            for i, existing in enumerate(objs):
                if existing is obj:
                    del objs[i]
                    removed += 1
                    break

    self._objs = objs
    _debug.msg(f"Removed {removed} {self._reqCls.__name__} objects matching {key} = {val}.")

remove_block

remove_block(blk)
Source code in FoSpy/blocks/blocks.py
def remove_block(self, blk):
    if blk in self._objs:
        self._objs.remove(blk)

    if blk in self._staged_templates.values():
        temp_id = next(k for k,v in self._staged_templates.items() if v is blk)
        self._staged_templates.pop(temp_id)

remove_idx

remove_idx(from_idx=None, to_idx=None)

Remove a range of items from this ListBlock

Removes a range of items from this ListBlock's _objs list, and passes the entire list back to __setattr__ for validation.

from_idx is inclusive, and to_idx is non-inclusive. i.e., if from_idx is 0 and to_idx is 1, then the first item in the list will be removed, but not the second.

If from_idx is None, then all items starting at and including to_idx will be removed. If to_idx is None, then all items up to and not including from_idx will be removed.

Parameters:

Name Type Description Default
from_idx int

The index of the first item to remove

None
to_idx int

The non-inclusive index to stop removing

None
Source code in FoSpy/blocks/blocks.py
def remove_idx(self, from_idx:int=None, to_idx:int=None):
    """
    Remove a range of items from this `ListBlock`

    Removes a range of items from this `ListBlock`'s `_objs` list, and
    passes the entire list back to
    [`__setattr__`][FoSpy.blocks.blocks.ListBlock.__setattr__] for
    validation.

    `from_idx` is inclusive, and `to_idx` is non-inclusive. i.e., if
    `from_idx` is 0 and `to_idx` is 1, then the first item in the list will
    be removed, but not the second.

    If `from_idx` is None, then all items starting at and including `to_idx`
    will be removed. If `to_idx` is None, then all items up to and **not**
    including `from_idx` will be removed.

    Args:
        from_idx:
            The index of the first item to remove
        to_idx:
            The non-inclusive index to stop removing
    """
    if from_idx is None and to_idx is None:
        self._objs = []

    objs = self._objs.copy()

    if from_idx is None:
        objs = objs[to_idx:]
    elif to_idx is None:
        objs = objs[:from_idx]
    else:
        objs = objs[:from_idx] + objs[to_idx:]

    self._objs = objs   

serialize

serialize(
    clean=False, shallow=False, override_list_type=None
)

Serialize this ListBlock as a list of dictionaries.

Overriding list type is only skipped when all objects in the list have the same list type. To prevent mutation, list type override is performed by calling set_list_type on a copy of this ListBlock and returning the serialized copy.

ListBlocks of length one are always overridden to "explicit".

List Types
  • "explicit": Each object declares its own keys.
  • "looped": Common keys are declared once at the beginning of a list. Each object specifies values for those keys in the declared order. Anomalous keys are still printed as key:value pairs.

Parameters:

Name Type Description Default
clean

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

False
shallow

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

False
override_list_type str | bool
  • When None (default): Checks for mixed list types and recurses with override set to "explicit" if found.
  • When False: Does not override any list type. This should be avoided for FOS-formatted output unless you know that all objects in the list have the same list type.
  • When str: Copies this ListBlock and passes override to copy.set_list_type before returning the serialized copy
None
Source code in FoSpy/blocks/blocks.py
def serialize(self, clean=False, shallow=False, override_list_type:str|bool=None):
    """
    Serialize this `ListBlock` as a list of dictionaries.

    Overriding list type is only skipped when all objects in the list have
    the same list type. To prevent mutation, list type override is performed
    by calling
    [`set_list_type`][FoSpy.blocks.blocks.ListBlock.set_list_type] on a copy
    of this `ListBlock` and returning the serialized copy.

    `ListBlock`s of length one are always overridden to "explicit".

    List Types:
        - "explicit": Each object declares its own keys.
        - "looped": 
            Common keys are declared once at the beginning of a list. Each
            object specifies values for those keys in the declared order.
            Anomalous keys are still printed as key:value pairs.

    Args:
        clean:
            When True, no FOS format read/write metadata is included in the
            serial. Recommended for sending output to other formats like
            JSON.
        shallow:
            When True, no recursive serialization occurs. Recommended when
            serialization is used only to inspect top-level keys for object
            dictionaries.
        override_list_type:
            - When `None` (default): Checks for mixed list types and
              recurses with override set to "explicit" if found.
            - When `False`: Does not override any list type. This should be
              avoided for FOS-formatted output unless you know that all
              objects in the list have the same list type.
            - When `str`: Copies this `ListBlock` and passes override to
              [`copy.set_list_type`][FoSpy.blocks.blocks.ListBlock.set_list_type]
              before returning the serialized copy
    """
    if override_list_type is None:
        for obj in self:
            if obj._meta.list_type == "explicit":
                return self.serialize(clean=clean, shallow=shallow, override_list_type="explicit")
        return self.serialize(clean=clean, shallow=shallow, override_list_type="looped")
    elif not override_list_type:
        keepListType = len(self)>1
        lst = [obj.serialize(clean=clean, shallow=shallow, keepListType=keepListType) for obj in self]
        return lst
    else:
        copy = self.copy()
        copy.set_list_type(override_list_type)
        return copy.serialize(clean=clean, shallow=shallow, override_list_type=False)

set_list_type

set_list_type(typ='explicit')

Set FOS list formatting (explicit or looped).

Sets metadata for all items in this ListBlock to the specified type.

List Types
  • "explicit": Each object declares its own keys.
  • "looped": Common keys are declared once at the beginning of a list. Each object specifies values for those keys in the declared order. Anomalous keys are still printed as key:value pairs.

Parameters:

Name Type Description Default
typ

The type to set

'explicit'
Source code in FoSpy/blocks/blocks.py
def set_list_type(self,typ="explicit"):
    """
    Set FOS list formatting (explicit or looped).

    Sets metadata for all items in this `ListBlock` to the specified type.

    List Types:
        - "explicit": Each object declares its own keys.
        - "looped": 
            Common keys are declared once at the beginning of a list. Each
            object specifies values for those keys in the declared order.
            Anomalous keys are still printed as key:value pairs.

    Args:
        typ:
            The type to set
    """
    if typ not in ("explicit", "looped"):
        raise ValueError("List type must be 'single' or 'looped'.")
    for obj in self:
        obj._meta.list_type = typ

stage_template

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

    if isinstance(template, TemplateBlock):
        temp_id = temp_id or template.template_name
        if temp_id in self._staged_templates:
            temp_id += f" ({next(self._temp_id_gen)})"
    elif isinstance(template, dict):
        temp_id = temp_id or template.get("template_name", next(self._temp_id_gen))
    else:
        raise ValueError("Template must be a TemplateBlock or dictionary.")

    if isinstance(template, dict):
        template = self._reqCls.reflex(serialize=False,**template)
        template.template_name = temp_id
    elif not isinstance(template, self._reqCls):
        raise ValueError("Template must be a TemplateBlock subclass of the same type as this ListBlock.")

    if temp_id in self._staged_templates:
        raise ValueError(f"A Template has already been staged for {temp_id}.")

    template._staged_parent = self
    self._staged_templates[temp_id] = template

    return temp_id, template

temp_id_gen

temp_id_gen()
Source code in FoSpy/blocks/blocks.py
def temp_id_gen(self):
    i = 0
    while True:
        yield "template_" + str(i)
        i += 1

Material

Bases: Chemical

Represents a material used in a synthesis

Source code in FoSpy/blocks/materials.py
class Material(Chemical):
    """
    Represents a material used in a synthesis
    """
    _id_key = "name"
    pass

_id_key class-attribute instance-attribute

_id_key = 'name'

MaterialList

Bases: ListBlock

Represents a list of materials used in a synthesis

Source code in FoSpy/blocks/materials.py
class MaterialList(ListBlock):
    """
    Represents a list of materials used in a synthesis
    """
    _reqCls = Material

    def calc_weight_pcts(self, typ=None):
        """
        Calculate weight percent for each material with matching type.

        Args:
            typ:
                Only materials with type attribute matching typ are considered
                in the total weight and given weight percents. This is useful
                if, for instance, you want weight percents of your contributing
                "reagents" without including "flux" or "solvent" materials.

        Returns:
            dict mapping material : weight_pct
        """
        from decimal import Decimal

        weights = {}
        for mat in self._objs:
            if mat.type == typ or typ is None:
                amount = mat.amount
                mw = mat.formula.formula_weight
                weights[mat] = amount() * Decimal(mw)

        percents = {}
        total_weight = sum(weights.values())
        for mat, wt in weights.items():
            pct = 100 * wt / total_weight
            percents[mat] = pct
        return percents



    @_calc_routine
    def add_all_MW(self):
        """
        Attach a molecular weight comment to all materials
        """
        for mat in self._objs:
            mat.add_MW()

    @_calc_routine
    def add_weight_pcts(self, typ=None):
        """
        Calculate weight percents and attach them as comments to each material's amount.

        Args:
            typ:
                Only materials with type attribute matching typ are considered
                in the total weight and given weight percents. This is useful
                if, for instance, you want weight percents of your contributing
                "reagents" without including "flux" or "solvent" materials.
        """
        for mat, pct in self.calc_weight_pcts(typ).items():
            label = typ.capitalize() if typ else "Total"
            comment = f"{label} weight percent: {pct:.2f}%"
            _debug.msg(f"Calculated {label} weight percent: {pct:.2f}% for {mat.name}")
            mat.add_calc_comment("amount",comment, f"{label}_pct")

_reqCls class-attribute instance-attribute

_reqCls = Material

add_all_MW

add_all_MW()

Attach a molecular weight comment to all materials

Source code in FoSpy/blocks/materials.py
@_calc_routine
def add_all_MW(self):
    """
    Attach a molecular weight comment to all materials
    """
    for mat in self._objs:
        mat.add_MW()

add_weight_pcts

add_weight_pcts(typ=None)

Calculate weight percents and attach them as comments to each material's amount.

Parameters:

Name Type Description Default
typ

Only materials with type attribute matching typ are considered in the total weight and given weight percents. This is useful if, for instance, you want weight percents of your contributing "reagents" without including "flux" or "solvent" materials.

None
Source code in FoSpy/blocks/materials.py
@_calc_routine
def add_weight_pcts(self, typ=None):
    """
    Calculate weight percents and attach them as comments to each material's amount.

    Args:
        typ:
            Only materials with type attribute matching typ are considered
            in the total weight and given weight percents. This is useful
            if, for instance, you want weight percents of your contributing
            "reagents" without including "flux" or "solvent" materials.
    """
    for mat, pct in self.calc_weight_pcts(typ).items():
        label = typ.capitalize() if typ else "Total"
        comment = f"{label} weight percent: {pct:.2f}%"
        _debug.msg(f"Calculated {label} weight percent: {pct:.2f}% for {mat.name}")
        mat.add_calc_comment("amount",comment, f"{label}_pct")

calc_weight_pcts

calc_weight_pcts(typ=None)

Calculate weight percent for each material with matching type.

Parameters:

Name Type Description Default
typ

Only materials with type attribute matching typ are considered in the total weight and given weight percents. This is useful if, for instance, you want weight percents of your contributing "reagents" without including "flux" or "solvent" materials.

None

Returns:

Type Description

dict mapping material : weight_pct

Source code in FoSpy/blocks/materials.py
def calc_weight_pcts(self, typ=None):
    """
    Calculate weight percent for each material with matching type.

    Args:
        typ:
            Only materials with type attribute matching typ are considered
            in the total weight and given weight percents. This is useful
            if, for instance, you want weight percents of your contributing
            "reagents" without including "flux" or "solvent" materials.

    Returns:
        dict mapping material : weight_pct
    """
    from decimal import Decimal

    weights = {}
    for mat in self._objs:
        if mat.type == typ or typ is None:
            amount = mat.amount
            mw = mat.formula.formula_weight
            weights[mat] = amount() * Decimal(mw)

    percents = {}
    total_weight = sum(weights.values())
    for mat, wt in weights.items():
        pct = 100 * wt / total_weight
        percents[mat] = pct
    return percents

_calc_routine

_calc_routine(func)

Decorator for SingleBlock or ListBlock methods that calculate values from existing attributes.

calc_routine functions can be called at any time, but can also be queued to run at serialization, as in refreshing relevant calculated values before saving the file. See SingleBlock.add_calc_routine()

Source code in FoSpy/blocks/_blockUtils.py
def _calc_routine(func):
    """
    Decorator for `SingleBlock` or `ListBlock` methods that calculate values
    from existing attributes.

    `calc_routine` functions can be called at any time, but can also be queued
    to run at serialization, as in refreshing relevant calculated values before
    saving the file. See `SingleBlock.add_calc_routine()`
    """

    func._is_calc_routine = True
    func.__doc__ = (func.__doc__ or "") + "\nThis function is decorated as a calc_routine"

    return func