Skip to content

files

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.files

EXT_DESC_MAP module-attribute

EXT_DESC_MAP = {k: (v[2]) for k, v in (EXT_MAP.items())}

EXT_MAP module-attribute

EXT_MAP = {
    "fos": (
        dict_from_file,
        write_dict_to_file,
        "FoS Format",
    ),
    "fosx": (open_fosx, save_fosx, "FoSX Package"),
    "json": (
        lambda fp: json.load(open(fp, "r")),
        lambda blockDict, fp, **kwargs: json.dump(
            blockDict,
            open(fp, "w"),
            indent=kwargs.pop("json_indent", 4),
            **kwargs
        ),
        "FoS-style JSON",
    ),
}

EXT_READ_MAP module-attribute

EXT_READ_MAP = {k: (v[0]) for k, v in (EXT_MAP.items())}

EXT_WRITE_MAP module-attribute

EXT_WRITE_MAP = {k: (v[1]) for k, v in (EXT_MAP.items())}

temp_dirs module-attribute

temp_dirs = []

FileBlock

Bases: SingleBlock

Represents a set of blocks loaded from a file.

All public attributes of FileBlock objects are either SingleBlock or ListBlock objects. Attributes without a header at the start of the file are parsed into {"metadata": blockDict} before passing to FileBlock.

Noteable Subclasses:

Synthesis(FileBlock)
TemplateSet(FileBlock)

Source code in FoSpy/blocks/files.py
@SingleBlock.setup_dispatch(from_key="_fos_type", allow_self=False)
class FileBlock(SingleBlock):
    """
    Represents a set of blocks loaded from a file.

    All public attributes of `FileBlock` objects are either `SingleBlock` or
    `ListBlock` objects. Attributes without a header at the start of the file
    are parsed into `{"metadata": blockDict}` before passing to `FileBlock`.

    Noteable Subclasses:
    ```
    Synthesis(FileBlock)
    TemplateSet(FileBlock)
    ```
    """
    def __init__(self, blockDict, _sourceFile=None, **kwargs):
        """
        Optionally specify _sourceFile before constructing from blockDict using parent `SingleBlock` constructor.
        """
        self._sourceFile = _sourceFile

        self._tempdir = tempfile.TemporaryDirectory()
        self._temppath = Path(self._tempdir.name)
        atexit.register(self.cleanup)

        super().__init__(blockDict, **kwargs)
        self.refresh_attachments()

    def __setattr__(self, name, value):
        if name == "_sourceFile":
            if str(value).endswith(".fosx"):
                self._ext_file = value
                self._ext_dir, value = _get_ext_dir(value)
            else:
                self._ext_file = None
                self._ext_dir = None
        super().__setattr__(name, value)  

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

        from .. import _errors as err
        from ._blockUtils import _unwrap_block

        from .metadata import MetaData
        metadata = blockDict.get("metadata", None)

        if metadata is None:
            raise err.MissingPropertyError("metadata", cls, blockDict=blockDict)

        metadata = _unwrap_block(metadata)
        fos_type = metadata.pop("fos_type", None)
        if fos_type is None:
            raise err.MissingPropertyError("fos_type", MetaData, blockDict=metadata)

        return {dispatch_key: fos_type}


    def cleanup(self):
        if self._tempdir is not None:
            self._tempdir.cleanup()

    def get_id(self):
        try:
            if self._sourceFile is not None:
                from pathlib import Path
                fp = Path(self._sourceFile)
                return "file_name", str(fp.name)
            else:
                return "file_name", "<Unsaved FileBlock>"
        except Exception:
            return super().get_id()

    @classmethod
    def fromFile(cls, filepath):
        abspath = os.path.abspath(filepath)
        pathstr = str(abspath)
        try:
            ext = pathstr.lower().split(".")[-1]
        except IndexError:
            raise ValueError(f"Could not determine extension for filepath: {pathstr}")

        if ext not in EXT_READ_MAP:
            raise ValueError(f"Unrecognized file extension '{ext}'. Supported extensions are: {list(EXT_READ_MAP.keys())}")

        blockDict = EXT_READ_MAP[ext](abspath)
        abspath = blockDict.pop("_sourceFile", abspath)

        return cls(blockDict, _sourceFile=abspath)

    def save(self, filepath:str=None, json_indent=4, **kwargs):
        """
        Sends a serialized dict to be written to file.

        Args:
            filepath:
                If specified, writes serialized dict to filepath. ks to `self._sourceFile`.
            json_indent:
                Indent to use for json.dump when saving as json
            **kwargs:
                Optional kwargs to pass to saving routine (unique to each file extension)

        Raises:
            ValueError:
                If _sourceFile is not specified (if `FileBlock` was copied from
                another object or constructed directly from a blockDict),
                filepath must be specified.
        """
        from warnings import warn
        saving_as = filepath is not None
        try:
            if not saving_as:
                if self._sourceFile is None:
                    raise ValueError("Synthesis object was constructed without a sourceFile. A save destination must be specified.")
                else:
                    filepath = self._sourceFile
            self._sourceFile = os.path.abspath(filepath)

            filepath = self._sourceFile if self._ext_file is None else self._ext_file
            if str(filepath).endswith(".fosx"):
                return self.package(filepath)
            self.refresh_attachments()
            pathstr = str(filepath)
            try:
                ext = pathstr.lower().split(".")[-1]
            except IndexError:
                raise ValueError(f"Could not determine extension for filepath: {pathstr}")


            ext = str(filepath).lower().split(".")[-1]

            if ext not in EXT_WRITE_MAP:
                raise ValueError(f"Unrecognized file extension '{ext}'. Supported extensions are: {list(EXT_WRITE_MAP.keys())}")

            blockDict = self.serialize(clean="fos" not in ext)

            EXT_WRITE_MAP[ext](blockDict, filepath, json_indent=json_indent, **kwargs)

        except Exception as e:
            if not saving_as:
                warn(f"Could not save file. Disconnected from source file for safety. Exception: {e}", RuntimeWarning)
                self._sourceFile = None
                return e
            else:
                raise e
        return True

    def get_file_name(self):
        if self._sourceFile is None:
            return "<Unsaved FileBlock>"

        if hasattr(self, "_ext_file") and self._ext_file is not None:
            filepath = self._ext_file
        else:
            filepath = self._sourceFile

        return os.path.basename(filepath)

    def copy(self, path=None):
        """
        Returns a deep copy of self by saving to a temp file and reloading.

        Save/Reload allows attachment tracking to remain intact where
        serialization/reconstruction would normally desync.
        """
        if path is None:
            # get temporary save location
            loc = self._temppath / "~temp~.fos"
        else:
            path = Path(path)
            loc = path

        # cache current source file
        src = self._sourceFile

        # save and restore cached source file
        self.save(loc)
        self._sourceFile = src

        # load copy from temp file
        copy = self.fromFile(loc)
        # desync copy from temp file
        copy._sourceFile = path
        return copy

    def check_attachments(self):
        pass

    def matches_file(self):
        reloaded = self.fromFile(self._sourceFile)

        return self.__eq__(reloaded, suppress_routine_paths=True)

    def package(self, pkg_fp):
        import shutil
        pkg_fp = Path(pkg_fp)
        pkg_dir = self._temppath / "~package~"
        pkg_dir.mkdir(exist_ok=True)

        attachment_dir = pkg_dir / "attachments"
        attachment_dir.mkdir(exist_ok=True)

        copy = self.copy()

        for attachment in copy.find_attachments():
            attachment.path = "attachments"

        copy._sourceFile = pkg_dir / (pkg_fp.stem + ".fos")
        copy.refresh_attachments(new_copy=True, overwrite=False)
        copy.save()

        pkg_fp = pkg_fp.parent / pkg_fp.stem
        fosx_fp = pkg_fp.with_suffix(".fosx")
        zip_fp = Path(shutil.make_archive(pkg_fp, "zip", pkg_dir))

        if fosx_fp.exists():
            fosx_fp.unlink()

        zip_fp.rename(fosx_fp)

        shutil.rmtree(pkg_dir)

_sourceFile instance-attribute

_sourceFile = _sourceFile

_tempdir instance-attribute

_tempdir = tempfile.TemporaryDirectory()

_temppath instance-attribute

_temppath = Path(self._tempdir.name)

__init__

__init__(blockDict, _sourceFile=None, **kwargs)

Optionally specify _sourceFile before constructing from blockDict using parent SingleBlock constructor.

Source code in FoSpy/blocks/files.py
def __init__(self, blockDict, _sourceFile=None, **kwargs):
    """
    Optionally specify _sourceFile before constructing from blockDict using parent `SingleBlock` constructor.
    """
    self._sourceFile = _sourceFile

    self._tempdir = tempfile.TemporaryDirectory()
    self._temppath = Path(self._tempdir.name)
    atexit.register(self.cleanup)

    super().__init__(blockDict, **kwargs)
    self.refresh_attachments()

__setattr__

__setattr__(name, value)
Source code in FoSpy/blocks/files.py
def __setattr__(self, name, value):
    if name == "_sourceFile":
        if str(value).endswith(".fosx"):
            self._ext_file = value
            self._ext_dir, value = _get_ext_dir(value)
        else:
            self._ext_file = None
            self._ext_dir = None
    super().__setattr__(name, value)  

add_dispatch classmethod

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

    from .. import _errors as err
    from ._blockUtils import _unwrap_block

    from .metadata import MetaData
    metadata = blockDict.get("metadata", None)

    if metadata is None:
        raise err.MissingPropertyError("metadata", cls, blockDict=blockDict)

    metadata = _unwrap_block(metadata)
    fos_type = metadata.pop("fos_type", None)
    if fos_type is None:
        raise err.MissingPropertyError("fos_type", MetaData, blockDict=metadata)

    return {dispatch_key: fos_type}

check_attachments

check_attachments()
Source code in FoSpy/blocks/files.py
def check_attachments(self):
    pass

cleanup

cleanup()
Source code in FoSpy/blocks/files.py
def cleanup(self):
    if self._tempdir is not None:
        self._tempdir.cleanup()

copy

copy(path=None)

Returns a deep copy of self by saving to a temp file and reloading.

Save/Reload allows attachment tracking to remain intact where serialization/reconstruction would normally desync.

Source code in FoSpy/blocks/files.py
def copy(self, path=None):
    """
    Returns a deep copy of self by saving to a temp file and reloading.

    Save/Reload allows attachment tracking to remain intact where
    serialization/reconstruction would normally desync.
    """
    if path is None:
        # get temporary save location
        loc = self._temppath / "~temp~.fos"
    else:
        path = Path(path)
        loc = path

    # cache current source file
    src = self._sourceFile

    # save and restore cached source file
    self.save(loc)
    self._sourceFile = src

    # load copy from temp file
    copy = self.fromFile(loc)
    # desync copy from temp file
    copy._sourceFile = path
    return copy

fromFile classmethod

fromFile(filepath)
Source code in FoSpy/blocks/files.py
@classmethod
def fromFile(cls, filepath):
    abspath = os.path.abspath(filepath)
    pathstr = str(abspath)
    try:
        ext = pathstr.lower().split(".")[-1]
    except IndexError:
        raise ValueError(f"Could not determine extension for filepath: {pathstr}")

    if ext not in EXT_READ_MAP:
        raise ValueError(f"Unrecognized file extension '{ext}'. Supported extensions are: {list(EXT_READ_MAP.keys())}")

    blockDict = EXT_READ_MAP[ext](abspath)
    abspath = blockDict.pop("_sourceFile", abspath)

    return cls(blockDict, _sourceFile=abspath)

get_file_name

get_file_name()
Source code in FoSpy/blocks/files.py
def get_file_name(self):
    if self._sourceFile is None:
        return "<Unsaved FileBlock>"

    if hasattr(self, "_ext_file") and self._ext_file is not None:
        filepath = self._ext_file
    else:
        filepath = self._sourceFile

    return os.path.basename(filepath)

get_id

get_id()
Source code in FoSpy/blocks/files.py
def get_id(self):
    try:
        if self._sourceFile is not None:
            from pathlib import Path
            fp = Path(self._sourceFile)
            return "file_name", str(fp.name)
        else:
            return "file_name", "<Unsaved FileBlock>"
    except Exception:
        return super().get_id()

matches_file

matches_file()
Source code in FoSpy/blocks/files.py
def matches_file(self):
    reloaded = self.fromFile(self._sourceFile)

    return self.__eq__(reloaded, suppress_routine_paths=True)

package

package(pkg_fp)
Source code in FoSpy/blocks/files.py
def package(self, pkg_fp):
    import shutil
    pkg_fp = Path(pkg_fp)
    pkg_dir = self._temppath / "~package~"
    pkg_dir.mkdir(exist_ok=True)

    attachment_dir = pkg_dir / "attachments"
    attachment_dir.mkdir(exist_ok=True)

    copy = self.copy()

    for attachment in copy.find_attachments():
        attachment.path = "attachments"

    copy._sourceFile = pkg_dir / (pkg_fp.stem + ".fos")
    copy.refresh_attachments(new_copy=True, overwrite=False)
    copy.save()

    pkg_fp = pkg_fp.parent / pkg_fp.stem
    fosx_fp = pkg_fp.with_suffix(".fosx")
    zip_fp = Path(shutil.make_archive(pkg_fp, "zip", pkg_dir))

    if fosx_fp.exists():
        fosx_fp.unlink()

    zip_fp.rename(fosx_fp)

    shutil.rmtree(pkg_dir)

save

save(filepath=None, json_indent=4, **kwargs)

Sends a serialized dict to be written to file.

Parameters:

Name Type Description Default
filepath str

If specified, writes serialized dict to filepath. ks to self._sourceFile.

None
json_indent

Indent to use for json.dump when saving as json

4
**kwargs

Optional kwargs to pass to saving routine (unique to each file extension)

{}

Raises:

Type Description
ValueError

If _sourceFile is not specified (if FileBlock was copied from another object or constructed directly from a blockDict), filepath must be specified.

Source code in FoSpy/blocks/files.py
def save(self, filepath:str=None, json_indent=4, **kwargs):
    """
    Sends a serialized dict to be written to file.

    Args:
        filepath:
            If specified, writes serialized dict to filepath. ks to `self._sourceFile`.
        json_indent:
            Indent to use for json.dump when saving as json
        **kwargs:
            Optional kwargs to pass to saving routine (unique to each file extension)

    Raises:
        ValueError:
            If _sourceFile is not specified (if `FileBlock` was copied from
            another object or constructed directly from a blockDict),
            filepath must be specified.
    """
    from warnings import warn
    saving_as = filepath is not None
    try:
        if not saving_as:
            if self._sourceFile is None:
                raise ValueError("Synthesis object was constructed without a sourceFile. A save destination must be specified.")
            else:
                filepath = self._sourceFile
        self._sourceFile = os.path.abspath(filepath)

        filepath = self._sourceFile if self._ext_file is None else self._ext_file
        if str(filepath).endswith(".fosx"):
            return self.package(filepath)
        self.refresh_attachments()
        pathstr = str(filepath)
        try:
            ext = pathstr.lower().split(".")[-1]
        except IndexError:
            raise ValueError(f"Could not determine extension for filepath: {pathstr}")


        ext = str(filepath).lower().split(".")[-1]

        if ext not in EXT_WRITE_MAP:
            raise ValueError(f"Unrecognized file extension '{ext}'. Supported extensions are: {list(EXT_WRITE_MAP.keys())}")

        blockDict = self.serialize(clean="fos" not in ext)

        EXT_WRITE_MAP[ext](blockDict, filepath, json_indent=json_indent, **kwargs)

    except Exception as e:
        if not saving_as:
            warn(f"Could not save file. Disconnected from source file for safety. Exception: {e}", RuntimeWarning)
            self._sourceFile = None
            return e
        else:
            raise e
    return True

SingleBlock

Bases: Block

Represents a single block of key:value pairs parsed from a FOS file.

Subclasses are mapped to expected keys and validation routines in ..parsing.validation. Expected values are validated and assigned to public attributes. Unexpected values are assigned to attributes of self.ext for safety, but can still be accessed as an attribute of the SingleBlock object if not overwritten

Notable Subclasses: FileBlock(SingleBlock) Synthesis(FileBlock) Reaction(SingleBlock) Material(SingleBlock) TemplateBlock(SingleBlock)

Source code in FoSpy/blocks/blocks.py
 236
 237
 238
 239
 240
 241
 242
 243
 244
 245
 246
 247
 248
 249
 250
 251
 252
 253
 254
 255
 256
 257
 258
 259
 260
 261
 262
 263
 264
 265
 266
 267
 268
 269
 270
 271
 272
 273
 274
 275
 276
 277
 278
 279
 280
 281
 282
 283
 284
 285
 286
 287
 288
 289
 290
 291
 292
 293
 294
 295
 296
 297
 298
 299
 300
 301
 302
 303
 304
 305
 306
 307
 308
 309
 310
 311
 312
 313
 314
 315
 316
 317
 318
 319
 320
 321
 322
 323
 324
 325
 326
 327
 328
 329
 330
 331
 332
 333
 334
 335
 336
 337
 338
 339
 340
 341
 342
 343
 344
 345
 346
 347
 348
 349
 350
 351
 352
 353
 354
 355
 356
 357
 358
 359
 360
 361
 362
 363
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
class SingleBlock(Block):
    """
    Represents a single block of key:value pairs parsed from a FOS file.

    Subclasses are mapped to expected keys and validation routines in
    [`..parsing.validation`][FoSpy.parsing.validation]. Expected values are validated and assigned to
    public attributes. Unexpected values are assigned to attributes of
    `self.ext` for safety, but can still be accessed as an attribute of the
    SingleBlock object if not overwritten

    Notable Subclasses:
    [`FileBlock(SingleBlock)`][FoSpy.blocks.files.FileBlock]
    [`Synthesis(FileBlock)`][FoSpy.blocks.synthesis.Synthesis]
    [`Reaction(SingleBlock)`][FoSpy.blocks.metadata.Reaction]
    [`Material(SingleBlock)`][FoSpy.blocks.materials.Material]
    [`TemplateBlock(SingleBlock)`][FoSpy.blocks.template.TemplateBlock]
    """
    dispatch = {}
    dispatch_key = None
    dispatch_default = None
    dispatch_allow_self = True
    _aliases = None
    _id_key = None

    @classmethod
    def print_summary(cls, mode="cli"):
        from .._docs.properties import get_summary

        print(get_summary(cls, mode=mode))

    @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)


    @classmethod
    def reflex(cls, serialize=True, include_temp_names=True, clean=False, **kwargs):
        """

        """
        kwargs.setdefault("template_name", f"Reflexed {cls.__name__}")

        reflexor = cls.TemplateClass()

        empty = reflexor(kwargs)

        if not serialize:
            return empty

        serial = empty.serialize(clean=clean)

        if include_temp_names:
            return serial

        from ._blockUtils import _prune_template_names
        serial = _prune_template_names(serial)
        return serial

# class SingleBlock(Block):
    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}")

    @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

    @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

    @classmethod
    def dispatch_subclass(cls, *args, **kwargs):
        # fallback.
        # overridden by setup_dispatch decorator
        return cls

    @classmethod
    def inject_defaults(cls, blockDict, *args, **kwargs):
        # fallback.
        # overridden by setup_dispatch decorator
        return blockDict

    @classmethod
    def add_dispatch(cls, blockDict, dispatch_key, _wrapped=False, **kwargs):
        if _wrapped:
            # subclasses may want to mutate blockDict
            return {}

        raise Exception("add_dispatch should only be called by a decorated dispatch_subclass method.")

    @classmethod
    def register_dispatch(cls, registry_val,
                          from_parent:type[BlockType]=None,
                          setup_from_key=None,
                          setup_allow_self=True,
                          inherit_dispatch=False,
                          defaults:dict=None):
        """
        Decorate a subclass to be dispatched from a parent class.

        The subclass will also be decorated with the parent's
        [setup_dispatch][FoSpy.blocks.blocks.SingleBlock.setup_dispatch] to set
        up its own dispatch parameters.

        Args:
            registry_val:
                The value to be found in a dispatched blockDict which will be
                mapped to the subclass in the parent class's registry.

            from_parent:
                The parent class to register the subclass to. Defaults to the
                class which calls the method, but may be overridden if a
                non-SingleBlock abstract class is the parent.

            setup_from_key:
                Passed as from_key to the parents setup_dispatch decorator.

            setup_allow_self:
                Passed as allow_self to the parent's setup_dispatch decorator.

            inherit_dispatch:
                Whether the decorated class should inherit its parent's
                `add_dispatch` method

            defaults:
                Default values that should be injected into the blockDict when
                trying to guarantee dispatching to the registered class.
                Defaults to {}. If parent's from_key is public, defaults will be
                updated with `{from_key: registry_val}`
        """
        setup = {
            "from_key": setup_from_key,
            "allow_self": setup_allow_self,
            "_defaults": defaults or {}
        }
        from_parent = from_parent or cls
        def decorator(subcls, _val=registry_val, _p=from_parent, _cls=cls, _s=setup):
            if "__dispatch__" not in _p.__dict__:
                raise Exception("You cannot register a subclass to a class that "
                f"hasn't been decorated with @setup_dispatch() or @register_dispatch() ({_p.__name__})")

            dispatch_key = _p.__dispatch__["from_key"]
            if not dispatch_key.startswith("_"):
                _s["_defaults"][dispatch_key] = _val


            _s["_dispatch_from"] = _p.__dispatch__["dispatch_from"]

            subcls = cls.setup_dispatch(subcls, **_s)
            registry = _p.__dispatch__["registry"]

            registry[registry_val] = subcls

            if not inherit_dispatch and "add_dispatch" not in subcls.__dict__:
                subcls.add_dispatch = SingleBlock.add_dispatch

            return subcls
        return decorator



    @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


    @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

    @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

    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

    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())

    def __init__(self, blockDict:dict, *args, **kwargs):
        """
        Constructs a SingleBlock object from a dictionary or another
        SingleBlock.

        [Before construction][FoSpy.blocks.blocks.SingleBlock.__new__], the
        `blockDict` is unwrapped into a deep-copied dictionary and scanned to
        determine which subclass should be constructed.

        SingleBlocks are constructed recursively from an arbitrarily nested
        dictionary. After popping the top-level [`rename`
        key][FoSpy.blocks.metadata.Rename], to see if any expected properties
        have been renamed, all keys identified by
        [`get_req_validators`][FoSpy.blocks.blocks.SingleBlock.get_req_validators]
        must be in the top-level. Validation of all properties is delegated to
        [setattr][FoSpy.blocks.blocks.SingleBlock.__setattr__].

        Args:
            blockDict:
                An arbitrarily nested dictionary mapping attribute names to
                values.  

                It is possible to pass a blockDict already containing FoSpy objects,
                but validation routines may fail if objects are not the correct
                type. Best practice is to serialize all nested objects into
                lists, dicts, and strings to allow full type coersion.

        Raises:
            ValueError:
                A key required by `build_req_validators()` is not
                present.
            TypeError:
                The value passed as `blockDict` was not able to be unwrapped
                into a dict, either by serialization of a passed `Block` object
                or by list index.

        """
        from .metadata import Rename, MetaData
        from ._blockUtils import _unwrap_block
        self._staged_templates = {}
        self._constructed = False

        self.track_attachments(**cfg.track_attachments())


        from ..parsing.validation import aliases as als
        new_als = als.copy()
        new_als.update(self._aliases or {})
        self._aliases = new_als
        self._reserved = ['ext']

        blockDict = _unwrap_block(blockDict)

        self._sourceDict = blockDict.copy()

        rename = blockDict.pop("rename", {})
        if getattr(self,"allow_rename",True):
            self.rename=rename

        req = self.get_req_validators()
        req.pop("ext",None)

        property_errors = [
            err.MissingPropertyError(key, self, blockDict=blockDict)
            for key in req if key not in blockDict
        ]

        self._meta = SubContainer()
        self._calc_comments = {}
        self._calc_routines = []
        self._key_overrides = {}

        for attr, key in mk.items():
            try:
                k = md[key].copy()
            except AttributeError:
                k = md[key]
            setattr(self._meta, attr, blockDict.pop(key,k))

        self._key_order = []
        self.ext = SubContainer()

        for key, val in blockDict.items():
            if key.startswith("_"):
                continue

            self._key_order.append(key)
            try:
                setattr(self, key, val)
            except err.PropertyError as e:
                property_errors.append(e)

        if property_errors:
            raise err.PropertyErrorGroup(self, blockDict=blockDict, errors=property_errors)

        self._constructed = True


    def _update_src(self):
        if self._constructed and self._props_changed:
            self._sourceDict = self.serialize(clean=True)
            self._props_changed = False

        return self._sourceDict   

    def __setattr__(self, name:str, value):
        """
        Assign an attribute with validation and controlled namespace behavior.

        Contract:
            `SingleBlock` enforces type correctness and validator execution for
            all public attributes defined for its subclass.

            Required types and validators are mapped by subclass in
            [`parsing.validation`][FoSpy.parsing.validation]

            Comment-mutating methods are attached to every object assigned to an
            attribute. (See
            [`add_comments`][FoSpy.blocks.blocks._add_comments_to_parent])

        Rules:
            1. Private attributes (`_`-prefixed) bypass validation.
            2. Attributes with registered validators are processed through the
            validator before assignment.
            3. Attributes with required types are coerced by calling the type
            constructor when necessary.
            4. Unrecognized attributes can be assigned to a validator mapped to
            an alias in [`parsing.validation`][FoSpy.parsing.validation], using
            the syntax `name="name$alias"`.
            5. Unrecognized attribute names are redirected as attributes of
            `self.ext`.

        Raises:
            ValueError:
                - If a required `SingleBlock` is passed as a list with length > 1.
                - If a block alias cannot be parsed from a key containing `$`.
                - If a block alias is unrecognized.
                - If a template field is found when constructing a non-template
                  subclass.
        """
        if name.startswith("_") or name in getattr(self, "_reserved", []):
            return super().__setattr__(name, value)

        from ..parsing.format_fos import format_field
        from .template import TemplateField, TemplateBlock

        validators = self.get_validators()

        if "$" in name:
            try:
                name, alias = name.split("$")
            except Exception:
                self._update_src()
                raise err.PropertyAliasError(name, self, blockDict=self._update_src(), hint="Unable to parse a block alias from key containing '$' character: ")

            try:
                val = self._aliases[alias]
            except KeyError:
                self._update_src()
                raise err.PropertyAliasError(name, self, blockDict=self._update_src(),
                                             hint=f"Unrecognized block alias: '{alias}' assigned to property: ",
                                             posthint=f"Valid aliases: {list(self._aliases.keys())}")

            if name in validators and val != validators[name]:
                self._update_src()
                raise err.PropertyAliasError(name, self, blockDict=self._update_src(),
                                             hint=f"Cannot apply '{alias}' alias override to property: ",
                                             posthint="That property is already assigned to a validator. "
                                             f"The following properties are reserved: {list(validators.keys())}")
            if name not in validators:
                validators[name] = val
                self._key_overrides[name] = val

        if name == "treatments":
            pass # debugging

        if name in validators:
            universal_val = self.universal_val

            validator = validators[name]
            # val_kwargs = {}
            # for kw, arg_func in (("sourceDict", self._update_src),("cls", lambda:type(self))):
            #     try:
            #         if kw in sign(validator).parameters:
            #             val_kwargs[kw] = arg_func()
            #     #TODO: find a better way to handle this
            #     except Exception:
            #         pass

            val_kwargs = {
                "sourceDict": self._update_src(),
                "blk_cls": type(self),
                "prop_name": name
            }

            value = universal_val(value, **val_kwargs)

            if isinstance(validator, type):
                if issubclass(validator, Block):
                    val_kwargs = {}

                elif str(value) == TemplateField.serialize() and not issubclass(validator, TemplateField):      
                    e = ValueError("Template field detected for a non-templated property.")
                    raise err.FailedValidatorError(name, self, e, blockDict=self._update_src(), hint="Template field passed to non-templated property: ")
                if isinstance(validator, type) and isinstance(value, validator):
                    return self._assign_and_inject(name, value)

            try:
                sig = inspect.signature(validator)
                needs_kwargs = any(
                    p.kind==p.VAR_KEYWORD for p in sig.parameters.values()
                )
            except Exception:
                needs_kwargs = False

            try:  
                val = validator(value, **val_kwargs) if needs_kwargs else validator(value)
            except Exception as e:
                raise err.FailedValidatorError(name, self, e, blockDict=self._update_src())

            return self._assign_and_inject(name, val)
        else:
            return self._assign_and_inject(name, value, extended=True)

    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}."
            )

    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

    def __hash__(self):
        return id(self)

    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

    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

    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

    def rename_dict(self):
        if not hasattr(self, "rename"):
            return {}
        return self.rename.serialize(shallow=True, clean=True)

    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

    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

    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

    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


    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)

    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)

    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

    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)

    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

    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)

    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

    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)

    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

    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)

    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

    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")

    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()

    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)

    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()

    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)

    def clear_comments(self):
        """
        Clear comments attached to top-level attributes only.
        """
        self._meta.comments = {}

    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)

    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)

    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()

    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)

    def find_attachments(self):
        attachments = []
        for propVal in self.get_prop_dict().values():
            if isinstance(propVal, ListBlock):
                for obj in propVal._objs:
                    attachments.extend([
                        o for o in obj.find_attachments()
                        if o not in attachments
                    ])
            elif isinstance(propVal, SingleBlock):
                attachments.extend([
                    o for o in propVal.find_attachments()
                    if o not in attachments
                ])
        return attachments

_aliases class-attribute instance-attribute

_aliases = new_als

_calc_comments instance-attribute

_calc_comments = {}

_calc_routines instance-attribute

_calc_routines = []

_constructed instance-attribute

_constructed = True

_id_key class-attribute instance-attribute

_id_key = None

_key_order instance-attribute

_key_order = []

_key_overrides instance-attribute

_key_overrides = {}

_meta instance-attribute

_meta = SubContainer()

_reserved instance-attribute

_reserved = ['ext']

_sourceDict instance-attribute

_sourceDict = blockDict.copy()

_staged_templates instance-attribute

_staged_templates = {}

dispatch class-attribute instance-attribute

dispatch = {}

dispatch_allow_self class-attribute instance-attribute

dispatch_allow_self = True

dispatch_default class-attribute instance-attribute

dispatch_default = None

dispatch_key class-attribute instance-attribute

dispatch_key = None

ext instance-attribute

ext = SubContainer()

rename instance-attribute

rename = rename

TemplateClass classmethod

TemplateClass(*args)

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__

__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__

__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__

__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__

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

__init__

__init__(blockDict, *args, **kwargs)

Constructs a SingleBlock object from a dictionary or another SingleBlock.

Before construction, the blockDict is unwrapped into a deep-copied dictionary and scanned to determine which subclass should be constructed.

SingleBlocks are constructed recursively from an arbitrarily nested dictionary. After popping the top-level rename key, to see if any expected properties have been renamed, all keys identified by get_req_validators must be in the top-level. Validation of all properties is delegated to setattr.

Parameters:

Name Type Description Default
blockDict dict

An arbitrarily nested dictionary mapping attribute names to values.

It is possible to pass a blockDict already containing FoSpy objects, but validation routines may fail if objects are not the correct type. Best practice is to serialize all nested objects into lists, dicts, and strings to allow full type coersion.

required

Raises:

Type Description
ValueError

A key required by build_req_validators() is not present.

TypeError

The value passed as blockDict was not able to be unwrapped into a dict, either by serialization of a passed Block object or by list index.

Source code in FoSpy/blocks/blocks.py
def __init__(self, blockDict:dict, *args, **kwargs):
    """
    Constructs a SingleBlock object from a dictionary or another
    SingleBlock.

    [Before construction][FoSpy.blocks.blocks.SingleBlock.__new__], the
    `blockDict` is unwrapped into a deep-copied dictionary and scanned to
    determine which subclass should be constructed.

    SingleBlocks are constructed recursively from an arbitrarily nested
    dictionary. After popping the top-level [`rename`
    key][FoSpy.blocks.metadata.Rename], to see if any expected properties
    have been renamed, all keys identified by
    [`get_req_validators`][FoSpy.blocks.blocks.SingleBlock.get_req_validators]
    must be in the top-level. Validation of all properties is delegated to
    [setattr][FoSpy.blocks.blocks.SingleBlock.__setattr__].

    Args:
        blockDict:
            An arbitrarily nested dictionary mapping attribute names to
            values.  

            It is possible to pass a blockDict already containing FoSpy objects,
            but validation routines may fail if objects are not the correct
            type. Best practice is to serialize all nested objects into
            lists, dicts, and strings to allow full type coersion.

    Raises:
        ValueError:
            A key required by `build_req_validators()` is not
            present.
        TypeError:
            The value passed as `blockDict` was not able to be unwrapped
            into a dict, either by serialization of a passed `Block` object
            or by list index.

    """
    from .metadata import Rename, MetaData
    from ._blockUtils import _unwrap_block
    self._staged_templates = {}
    self._constructed = False

    self.track_attachments(**cfg.track_attachments())


    from ..parsing.validation import aliases as als
    new_als = als.copy()
    new_als.update(self._aliases or {})
    self._aliases = new_als
    self._reserved = ['ext']

    blockDict = _unwrap_block(blockDict)

    self._sourceDict = blockDict.copy()

    rename = blockDict.pop("rename", {})
    if getattr(self,"allow_rename",True):
        self.rename=rename

    req = self.get_req_validators()
    req.pop("ext",None)

    property_errors = [
        err.MissingPropertyError(key, self, blockDict=blockDict)
        for key in req if key not in blockDict
    ]

    self._meta = SubContainer()
    self._calc_comments = {}
    self._calc_routines = []
    self._key_overrides = {}

    for attr, key in mk.items():
        try:
            k = md[key].copy()
        except AttributeError:
            k = md[key]
        setattr(self._meta, attr, blockDict.pop(key,k))

    self._key_order = []
    self.ext = SubContainer()

    for key, val in blockDict.items():
        if key.startswith("_"):
            continue

        self._key_order.append(key)
        try:
            setattr(self, key, val)
        except err.PropertyError as e:
            property_errors.append(e)

    if property_errors:
        raise err.PropertyErrorGroup(self, blockDict=blockDict, errors=property_errors)

    self._constructed = True

__new__

__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__

__setattr__(name, value)

Assign an attribute with validation and controlled namespace behavior.

Contract

SingleBlock enforces type correctness and validator execution for all public attributes defined for its subclass.

Required types and validators are mapped by subclass in parsing.validation

Comment-mutating methods are attached to every object assigned to an attribute. (See add_comments)

Rules
  1. Private attributes (_-prefixed) bypass validation.
  2. Attributes with registered validators are processed through the validator before assignment.
  3. Attributes with required types are coerced by calling the type constructor when necessary.
  4. Unrecognized attributes can be assigned to a validator mapped to an alias in parsing.validation, using the syntax name="name$alias".
  5. Unrecognized attribute names are redirected as attributes of self.ext.

Raises:

Type Description
ValueError
  • If a required SingleBlock is passed as a list with length > 1.
  • If a block alias cannot be parsed from a key containing $.
  • If a block alias is unrecognized.
  • If a template field is found when constructing a non-template subclass.
Source code in FoSpy/blocks/blocks.py
def __setattr__(self, name:str, value):
    """
    Assign an attribute with validation and controlled namespace behavior.

    Contract:
        `SingleBlock` enforces type correctness and validator execution for
        all public attributes defined for its subclass.

        Required types and validators are mapped by subclass in
        [`parsing.validation`][FoSpy.parsing.validation]

        Comment-mutating methods are attached to every object assigned to an
        attribute. (See
        [`add_comments`][FoSpy.blocks.blocks._add_comments_to_parent])

    Rules:
        1. Private attributes (`_`-prefixed) bypass validation.
        2. Attributes with registered validators are processed through the
        validator before assignment.
        3. Attributes with required types are coerced by calling the type
        constructor when necessary.
        4. Unrecognized attributes can be assigned to a validator mapped to
        an alias in [`parsing.validation`][FoSpy.parsing.validation], using
        the syntax `name="name$alias"`.
        5. Unrecognized attribute names are redirected as attributes of
        `self.ext`.

    Raises:
        ValueError:
            - If a required `SingleBlock` is passed as a list with length > 1.
            - If a block alias cannot be parsed from a key containing `$`.
            - If a block alias is unrecognized.
            - If a template field is found when constructing a non-template
              subclass.
    """
    if name.startswith("_") or name in getattr(self, "_reserved", []):
        return super().__setattr__(name, value)

    from ..parsing.format_fos import format_field
    from .template import TemplateField, TemplateBlock

    validators = self.get_validators()

    if "$" in name:
        try:
            name, alias = name.split("$")
        except Exception:
            self._update_src()
            raise err.PropertyAliasError(name, self, blockDict=self._update_src(), hint="Unable to parse a block alias from key containing '$' character: ")

        try:
            val = self._aliases[alias]
        except KeyError:
            self._update_src()
            raise err.PropertyAliasError(name, self, blockDict=self._update_src(),
                                         hint=f"Unrecognized block alias: '{alias}' assigned to property: ",
                                         posthint=f"Valid aliases: {list(self._aliases.keys())}")

        if name in validators and val != validators[name]:
            self._update_src()
            raise err.PropertyAliasError(name, self, blockDict=self._update_src(),
                                         hint=f"Cannot apply '{alias}' alias override to property: ",
                                         posthint="That property is already assigned to a validator. "
                                         f"The following properties are reserved: {list(validators.keys())}")
        if name not in validators:
            validators[name] = val
            self._key_overrides[name] = val

    if name == "treatments":
        pass # debugging

    if name in validators:
        universal_val = self.universal_val

        validator = validators[name]
        # val_kwargs = {}
        # for kw, arg_func in (("sourceDict", self._update_src),("cls", lambda:type(self))):
        #     try:
        #         if kw in sign(validator).parameters:
        #             val_kwargs[kw] = arg_func()
        #     #TODO: find a better way to handle this
        #     except Exception:
        #         pass

        val_kwargs = {
            "sourceDict": self._update_src(),
            "blk_cls": type(self),
            "prop_name": name
        }

        value = universal_val(value, **val_kwargs)

        if isinstance(validator, type):
            if issubclass(validator, Block):
                val_kwargs = {}

            elif str(value) == TemplateField.serialize() and not issubclass(validator, TemplateField):      
                e = ValueError("Template field detected for a non-templated property.")
                raise err.FailedValidatorError(name, self, e, blockDict=self._update_src(), hint="Template field passed to non-templated property: ")
            if isinstance(validator, type) and isinstance(value, validator):
                return self._assign_and_inject(name, value)

        try:
            sig = inspect.signature(validator)
            needs_kwargs = any(
                p.kind==p.VAR_KEYWORD for p in sig.parameters.values()
            )
        except Exception:
            needs_kwargs = False

        try:  
            val = validator(value, **val_kwargs) if needs_kwargs else validator(value)
        except Exception as e:
            raise err.FailedValidatorError(name, self, e, blockDict=self._update_src())

        return self._assign_and_inject(name, val)
    else:
        return self._assign_and_inject(name, value, extended=True)

_assign_and_inject

_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

_meta_to_front

_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

_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

_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

_update_src

_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   

add_all_calc_routines

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

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

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

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

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 classmethod

add_dispatch(
    blockDict, dispatch_key, _wrapped=False, **kwargs
)
Source code in FoSpy/blocks/blocks.py
@classmethod
def add_dispatch(cls, blockDict, dispatch_key, _wrapped=False, **kwargs):
    if _wrapped:
        # subclasses may want to mutate blockDict
        return {}

    raise Exception("add_dispatch should only be called by a decorated dispatch_subclass method.")

build_req_validators classmethod

build_req_validators()

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

build_validators()

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

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()

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

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

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 classmethod

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

dispatcher staticmethod

dispatcher(dispatch_method)

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

fill_staged_template

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

find_attachments()
Source code in FoSpy/blocks/blocks.py
def find_attachments(self):
    attachments = []
    for propVal in self.get_prop_dict().values():
        if isinstance(propVal, ListBlock):
            for obj in propVal._objs:
                attachments.extend([
                    o for o in obj.find_attachments()
                    if o not in attachments
                ])
        elif isinstance(propVal, SingleBlock):
            attachments.extend([
                o for o in propVal.find_attachments()
                if o not in attachments
            ])
    return attachments

get_id

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_prop_dict

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_req_validators

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

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

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 classmethod

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

key_to_idx

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

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

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

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

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 classmethod

print_summary(mode='cli')
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 classmethod

reflex(
    serialize=True,
    include_temp_names=True,
    clean=False,
    **kwargs
)
Source code in FoSpy/blocks/blocks.py
@classmethod
def reflex(cls, serialize=True, include_temp_names=True, clean=False, **kwargs):
    """

    """
    kwargs.setdefault("template_name", f"Reflexed {cls.__name__}")

    reflexor = cls.TemplateClass()

    empty = reflexor(kwargs)

    if not serialize:
        return empty

    serial = empty.serialize(clean=clean)

    if include_temp_names:
        return serial

    from ._blockUtils import _prune_template_names
    serial = _prune_template_names(serial)
    return serial

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 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 classmethod

register_dispatch(
    registry_val,
    from_parent=None,
    setup_from_key=None,
    setup_allow_self=True,
    inherit_dispatch=False,
    defaults=None,
)

Decorate a subclass to be dispatched from a parent class.

The subclass will also be decorated with the parent's setup_dispatch to set up its own dispatch parameters.

Parameters:

Name Type Description Default
registry_val

The value to be found in a dispatched blockDict which will be mapped to the subclass in the parent class's registry.

required
from_parent type[BlockType]

The parent class to register the subclass to. Defaults to the class which calls the method, but may be overridden if a non-SingleBlock abstract class is the parent.

None
setup_from_key

Passed as from_key to the parents setup_dispatch decorator.

None
setup_allow_self

Passed as allow_self to the parent's setup_dispatch decorator.

True
inherit_dispatch

Whether the decorated class should inherit its parent's add_dispatch method

False
defaults dict

Default values that should be injected into the blockDict when trying to guarantee dispatching to the registered class. Defaults to {}. If parent's from_key is public, defaults will be updated with {from_key: registry_val}

None
Source code in FoSpy/blocks/blocks.py
@classmethod
def register_dispatch(cls, registry_val,
                      from_parent:type[BlockType]=None,
                      setup_from_key=None,
                      setup_allow_self=True,
                      inherit_dispatch=False,
                      defaults:dict=None):
    """
    Decorate a subclass to be dispatched from a parent class.

    The subclass will also be decorated with the parent's
    [setup_dispatch][FoSpy.blocks.blocks.SingleBlock.setup_dispatch] to set
    up its own dispatch parameters.

    Args:
        registry_val:
            The value to be found in a dispatched blockDict which will be
            mapped to the subclass in the parent class's registry.

        from_parent:
            The parent class to register the subclass to. Defaults to the
            class which calls the method, but may be overridden if a
            non-SingleBlock abstract class is the parent.

        setup_from_key:
            Passed as from_key to the parents setup_dispatch decorator.

        setup_allow_self:
            Passed as allow_self to the parent's setup_dispatch decorator.

        inherit_dispatch:
            Whether the decorated class should inherit its parent's
            `add_dispatch` method

        defaults:
            Default values that should be injected into the blockDict when
            trying to guarantee dispatching to the registered class.
            Defaults to {}. If parent's from_key is public, defaults will be
            updated with `{from_key: registry_val}`
    """
    setup = {
        "from_key": setup_from_key,
        "allow_self": setup_allow_self,
        "_defaults": defaults or {}
    }
    from_parent = from_parent or cls
    def decorator(subcls, _val=registry_val, _p=from_parent, _cls=cls, _s=setup):
        if "__dispatch__" not in _p.__dict__:
            raise Exception("You cannot register a subclass to a class that "
            f"hasn't been decorated with @setup_dispatch() or @register_dispatch() ({_p.__name__})")

        dispatch_key = _p.__dispatch__["from_key"]
        if not dispatch_key.startswith("_"):
            _s["_defaults"][dispatch_key] = _val


        _s["_dispatch_from"] = _p.__dispatch__["dispatch_from"]

        subcls = cls.setup_dispatch(subcls, **_s)
        registry = _p.__dispatch__["registry"]

        registry[registry_val] = subcls

        if not inherit_dispatch and "add_dispatch" not in subcls.__dict__:
            subcls.add_dispatch = SingleBlock.add_dispatch

        return subcls
    return decorator

rename_block

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

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

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 classmethod

set_dispatch(
    value=None,
    from_parent=None,
    from_key=None,
    allow_self=None,
)
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 staticmethod

setup_dispatch(
    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 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

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

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)

_clear_path_contents

_clear_path_contents(path)
Source code in FoSpy/blocks/files.py
def _clear_path_contents(path:Path):
    if not path.exists():
        return

    if not path.is_dir():
        raise NotADirectoryError(path)

    for item in path.iterdir():
        try:
            if item.is_dir() and not item.is_symlink():
                shutil.rmtree(item)
        except Exception as e:
            raise Exception("Failed to remove directory") from e

_get_ext_dir

_get_ext_dir(fosx_path, ext_dir=None)
Source code in FoSpy/blocks/files.py
def _get_ext_dir(fosx_path, ext_dir=None):
    val_path = Path(fosx_path)
    parent = val_path.parent
    i = 0
    if ext_dir is not None:
        cleanup = False
        ext_dir = Path(ext_dir)/ f"{val_path.stem}_extracted"
    else:
        cleanup = True
    while (ext_dir is None or 
        (str(ext_dir) in temp_dirs and ext_dir.exists())):
        i += 1
        stem = f"~.{i}{val_path.stem}"
        ext_dir = parent / stem

    ext_dir.mkdir(exist_ok=True)
    if cleanup:
        _clear_path_contents(ext_dir)
        temp_dirs.append(str(ext_dir))
        atexit.register(lambda d=ext_dir: shutil.rmtree(d))

    fos_path = ext_dir / (val_path.stem + ".fos")

    return ext_dir, fos_path

dict_from_file

dict_from_file(filepath)
Source code in FoSpy/parsing/read.py
def dict_from_file(filepath):
    block = None
    blocks = {}
    comments = {}
    current_block = "metadata"
    current_type = "single"
    embedding = False
    multiline = False
    break_line = None
    break_num = 0
    with open(filepath, "r", encoding="utf-8") as f:
        endComments = []
        for line in f:
            break_num += 1
            if rx.EMBEDDED_END.match(line):
                embedding = False
                block.append(line)
                continue

            if embedding:
                if rx.EMBEDDED_START.match(line):
                    break_line = line
                    break
                block.append(line)
                continue

            txt = line.strip()     
            if rx.CALC_COMMENT_LINE.match(txt) or (
                txt == "" and not multiline
            ):
                continue

            if txt.endswith(";;;"):
                multiline = True

            if multiline and SYNTAX["nested"]["close"] in txt:
                multiline = False

            if rx.EMBEDDED_START.match(txt):
                _debug.msg(f"Starting Embedding on line: {txt}")
                embedding = True

            # block = blocks.get(current_block)
            # if block is None:
            #     blocks[current_block] = (current_type,[])
            #     block = blocks[current_block][1]
            # else:
            #     block = block[1]

            blocks.setdefault(current_block, (current_type,[]))
            block = blocks[current_block][1]

            m = rx.BLOCK_HEADER.match(txt)
            if m:
                name_found = False
                for regex_name, typ in zip(("list_name", "single_name"),("list","single")):
                    if m.group(regex_name) is not None:
                        current_block = m.group(regex_name).lower()
                        current_type = typ
                        name_found = True
                        break
                if not name_found:
                    raise SyntaxError(f"Line: '{txt}' was identified as a block header but no header name could be identified")

                #_debug.msg(f"Identified {current_type} block header in line: {txt}")
                #_debug.msg(f"Block name: {current_block}")

                comments[current_block] = [rx.COMMENT_LINE.match(ln).group("text").lstrip() for ln in endComments]
                endComments = []
                continue

            if rx.COMMENT_LINE.match(txt):
                endComments.append(txt)
                continue

            for ln in [*endComments,txt]:
                block.append(ln.strip())
            endComments = []
    if embedding:
        raise SyntaxError(
            "An embedded document was never closed before the end of the file or starting a new embed.\n"
            + f"Current Line (line #{break_num}): "
            + ("<end of FOS file>\n" if not break_line else break_line)
            + "Ensure that there are spaces between '#', 'END FOS EMBED', and '}}}' when ending an "
            + "embedded document."
        )

    for block, (typ, lines) in blocks.items():
        if typ == "single":
            blockDict = create_single_block_dict(lines)
            if blockDict == {}:
                blocks[block] = []
            else:
                blocks[block] = [blockDict]
        elif typ == "list":
            blocks[block] = create_list_block_dict(lines)
        else:
            raise ValueError(f"Unrecognized block type: '{typ}', expected either single or list")
    blocks[mk["comments"]] = comments

    for meta_key in mk.values():
        if meta_key not in blocks:
            try:
                blocks[meta_key] = md[meta_key].copy()
            except AttributeError:
                blocks[meta_key] = md[meta_key]

    return blocks

open_fosx

open_fosx(fosx_path, ext_dir=None, **kwargs)
Source code in FoSpy/blocks/files.py
def open_fosx(fosx_path, ext_dir=None, **kwargs):
    returning_path = ext_dir is not None

    ext_dir, fos_path = _get_ext_dir(fosx_path, ext_dir=ext_dir)

    shutil.unpack_archive(
        filename=fosx_path,
        extract_dir=ext_dir,
        format="zip"
    )

    if returning_path:
        return fos_path

    load = FileBlock.fromFile(fos_path)

    serial = load.serialize()
    serial["_sourceFile"] = fos_path
    return serial

save_fosx

save_fosx(blockDict, filepath, **kwargs)
Source code in FoSpy/blocks/files.py
def save_fosx(blockDict, filepath, **kwargs):
    tempdir = tempfile.TemporaryDirectory()
    atexit.register(tempdir.cleanup)

    tempdir = Path(tempdir.name)
    tempdir.mkdir(exist_ok=True)

    temp_path = tempdir / "~temp~.fos"
    write_dict_to_file(blockDict, temp_path, **kwargs)

    new_file = FileBlock.fromFile(temp_path)
    new_file.package(filepath)

write_dict_to_file

write_dict_to_file(blocks, filepath, **kwargs)
Source code in FoSpy/parsing/write.py
def write_dict_to_file(blocks, filepath, **kwargs):
    os.makedirs(os.path.dirname(filepath), exist_ok=True)
    blocks = blocks.copy()
    block_comments = blocks.pop(mk["comments"])

    meta = [blocks.pop("metadata",{})]

    with open(filepath, "w") as f:
        for line in block_list_to_lines(meta):
            f.write(f'{line}\n')
        for name, block in blocks.items():
            if isinstance(block,dict):
                block = [block]
            if name in mk.values():
                continue
            if name != "metadata":
                comments = block_comments.get(name,[])
                for comment in comments:
                    f.write(f'{fm.format_comment(comment)}\n')

                f.write(f'{fm.format_block_header(name, "list" if len(block)>1 else "single")}\n')           

            for line in block_list_to_lines(block):
                f.write(f'{line}\n')