Skip to content

Singleton Factory API

sweet_tea.singleton_factory.SingletonFactory

Bases: BaseFactory

Factory for registering and retrieving singleton instances.

This implements the service locator pattern where pre-configured instances can be registered once and retrieved multiple times. This is useful for:

  • Singleton services that should only exist once
  • Pre-configured database connections
  • Dependency injection containers
  • Caching expensive-to-create objects

Thread-safe: the cache is synchronized, and exactly one thread constructs the instance for a given key. The factory's lock is not held while a constructor runs — a constructor may itself resolve or create, and holding this lock across that inverted the lock order against the registry (SWE-22) — so construction is admitted by a per-key lock instead. Nor is it held while a configuration is judged for drift: that applies any declared configuration model, and a pydantic validator is arbitrary code on the same footing as a constructor (SWE-29).

Because a constructor may resolve or create, it may also — by mistake or by design — ask for a singleton whose construction it is already inside. That is a circular dependency, and this factory reports it as a :class:SweetTeaError naming the cycle rather than blocking on a lock that can never be released (SWE-36).

Source code in sweet_tea/singleton_factory.py
  39
  40
  41
  42
  43
  44
  45
  46
  47
  48
  49
  50
  51
  52
  53
  54
  55
  56
  57
  58
  59
  60
  61
  62
  63
  64
  65
  66
  67
  68
  69
  70
  71
  72
  73
  74
  75
  76
  77
  78
  79
  80
  81
  82
  83
  84
  85
  86
  87
  88
  89
  90
  91
  92
  93
  94
  95
  96
  97
  98
  99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
 128
 129
 130
 131
 132
 133
 134
 135
 136
 137
 138
 139
 140
 141
 142
 143
 144
 145
 146
 147
 148
 149
 150
 151
 152
 153
 154
 155
 156
 157
 158
 159
 160
 161
 162
 163
 164
 165
 166
 167
 168
 169
 170
 171
 172
 173
 174
 175
 176
 177
 178
 179
 180
 181
 182
 183
 184
 185
 186
 187
 188
 189
 190
 191
 192
 193
 194
 195
 196
 197
 198
 199
 200
 201
 202
 203
 204
 205
 206
 207
 208
 209
 210
 211
 212
 213
 214
 215
 216
 217
 218
 219
 220
 221
 222
 223
 224
 225
 226
 227
 228
 229
 230
 231
 232
 233
 234
 235
 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
class SingletonFactory(BaseFactory):
    """
    Factory for registering and retrieving singleton instances.

    This implements the service locator pattern where pre-configured instances
    can be registered once and retrieved multiple times. This is useful for:

    - Singleton services that should only exist once
    - Pre-configured database connections
    - Dependency injection containers
    - Caching expensive-to-create objects

    Thread-safe: the cache is synchronized, and exactly one thread constructs the
    instance for a given key. The factory's lock is **not** held while a constructor
    runs — a constructor may itself resolve or create, and holding this lock across
    that inverted the lock order against the registry (SWE-22) — so construction is
    admitted by a per-key lock instead. Nor is it held while a configuration is judged
    for drift: that applies any declared configuration model, and a pydantic validator
    is arbitrary code on the same footing as a constructor (SWE-29).

    Because a constructor may resolve or create, it may also — by mistake or by design
    — ask for a singleton whose construction it is already inside. That is a circular
    dependency, and this factory reports it as a :class:`SweetTeaError` naming the
    cycle rather than blocking on a lock that can never be released (SWE-36).
    """

    # Threading lock for synchronizing operations
    __lock = threading.RLock()

    # Cached instances, keyed by the identity of the registry entry they were built
    # from: (key, library, label). Keying on the resolved entry rather than on the
    # caller's spelling is what makes the singleton guarantee hold — every spelling
    # of one key resolves to the same entry and therefore the same slot (see SWE-5).
    __instances: Dict[tuple[str, str, str], Any] = {}

    # The configuration each cached instance was built from, under the same key. Kept
    # so a later call passing a different one can be told that it had no effect (see
    # SWE-14); the instance itself is never rebuilt.
    #
    # Stored as the keyword arguments construction actually used, deep-copied away from
    # the caller's objects before the constructor ran (see SWE-29). None means the
    # basis could not be computed, which leaves drift undecidable rather than judged.
    __configurations: Dict[tuple[str, str, str], dict[str, Any] | None] = {}

    # One lock per cache key, guarding construction of that key's instance. The
    # factory's own lock cannot do this job: holding it across a constructor runs
    # arbitrary code under it, and a constructor that looks something up is ordinary
    # dependency injection (SWE-22). These are created under __lock and held without
    # it, so exactly one thread constructs a given key while every other key and
    # every cache read proceeds unblocked.
    #
    # Never discarded, not even by clear(). A lock dropped while a thread held it was
    # replaced by a fresh one for the same key on the next call, which admitted a
    # second thread into a construction already running and produced two instances of
    # a singleton (SWE-39). They are keyed by resolved cache key and so bounded by the
    # registry, which is a small price for the guarantee the class exists to make.
    __construction_locks: Dict[tuple[str, str, str], threading.Lock] = {}

    # The keys each thread is constructing, outermost first. Thread-local, so nothing
    # guards it: a thread is the only reader and the only writer of its own stack.
    # This is what catches the single-threaded shapes of SWE-36 — a constructor that
    # reaches back for its own key, or for one further up its own chain — before the
    # thread blocks forever on a lock it is already holding.
    __construction_stack = threading.local()

    # Who is constructing what, and what each thread has stopped to wait for: the
    # construction lock's owner per key, as (thread id, thread name), and the key each
    # blocked thread is waiting to be admitted to. Together they are a wait-for graph,
    # which is the only way to see the shape of SWE-36 no single thread's own stack
    # can show — two threads entering one cycle from opposite ends. Both are written
    # and read under __lock, and that is also what keeps them consistent with each
    # other: a thread walking the graph sees the state before a lock changed hands or
    # the state after, never half of each.
    __construction_owner: Dict[tuple[str, str, str], tuple[int, str]] = {}
    __blocked_on: Dict[int, tuple[str, str, str]] = {}

    # What a construction has to still be true when it finishes, or its instance is not
    # cached: the number of clear() calls, and the number of times each key has been
    # popped. A construction reads both before it begins and they are compared under
    # the lock at the commit, so an instance built against the registry as it was
    # before a reset cannot install itself after one (SWE-45). Cancelling the
    # construction is not an option and holding the lock across it is forbidden (see
    # the class docstring), so the commit is the only place the race can be settled.
    #
    # Two counters rather than one, because one would make pop of a key disown an
    # in-flight construction of a *different* key — a warning about an instance nobody
    # reset, which is the kind of false positive that gets a whole warning category
    # filtered out. clear() empties the per-key counts instead of bumping each of them:
    # every token in flight already carries the old epoch and so loses regardless.
    #
    # pop() can only remove an instance that is cached, and nothing is cached for a key
    # while a thread holds its construction lock and has not committed, so pop cannot
    # in fact race a construction of the same key today. Its bump is there so that the
    # invariant is stated in both places that break it rather than only in the one
    # that can currently be observed.
    __clear_epoch: int = 0
    __removals: Dict[tuple[str, str, str], int] = {}

    # Stands in for the comparison basis of a configuration a declared
    # ``__configuration__`` will not accept. Not a basis, and not undecidable either: a
    # configuration that fails validation cannot be the one the cached instance was
    # built from, since that one validated. It drifted (see SWE-29).
    #
    # Only ever reached on a cache hit. A constructing call never gets this far with a
    # configuration the schema refuses: it raises on it, which is the stronger report.
    __REJECTED_BY_SCHEMA = object()

    # Logger instance
    _logger = logging.getLogger(__name__)

    @classmethod
    def create(
        cls,
        key: str,
        library: str = "",
        label: str = "",
        configuration: dict[str, Any] | BaseModel | None = None,
    ) -> Any:
        """
        Get an existing singleton instance or create a new one if it doesn't exist.

        This method provides lazy initialization - instances are created only when first requested.
        Subsequent calls with the same key will return the same cached instance.

        A :meth:`clear` or a :meth:`pop` of this key that lands while the constructor
        is running is honoured over this call: the instance is returned to the caller
        but not cached, with a warning, since an instance built against state that has
        since been reset must not install itself over the reset (SWE-45).

        Args:
            key: Name to reference the class from the registry.
            library: Optional library filter for the class.
            label: Optional label filter for the class.
            configuration: Configuration parameters as keyword arguments, given as a
                dict or a pydantic model whose fields are spread into the constructor.

        Returns:
            The existing singleton instance, or a newly created instance — registered,
            unless the factory was reset while it was being constructed.

        Raises:
            SweetTeaError: If the key is not found in the registry, filters don't
                match, or the key is already being constructed by the calling thread
                or by a thread this call would deadlock against (see SWE-36).
        """
        # Resolved before this factory's lock is taken. Resolution imports, and
        # taking both locks across an import is what let a module body that creates a
        # singleton deadlock against a singleton whose constructor looks one up
        # (SWE-22). The cache is still keyed on the resolved entry rather than on the
        # caller's spelling, which is what makes every spelling share one slot
        # (SWE-5).
        entry = cls._select_entry(cls._find_entries(key), key, library, label)
        cache_key = (entry.key, entry.library, entry.label)

        with cls.__lock:
            cached = cache_key in cls.__instances
            if cached:
                cached_instance = cls.__instances[cache_key]
            else:
                construction_lock = cls.__construction_locks.setdefault(
                    cache_key, threading.Lock()
                )

        if cached:
            # Reported with no factory lock held, for the same reason construction is
            # not: judging drift applies a declared __configuration__, and a pydantic
            # validator is as much arbitrary code as a constructor (SWE-22, SWE-29).
            cls.__warn_on_configuration_drift(cache_key, entry, configuration)
            return cached_instance

        with cls.__admit_construction(cache_key, construction_lock):
            # Re-checked now that this thread owns construction for the key: another
            # thread may have finished between the miss above and this point.
            with cls.__lock:
                cached = cache_key in cls.__instances
                if cached:
                    cached_instance = cls.__instances[cache_key]
                # Read here, in the same hold as the last look at the cache: from this
                # point on this call is building an instance of the registry as it is
                # now, and a clear() after it must not be undone by the result landing
                # (SWE-45). Taken before the configuration is applied rather than
                # after, because applying it is already this call's work.
                token = cls.__commit_token(cache_key)

            if cached:
                # Again outside the factory lock. The construction lock is still held,
                # which costs nothing: the instance exists now, so a call this helper
                # provoked for the same key takes the cache-hit path above and never
                # reaches for this lock.
                cls.__warn_on_configuration_drift(cache_key, entry, configuration)
                return cached_instance

            # Any declared __configuration__ is applied once, here, and construction is
            # handed the result. Both want it: construction builds from the validated
            # model, and the basis has to be of what construction would use rather than
            # of what the caller typed (SWE-29). Computing the basis by validating a
            # second time ran the consumer's validators twice for every singleton built,
            # and a validator is consumer code — it may be slow, and it may count
            # (SWE-42). There is nothing to repeat, because _validate_configuration
            # takes an instance of the declared model as already valid, so passing the
            # applied configuration on costs construction nothing.
            #
            # Whatever the schema raises is left to propagate. It would have come out of
            # _construct a moment later on the same configuration, so this hands the
            # caller nothing they were not already going to see — and the alternative,
            # swallowing it, would cache an instance built from a configuration the
            # class refused.
            #
            # The class is read through _class_of, which is what construction reads and
            # which resolves a lazy entry by importing. That is allowed here and nowhere
            # else in this comparison: no factory lock is held on this path, and
            # _construct is about to import the same module anyway. The cache-hit path
            # has no such licence — see __comparable.
            applied = cls.__applied(entry, cls._class_of(entry), configuration)

            # Recorded before the constructor runs, deliberately. A constructor is
            # entitled to mutate what it was handed — appending to a list it was given
            # is ordinary — and a basis captured afterwards would hold the constructor's
            # own edit. The next caller passing the configuration they actually passed
            # would then be blamed for a difference the first call introduced (SWE-29).
            comparison_basis = cls.__snapshot(cls.__reduced(applied))

            # No factory lock held here, by design: a constructor runs arbitrary code
            # and may itself resolve or create. The construction lock still admits
            # exactly one thread per key, so the single-instance guarantee holds. A
            # constructor that creates some *other* singleton is ordinary dependency
            # injection and proceeds; one that comes back for this key is a cycle, and
            # the admission above has it on this thread's stack so the nested call
            # raises instead of blocking on the lock this thread holds (SWE-36).
            new_instance = cls._construct(entry, applied)

            with cls.__lock:
                # The commit is where the race with clear() and pop() is settled,
                # because it is the only place left: the construction above cannot be
                # cancelled and cannot be run under this lock. An instance whose token
                # has moved was built against a registry that has since been reset, so
                # caching it would resurrect a key the caller was told was gone —
                # list_singletons() going [] and then ['slow'] with no create() in
                # between — and would hand the next caller an instance configured
                # before the reset (SWE-45).
                disowned = cls.__commit_token(cache_key) != token
                if not disowned:
                    cls.__instances[cache_key] = new_instance
                    cls.__configurations[cache_key] = comparison_basis
                    cls._logger.info(
                        f"Created and registered singleton instance: {entry.key}"
                    )
                else:
                    cls._logger.info(
                        f"Discarded a singleton instance constructed across a reset: "
                        f"{entry.key}"
                    )

        # Warned with neither lock held, and after the construction lock is released:
        # a filter that escalates SweetTeaWarning raises from here, and must not do so
        # holding a lock or leaving the key unconstructible. That escalation is the
        # right behaviour for whoever asks for it — a reset losing a construction is
        # exactly the kind of thing a strict caller wants to fail on — and it is the
        # reason the instance is returned on the line below rather than here.
        if disowned:
            cls.__warn_on_disowned_instance(cache_key)

        return new_instance

    @classmethod
    def __commit_token(cls, cache_key: tuple[str, str, str]) -> tuple[int, int]:
        """
        What must still hold when a construction commits, for its instance to be kept.

        Must be called with the factory's lock held, by both readers: the point of the
        token is that the state it samples cannot move between the sample and the
        comparison without a clear() or a pop() happening in between, and only the lock
        makes that true.

        Args:
            cache_key: Identity of the entry being constructed.

        Returns:
            The number of clear() calls so far, with the number of times this key has
            been popped.
        """
        return (cls.__clear_epoch, cls.__removals.get(cache_key, 0))

    @classmethod
    def __warn_on_disowned_instance(cls, cache_key: tuple[str, str, str]) -> None:
        """
        Report an instance that was constructed but not cached.

        A dropped instance is worth a warning rather than a log line, because the
        caller is left holding something the factory has disowned: it is not the
        singleton for its key, nothing else will ever be handed it, and the next
        ``create`` for that key constructs a second object the holder of this one has
        no way to learn about. Two objects that were both, briefly, "the" singleton is
        precisely the situation this class exists to prevent, and the reset is what
        caused it — so the report belongs with whoever's call lost, not only in a log
        nobody reads after the fact.

        Returning the instance anyway is deliberate. The construction finished, the
        object is valid, and raising instead would turn a caller's correct call into a
        failure because another thread reset the factory while it ran. A caller who
        would rather fail has ``warnings.simplefilter("error", SweetTeaWarning)``,
        which is the same switch the rest of this library hands them.

        Args:
            cache_key: Identity of the entry whose instance was dropped.
        """
        warnings.warn(
            f"The singleton instance just constructed for {cache_key[0]!r} was not "
            f"cached: SingletonFactory.clear() or pop() ran while it was being built, "
            f"so it was constructed against state that has since been reset. This "
            f"call returns it, but the factory does not hold it and the next create() "
            f"for this key will construct a new one — so this instance is not shared "
            f"with anything. Reset the factory before the constructions that depend "
            f"on it, not during them.",
            SweetTeaWarning,
            # 1 is this helper, 2 is create, 3 is the caller whose instance was
            # dropped — which is the line worth pointing at.
            stacklevel=3,
        )

    @classmethod
    @contextlib.contextmanager
    def __admit_construction(
        cls,
        cache_key: tuple[str, str, str],
        construction_lock: threading.Lock,
    ) -> Iterator[None]:
        """
        Admit this thread to construct one key, or say why it never can.

        Construction is admitted by a per-key lock, and a plain lock is the right tool
        for the thing it guards — but only for threads that have something to wait for.
        A thread that already holds the lock has not: a constructor reaching back into
        :meth:`create` for the key it is being constructed under is asking for an
        instance that cannot exist until it returns. Blocking there was a hang with no
        traceback and no timeout, which is the one failure mode a container must never
        hand a consumer, since there is nothing left to debug (SWE-36). Circular
        dependencies are the consumer's mistake; reporting them is this factory's job.

        Two shapes are detected, by the two things that can see them:

        - **Within one thread** — the self-referential constructor, and the longer
          ``alpha -> beta -> alpha`` chain — against this thread's own construction
          stack, before any lock is touched. No shared state is read, so the check
          costs a list scan whose length is the depth of the dependency graph.
        - **Across threads** — two threads entering one cycle from opposite ends, each
          holding the lock the other needs. No thread's own stack can see this, so a
          thread about to block registers what it is waiting for and walks the wait-for
          graph. Whichever of the two registers second finds the chain closed and
          raises; the other is freed when that one unwinds, and then meets the cycle on
          its own stack.

        The alternative for the cross-thread case was acquiring with a timeout and
        calling expiry a deadlock. That was rejected: any timeout short enough to
        diagnose a deadlock promptly is one a legitimately slow constructor — a
        connection pool, a model load, a cold cache on a loaded CI box — can exceed,
        and turning a correct program into a failing one to diagnose an incorrect one
        is the wrong trade. The wait-for graph needs no number chosen, reports the
        instant the cycle closes rather than seconds later, and names the threads and
        keys involved. What it cannot see is a cycle that leaves this factory — a
        constructor blocking on a lock of the consumer's own, held by a thread waiting
        on a singleton — which no bookkeeping here could see either; that remains a
        documented limitation rather than a guess enforced by a timer.

        Args:
            cache_key: Identity of the entry being constructed.
            construction_lock: The key's construction lock, already created under the
                factory's lock by the caller.

        Yields:
            None, with the construction lock held and this thread recorded as the key's
            constructor.

        Raises:
            SweetTeaError: When this thread is already constructing the key, or when
                waiting for it would close a cycle of threads that can never finish.
        """
        stack = cls.__keys_under_construction()
        if cache_key in stack:
            cycle = stack[stack.index(cache_key) :] + [cache_key]
            raise SweetTeaError(
                f"Circular singleton construction for key '{cache_key[0]}': "
                f"{' -> '.join(cls.__describe(key) for key in cycle)}. A constructor "
                f"asked SingletonFactory for a singleton whose construction it is "
                f"already inside, so the instance it wants cannot exist yet. Break the "
                f"cycle: take the collaborator as a constructor argument, or look it up "
                f"after construction rather than during it."
            )

        identity = threading.get_ident()
        if not construction_lock.acquire(blocking=False):
            # Another thread is constructing this key. Ordinarily that is contention
            # and this thread simply waits for it; it is a deadlock only if that thread
            # is itself waiting, directly or through others, on a key this thread is
            # constructing.
            cls.__block_until_admitted(cache_key, construction_lock, identity)

        with cls.__lock:
            cls.__construction_owner[cache_key] = (
                identity,
                threading.current_thread().name,
            )
        stack.append(cache_key)
        try:
            yield
        finally:
            # Unwound in the reverse order, and unconditionally: a constructor that
            # raises must leave the key constructible again, so the next caller
            # re-enters the constructor and sees the real exception rather than a
            # report of a cycle that is no longer in progress.
            stack.pop()
            with cls.__lock:
                cls.__construction_owner.pop(cache_key, None)
            construction_lock.release()

    @classmethod
    def __block_until_admitted(
        cls,
        cache_key: tuple[str, str, str],
        construction_lock: threading.Lock,
        identity: int,
    ) -> None:
        """
        Wait for another thread's construction of a key, unless that would deadlock.

        The wait is registered before it is judged, and both happen in one hold of the
        factory's lock. That ordering is what makes the detection complete rather than
        best-effort: two threads closing a cycle each register, and the one that
        registers second necessarily sees the other's registration, so the cycle cannot
        be missed by both. The lock is released before blocking, since blocking under it
        is the lock inversion SWE-22 exists to prevent.

        Args:
            cache_key: The key this thread wants to construct.
            construction_lock: Its construction lock, currently held by another thread.
            identity: This thread's id, as :func:`threading.get_ident` reports it.

        Raises:
            SweetTeaError: When the wait would close a cycle of threads waiting on one
                another's constructions.
        """
        with cls.__lock:
            cls.__blocked_on[identity] = cache_key
            chain = cls.__deadlock_chain(cache_key, identity)
            if chain is not None:
                del cls.__blocked_on[identity]

        if chain is not None:
            raise SweetTeaError(
                f"Deadlocked singleton construction for key '{cache_key[0]}': "
                f"{' -> '.join(chain + [cls.__describe(cache_key)])}. Two or more "
                f"threads are each constructing a singleton another of them is waiting "
                f"for, so none of them can finish. This is the same circular "
                f"dependency as the single-threaded case, entered from both ends at "
                f"once: break the cycle rather than retrying, since a retry will "
                f"deadlock again."
            )

        try:
            construction_lock.acquire()
        finally:
            # Cleared whether or not the wait succeeded, so a thread that is running
            # again is never read as still blocked.
            with cls.__lock:
                cls.__blocked_on.pop(identity, None)

    @classmethod
    def __deadlock_chain(
        cls, wanted: tuple[str, str, str], identity: int
    ) -> list[str] | None:
        """
        Follow the wait-for graph from a key, looking for a way back to this thread.

        Must be called with the factory's lock held and this thread's wait already
        registered, so that what the walk reads is the whole graph at one instant.

        Each hop asks the same two questions: who is constructing the key being waited
        for, and is that thread itself waiting? An owner that is running, or a key with
        no owner at all, ends the walk — the chain terminates in work that can finish,
        which is contention and not deadlock. An owner that turns out to be *this*
        thread closes the circle: this thread cannot release what it is constructing
        while it waits, so nothing in the chain can ever proceed.

        Args:
            wanted: The key this thread is waiting to construct.
            identity: This thread's id, as :func:`threading.get_ident` reports it.

        Returns:
            The chain of keys and the threads holding them, as text ready to join, when
            waiting would deadlock; None when the wait is ordinary contention.
        """
        chain: list[str] = []
        key = wanted
        # At most one hop per thread that could be holding a construction lock: a walk
        # longer than that has revisited a thread, which with every edge recorded under
        # one lock cannot happen without passing through this thread first.
        for _ in range(len(cls.__construction_owner) + 1):
            owner = cls.__construction_owner.get(key)
            if owner is None:
                return None

            owner_identity, owner_name = owner
            chain.append(f"{cls.__describe(key)} (being constructed on {owner_name})")
            if owner_identity == identity:
                return chain

            blocked_on = cls.__blocked_on.get(owner_identity)
            if blocked_on is None:
                return None
            key = blocked_on

        return None

    @classmethod
    def __keys_under_construction(cls) -> list[tuple[str, str, str]]:
        """
        Get the calling thread's own stack of keys it is part-way through constructing.

        Thread-local and therefore lock-free: the list is created on first use by the
        thread that will be its only reader and only writer.

        Returns:
            The live list, outermost key first, to be appended to and popped by the
            calling thread alone.
        """
        stack: list[tuple[str, str, str]] | None = getattr(
            cls.__construction_stack, "keys", None
        )
        if stack is None:
            stack = []
            cls.__construction_stack.keys = stack
        return stack

    @classmethod
    def __describe(cls, cache_key: tuple[str, str, str]) -> str:
        """
        Name a cache key the way a reader of a cycle message needs to see it.

        The key alone reads best and is almost always enough, but two entries can share
        a key and differ by library or label — which is exactly the pair a cycle message
        would otherwise render as ``alpha -> alpha`` — so those are spelled out when
        they are set.

        Args:
            cache_key: Identity of a registry entry, as (key, library, label).

        Returns:
            A short description for a diagnostic message.
        """
        key, library, label = cache_key
        if not library and not label:
            return key
        return f"{key}[{library}:{label}]"

    @classmethod
    def __applied(
        cls,
        entry: Entry,
        class_def: Any,
        configuration: dict[str, Any] | BaseModel | None,
    ) -> dict[str, Any] | BaseModel | None:
        """
        Apply a class's declared ``__configuration__``, when it declares one.

        Done through the same helper construction uses, so that one application serves
        both: construction builds from the model, and the comparison basis is of the
        model rather than of the dict the caller happened to type (see SWE-29).

        Applied once per call to :meth:`create`, which is what it takes to keep a
        validator from running twice for every singleton built (see SWE-42).

        Nothing is caught. Which failures are survivable is the caller's question, not
        this one's, and the two callers answer it differently: on the constructing path
        a schema that refuses the configuration is a create that must fail, while on the
        cache-hit path no instance is being built and there is nothing to fail at.

        Args:
            entry: The resolved registry entry, used for error messages.
            class_def: The class whose declaration to read. Passed in rather than taken
                from the entry because the two callers may not read it the same way —
                see :meth:`__comparable` on why a warning must not resolve a lazy entry.
            configuration: The configuration as given.

        Returns:
            The validated model when a schema is declared, and the configuration
            unchanged when none is.

        Raises:
            SweetTeaError: When the declaration is not a BaseModel subclass, or when it
                rejects the configuration.
            Exception: Whatever a validator raises on its own account.
        """
        schema = getattr(class_def, "__configuration__", None)
        if schema is None:
            return configuration
        return cls._validate_configuration(entry, schema, configuration)

    @classmethod
    def __reduced(
        cls, configuration: dict[str, Any] | BaseModel | None
    ) -> dict[str, Any] | None:
        """
        Reduce a schema-applied configuration to the keyword arguments it spreads.

        Drift is judged on what the class would actually be built with rather than on
        what the caller typed, because that is the question the warning answers: would
        this call have produced a different instance? Several spellings of one
        configuration reach the constructor identically — ``None`` and ``{}``, a dict
        and the model a declared ``__configuration__`` validates it into, a field
        omitted and the same field spelled out as the schema's own default — and all of
        them have to compare equal (see SWE-29).

        This mirrors the tail of :meth:`BaseFactory._construct` step for step; if that
        reduction ever changes, this must change with it.

        Args:
            configuration: The configuration with any declared schema already applied,
                as :meth:`__applied` returns it.

        Returns:
            The keyword arguments construction would spread, or None when the reduction
            cannot be made at all, which leaves drift undecidable.
        """
        try:
            if configuration is None:
                return {}
            return dict(configuration)
        except Exception:
            # A mapping that will not convert, a model whose iteration fails. This runs
            # for the sake of a warning; it must not be the reason a create fails, nor
            # raise on a cache-hit path that never raised before.
            return None

    @classmethod
    def __comparable(
        cls, entry: Entry, configuration: dict[str, Any] | BaseModel | None
    ) -> Any:
        """
        Compute a comparison basis on the cache-hit path, where nothing may go wrong.

        Nothing is being constructed here. The caller has its instance already and is
        owed it whatever this makes of the configuration, so every way of failing to
        reach a basis has to end in a return rather than a raise. Two of them end
        differently, and the distinction is the point of this method:

        - **The schema refuses the configuration.** Decidable, and decided: the cached
          instance's configuration validated, so one that does not validate cannot be
          the same request. That is drift, and it is reported (``__REJECTED_BY_SCHEMA``).
        - **The schema cannot be applied at all** — a validator raising ``AttributeError``
          on a value of the wrong type, a ``KeyError``, anything that is not pydantic
          saying no. Then there is no basis to compare and no verdict to give, so this
          falls silent, exactly as a value whose ``==`` is not boolean does. Previously
          such an exception escaped :meth:`create` on a call that only wanted the cached
          instance, which is the one thing a warning path must never do (SWE-42).

        Telling the two apart by exception type is the honest reading of each: a
        ``ValidationError``, which :meth:`BaseFactory._validate_configuration` has
        already wrapped as a :class:`SweetTeaError`, is the schema's own considered no.
        Anything else is a validator that broke, and a broken validator is evidence
        about the validator and none at all about the configuration.

        Nothing here imports, and nothing here resolves a lazy entry: the cache-hit
        path runs where SWE-22 showed what an import under a lock costs. A still-lazy
        entry leaves drift undecidable.

        Args:
            entry: The resolved registry entry the cached instance was built from.
            configuration: The configuration as given.

        Returns:
            The keyword arguments construction would spread; ``__REJECTED_BY_SCHEMA``
            when a declared model refuses the configuration; None when no basis can be
            computed, which leaves drift undecidable.
        """
        class_def = entry.class_object
        if class_def is None:
            # The stored field, never ``entry.class_def``: that property resolves a lazy
            # entry by importing its module, and an import on this path is the lock
            # inversion SWE-22 exists to prevent. Resolution is for construction; a
            # warning has less standing than that and gives up instead. In practice the
            # entry arrives resolved, since looking it up is what resolved it.
            return None

        try:
            applied = cls.__applied(entry, class_def, configuration)
        except SweetTeaError:
            return cls.__REJECTED_BY_SCHEMA
        except Exception:
            return None

        return cls.__reduced(applied)

    @classmethod
    def __snapshot(cls, kwargs: dict[str, Any] | None) -> dict[str, Any] | None:
        """
        Copy a comparison basis away from the caller's own objects.

        The recorded basis has to keep answering what the first caller asked for,
        however long ago. A shallow copy cannot: it keeps the caller's own containers,
        so a later mutation of one rewrites history. That cuts both ways — a caller who
        passes an equal configuration gets blamed for a mutation someone else made, and
        a caller who mutates a nested value and asks again is told nothing, because both
        sides are reading the one object (see SWE-29).

        Values are copied one at a time rather than the mapping as a whole, because
        ``copy.deepcopy`` raises on plenty of ordinary objects — an open file, a lock, a
        socket, some C extension types. A value that refuses to copy is kept by
        reference instead, which is no worse for that value than the old behaviour and
        leaves every other key in the configuration properly snapshotted; the
        alternative, discarding the whole basis, would cost the warning entirely for any
        configuration carrying a lock. Copying per value also drops any aliasing between
        two values of one configuration, which equality does not look at.

        Args:
            kwargs: The keyword arguments to record, as :meth:`__reduced` computed them,
                or None when the configuration would not reduce at all.

        Returns:
            A snapshot safe to keep, or None when there were no keyword arguments to
            record — which records drift against this instance as undecidable.
        """
        if kwargs is None:
            # The configuration would not reduce, so there is nothing a later call can
            # be compared against. Recording None is what makes drift against this
            # instance undecidable for good rather than guessed at.
            return None

        snapshot: dict[str, Any] = {}
        for name, value in kwargs.items():
            try:
                snapshot[name] = copy.deepcopy(value)
            except Exception:
                snapshot[name] = value
        return snapshot

    @classmethod
    def __same_request(cls, requested: Any, existing: Any) -> bool:
        """
        Decide whether two reduced configurations ask for the same thing.

        Plain ``==`` answers that wrongly in two places, and both rulings here are
        deliberate (see SWE-29):

        - **NaN is the same request as NaN.** That ``float("nan") != float("nan")`` is
          a statement about arithmetic, not about intent: a caller who passes NaN twice
          asked for the same configuration both times. ``==`` already agreed whenever
          the two calls happened to share one NaN object, since container comparison
          short-circuits on identity, so reflexivity was never really the rule — only
          an accident of which object the caller was holding.
        - **1, 1.0 and True are different requests.** ``==`` conflates them, but they
          arrive at the constructor as different types and behave differently once
          there, and ``0`` passed where ``False`` was meant is a plausible mistake worth
          reporting. Numbers are therefore compared type-sensitively. Where a class
          declares ``__configuration__`` the schema has already coerced both spellings
          to the field's own type before this is reached, so an equivalence somebody
          declared is honoured and one nobody declared is not.

        Dicts, lists and tuples are walked so that both rulings hold at any depth.
        Everything else is left to the value's own ``==``. Identity is taken as equality
        first, which is what lets an object with no useful ``==`` compare equal to
        itself.

        Args:
            requested: What this call would build with.
            existing: The recorded basis of the cached instance.

        Returns:
            True when the two are the same request.

        Raises:
            Exception: Whatever a value's own ``==`` raises; the caller reads that as
                undecidable drift.
        """
        if requested is existing:
            return True

        if isinstance(requested, (int, float, complex)) and isinstance(
            existing, (int, float, complex)
        ):
            if type(requested) is not type(existing):
                return False
            if (
                isinstance(requested, float)
                # Already implied by the matching types above; spelled out because the
                # type checker reads the narrowing of each name separately.
                and isinstance(existing, float)
                and math.isnan(requested)
                and math.isnan(existing)
            ):
                return True
            return bool(requested == existing)

        if isinstance(requested, dict) and isinstance(existing, dict):
            if requested.keys() != existing.keys():
                return False
            return all(
                cls.__same_request(value, existing[name])
                for name, value in requested.items()
            )

        if isinstance(requested, (list, tuple)) and isinstance(existing, (list, tuple)):
            # A list and a tuple of the same items are not the same request, which is
            # also what == says; the length check is only to keep zip from truncating.
            if type(requested) is not type(existing) or len(requested) != len(existing):
                return False
            return all(
                cls.__same_request(left, right)
                for left, right in zip(requested, existing)
            )

        return bool(requested == existing)

    @classmethod
    def __drifted(
        cls,
        cache_key: tuple[str, str, str],
        entry: Entry,
        configuration: dict[str, Any] | BaseModel | None,
    ) -> bool:
        """
        Decide whether this call asked for something other than what is cached.

        The answer is no whenever the question cannot be answered. Drift is a warning,
        not a diagnosis: where the comparison is undecidable, the instance is returned
        exactly as it would have been and the caller hears nothing, which is quieter
        than a warning fired on every call and far quieter than an exception out of
        :meth:`create`.

        Args:
            cache_key: Identity of the cached entry.
            entry: The resolved entry, carrying the class whose declared configuration
                model the comparison is made through.
            configuration: What this call passed.

        Returns:
            True when this call's configuration would have built a different instance.
        """
        existing = cls.__configurations.get(cache_key)
        if existing is None:
            # The cached instance's own basis was never computable, so there is nothing
            # to disagree with.
            return False

        requested = cls.__comparable(entry, configuration)

        if requested is cls.__REJECTED_BY_SCHEMA:
            # Decidably different: the cached instance's configuration validated and
            # this one does not, so they cannot be the same request. Worth saying, since
            # this caller gets neither the configuration they passed nor the
            # SweetTeaError a first call would have raised for it.
            return True

        if requested is None:
            return False

        try:
            return not cls.__same_request(requested, existing)
        except Exception:
            # Values whose comparison is not boolean — a numpy array, say — make drift
            # undecidable. A warning on every call would be worse than none.
            return False

    @classmethod
    def __warn_on_configuration_drift(
        cls,
        cache_key: tuple[str, str, str],
        entry: Entry,
        configuration: dict[str, Any] | BaseModel | None,
    ) -> None:
        """
        Report a configuration that arrived too late to take effect.

        A cached singleton is never rebuilt — other holders rely on the identity — so a
        configuration passed to a later call is discarded. That is the right behaviour
        and the wrong silence: a caller who believes they configured something gets an
        instance configured by whoever called first (see SWE-14).

        Nothing is said when the caller passed no configuration, since that is the
        ordinary way to fetch an existing singleton. An empty dict says the same thing
        and is treated the same way: construction maps it and ``None`` alike to no
        keyword arguments, and someone writing ``configuration=options.get("cfg", {})``
        is fetching rather than configuring. Warning there was the false positive most
        likely to get the whole category filtered out (see SWE-29).

        Args:
            cache_key: Identity of the cached entry.
            entry: The resolved entry, which carries the class whose declared
                configuration model the comparison is made through.
            configuration: What this call passed.
        """
        if configuration is None or (
            isinstance(configuration, dict) and not configuration
        ):
            return

        if not cls.__drifted(cache_key, entry, configuration):
            return

        warnings.warn(
            f"Ignoring the configuration passed for singleton {cache_key[0]!r}: an "
            f"instance is already cached and is not rebuilt, so the configuration from "
            f"the first call still applies. Use Factory for per-call configuration, or "
            f"SingletonFactory.pop to discard the cached instance first.",
            SweetTeaWarning,
            # 1 is this helper, 2 is create, 3 is the caller whose configuration was
            # dropped — which is the line worth pointing at.
            stacklevel=3,
        )

    @classmethod
    def clear(cls) -> None:
        """
        Remove all registered instances.

        This is primarily useful for testing or resetting the factory state.

        The construction locks are deliberately left in place. Dropping them could not
        strand a construction — a lock a thread holds stays alive through that thread's
        own reference — but that was never the danger. The danger was the *second* lock:
        with the mapping emptied, the next caller for a key under construction created a
        fresh lock for it, took that one unopposed, and ran the constructor a second time
        in parallel with the first. Two instances of a singleton, from a reset API, is
        the one outcome this class must not have (SWE-39). The locks are keyed by
        resolved cache key and so bounded by the registry, which costs little to keep.

        A construction already running is not cancelled either — nothing can cancel it
        — but it no longer installs its result over this reset. Clearing bumps the
        epoch every in-flight construction sampled before it began, so a construction
        that finishes after this call returns its instance to its caller with a warning
        and caches nothing (SWE-45). Without that, ``list_singletons()`` went ``[]``
        and then back to ``['slow']`` with no ``create`` in between, holding an
        instance built against the registry as it was before the reset.
        """
        with cls.__lock:
            count = len(cls.__instances)
            cls.__instances.clear()
            cls.__configurations.clear()
            # Emptied rather than bumped per key: every token in flight carries the
            # old epoch, so it loses whatever its per-key count says, and the mapping
            # starts again at nothing instead of growing across resets.
            cls.__clear_epoch += 1
            cls.__removals.clear()
            cls._logger.info(f"Cleared {count} singleton instances")

    @classmethod
    def pop(cls, key: str, library: str = "", label: str = "") -> Any:
        """
        Remove and return a cached instance.

        Resolves through the same path as :meth:`create`, so any spelling of the key
        removes the instance that spelling would have returned.

        A construction of this key that began before this call does not cache its
        result afterwards, for the reason :meth:`clear` gives: an instance built before
        a removal must not undo it (SWE-45). Only this key is affected — a construction
        of some other singleton is none of this call's business.

        Args:
            key: The key of the instance to remove, in any supported spelling.
            library: Optional library filter, matching the one passed to create.
            label: Optional label filter, matching the one passed to create.

        Returns:
            The removed instance.

        Raises:
            SweetTeaError: If no instance is cached for the given key.
        """
        with cls.__lock:
            try:
                entry = cls._select_entry(cls._find_entries(key), key, library, label)
            except SweetTeaError:
                # Unresolvable keys and resolvable-but-uncached ones report the same
                # way; from the caller's side both mean "nothing to remove".
                entry = None

            cache_key = (
                (entry.key, entry.library, entry.label) if entry is not None else None
            )

            if cache_key is None or cache_key not in cls.__instances:
                raise SweetTeaError(
                    f"No singleton instance registered for key '{key}'. "
                    f"Available keys: {cls.list_singletons()}"
                )

            instance = cls.__instances.pop(cache_key)
            cls.__configurations.pop(cache_key, None)

            # The same guard clear() gets, narrowed to this key: a construction that
            # began before this removal must not cache its result after it (SWE-45).
            # Only this key's count moves, so an unrelated construction is not disowned
            # by the removal of a neighbour. Bounded by the registry, as the
            # construction locks are.
            cls.__removals[cache_key] = cls.__removals.get(cache_key, 0) + 1

            # Python's garbage collector will handle destruction automatically
            # when all references are removed

            # Reported off cache_key rather than entry: the None check above narrows
            # cache_key but not entry, and both carry the same resolved key.
            cls._logger.info(f"Removed singleton instance: {key} (key: {cache_key[0]})")
            return instance

    @classmethod
    def list_keys(cls) -> list[str]:
        """
        Get a list of all class keys that can be created.

        Returns:
            List of available class keys from the registry in alphabetical order.
        """
        # Return keys from the Registry that can be created
        entries = cls._registry.entries()
        return sorted([entry.key for entry in entries])

    @classmethod
    def list_singletons(cls) -> list[str]:
        """
        Get a list of all cached singleton instance keys.

        Returns:
            List of cached singleton keys in alphabetical order. Entries that share a
            key across libraries or labels appear once per cached instance.
        """
        with cls.__lock:
            return sorted(entry_key for entry_key, _, _ in cls.__instances)

__admit_construction(cache_key, construction_lock) classmethod

Admit this thread to construct one key, or say why it never can.

Construction is admitted by a per-key lock, and a plain lock is the right tool for the thing it guards — but only for threads that have something to wait for. A thread that already holds the lock has not: a constructor reaching back into :meth:create for the key it is being constructed under is asking for an instance that cannot exist until it returns. Blocking there was a hang with no traceback and no timeout, which is the one failure mode a container must never hand a consumer, since there is nothing left to debug (SWE-36). Circular dependencies are the consumer's mistake; reporting them is this factory's job.

Two shapes are detected, by the two things that can see them:

  • Within one thread — the self-referential constructor, and the longer alpha -> beta -> alpha chain — against this thread's own construction stack, before any lock is touched. No shared state is read, so the check costs a list scan whose length is the depth of the dependency graph.
  • Across threads — two threads entering one cycle from opposite ends, each holding the lock the other needs. No thread's own stack can see this, so a thread about to block registers what it is waiting for and walks the wait-for graph. Whichever of the two registers second finds the chain closed and raises; the other is freed when that one unwinds, and then meets the cycle on its own stack.

The alternative for the cross-thread case was acquiring with a timeout and calling expiry a deadlock. That was rejected: any timeout short enough to diagnose a deadlock promptly is one a legitimately slow constructor — a connection pool, a model load, a cold cache on a loaded CI box — can exceed, and turning a correct program into a failing one to diagnose an incorrect one is the wrong trade. The wait-for graph needs no number chosen, reports the instant the cycle closes rather than seconds later, and names the threads and keys involved. What it cannot see is a cycle that leaves this factory — a constructor blocking on a lock of the consumer's own, held by a thread waiting on a singleton — which no bookkeeping here could see either; that remains a documented limitation rather than a guess enforced by a timer.

Parameters:

Name Type Description Default
cache_key tuple[str, str, str]

Identity of the entry being constructed.

required
construction_lock Lock

The key's construction lock, already created under the factory's lock by the caller.

required

Yields:

Type Description
None

None, with the construction lock held and this thread recorded as the key's

None

constructor.

Raises:

Type Description
SweetTeaError

When this thread is already constructing the key, or when waiting for it would close a cycle of threads that can never finish.

Source code in sweet_tea/singleton_factory.py
@classmethod
@contextlib.contextmanager
def __admit_construction(
    cls,
    cache_key: tuple[str, str, str],
    construction_lock: threading.Lock,
) -> Iterator[None]:
    """
    Admit this thread to construct one key, or say why it never can.

    Construction is admitted by a per-key lock, and a plain lock is the right tool
    for the thing it guards — but only for threads that have something to wait for.
    A thread that already holds the lock has not: a constructor reaching back into
    :meth:`create` for the key it is being constructed under is asking for an
    instance that cannot exist until it returns. Blocking there was a hang with no
    traceback and no timeout, which is the one failure mode a container must never
    hand a consumer, since there is nothing left to debug (SWE-36). Circular
    dependencies are the consumer's mistake; reporting them is this factory's job.

    Two shapes are detected, by the two things that can see them:

    - **Within one thread** — the self-referential constructor, and the longer
      ``alpha -> beta -> alpha`` chain — against this thread's own construction
      stack, before any lock is touched. No shared state is read, so the check
      costs a list scan whose length is the depth of the dependency graph.
    - **Across threads** — two threads entering one cycle from opposite ends, each
      holding the lock the other needs. No thread's own stack can see this, so a
      thread about to block registers what it is waiting for and walks the wait-for
      graph. Whichever of the two registers second finds the chain closed and
      raises; the other is freed when that one unwinds, and then meets the cycle on
      its own stack.

    The alternative for the cross-thread case was acquiring with a timeout and
    calling expiry a deadlock. That was rejected: any timeout short enough to
    diagnose a deadlock promptly is one a legitimately slow constructor — a
    connection pool, a model load, a cold cache on a loaded CI box — can exceed,
    and turning a correct program into a failing one to diagnose an incorrect one
    is the wrong trade. The wait-for graph needs no number chosen, reports the
    instant the cycle closes rather than seconds later, and names the threads and
    keys involved. What it cannot see is a cycle that leaves this factory — a
    constructor blocking on a lock of the consumer's own, held by a thread waiting
    on a singleton — which no bookkeeping here could see either; that remains a
    documented limitation rather than a guess enforced by a timer.

    Args:
        cache_key: Identity of the entry being constructed.
        construction_lock: The key's construction lock, already created under the
            factory's lock by the caller.

    Yields:
        None, with the construction lock held and this thread recorded as the key's
        constructor.

    Raises:
        SweetTeaError: When this thread is already constructing the key, or when
            waiting for it would close a cycle of threads that can never finish.
    """
    stack = cls.__keys_under_construction()
    if cache_key in stack:
        cycle = stack[stack.index(cache_key) :] + [cache_key]
        raise SweetTeaError(
            f"Circular singleton construction for key '{cache_key[0]}': "
            f"{' -> '.join(cls.__describe(key) for key in cycle)}. A constructor "
            f"asked SingletonFactory for a singleton whose construction it is "
            f"already inside, so the instance it wants cannot exist yet. Break the "
            f"cycle: take the collaborator as a constructor argument, or look it up "
            f"after construction rather than during it."
        )

    identity = threading.get_ident()
    if not construction_lock.acquire(blocking=False):
        # Another thread is constructing this key. Ordinarily that is contention
        # and this thread simply waits for it; it is a deadlock only if that thread
        # is itself waiting, directly or through others, on a key this thread is
        # constructing.
        cls.__block_until_admitted(cache_key, construction_lock, identity)

    with cls.__lock:
        cls.__construction_owner[cache_key] = (
            identity,
            threading.current_thread().name,
        )
    stack.append(cache_key)
    try:
        yield
    finally:
        # Unwound in the reverse order, and unconditionally: a constructor that
        # raises must leave the key constructible again, so the next caller
        # re-enters the constructor and sees the real exception rather than a
        # report of a cycle that is no longer in progress.
        stack.pop()
        with cls.__lock:
            cls.__construction_owner.pop(cache_key, None)
        construction_lock.release()

__applied(entry, class_def, configuration) classmethod

Apply a class's declared __configuration__, when it declares one.

Done through the same helper construction uses, so that one application serves both: construction builds from the model, and the comparison basis is of the model rather than of the dict the caller happened to type (see SWE-29).

Applied once per call to :meth:create, which is what it takes to keep a validator from running twice for every singleton built (see SWE-42).

Nothing is caught. Which failures are survivable is the caller's question, not this one's, and the two callers answer it differently: on the constructing path a schema that refuses the configuration is a create that must fail, while on the cache-hit path no instance is being built and there is nothing to fail at.

Parameters:

Name Type Description Default
entry Entry

The resolved registry entry, used for error messages.

required
class_def Any

The class whose declaration to read. Passed in rather than taken from the entry because the two callers may not read it the same way — see :meth:__comparable on why a warning must not resolve a lazy entry.

required
configuration dict[str, Any] | BaseModel | None

The configuration as given.

required

Returns:

Type Description
dict[str, Any] | BaseModel | None

The validated model when a schema is declared, and the configuration

dict[str, Any] | BaseModel | None

unchanged when none is.

Raises:

Type Description
SweetTeaError

When the declaration is not a BaseModel subclass, or when it rejects the configuration.

Exception

Whatever a validator raises on its own account.

Source code in sweet_tea/singleton_factory.py
@classmethod
def __applied(
    cls,
    entry: Entry,
    class_def: Any,
    configuration: dict[str, Any] | BaseModel | None,
) -> dict[str, Any] | BaseModel | None:
    """
    Apply a class's declared ``__configuration__``, when it declares one.

    Done through the same helper construction uses, so that one application serves
    both: construction builds from the model, and the comparison basis is of the
    model rather than of the dict the caller happened to type (see SWE-29).

    Applied once per call to :meth:`create`, which is what it takes to keep a
    validator from running twice for every singleton built (see SWE-42).

    Nothing is caught. Which failures are survivable is the caller's question, not
    this one's, and the two callers answer it differently: on the constructing path
    a schema that refuses the configuration is a create that must fail, while on the
    cache-hit path no instance is being built and there is nothing to fail at.

    Args:
        entry: The resolved registry entry, used for error messages.
        class_def: The class whose declaration to read. Passed in rather than taken
            from the entry because the two callers may not read it the same way —
            see :meth:`__comparable` on why a warning must not resolve a lazy entry.
        configuration: The configuration as given.

    Returns:
        The validated model when a schema is declared, and the configuration
        unchanged when none is.

    Raises:
        SweetTeaError: When the declaration is not a BaseModel subclass, or when it
            rejects the configuration.
        Exception: Whatever a validator raises on its own account.
    """
    schema = getattr(class_def, "__configuration__", None)
    if schema is None:
        return configuration
    return cls._validate_configuration(entry, schema, configuration)

__block_until_admitted(cache_key, construction_lock, identity) classmethod

Wait for another thread's construction of a key, unless that would deadlock.

The wait is registered before it is judged, and both happen in one hold of the factory's lock. That ordering is what makes the detection complete rather than best-effort: two threads closing a cycle each register, and the one that registers second necessarily sees the other's registration, so the cycle cannot be missed by both. The lock is released before blocking, since blocking under it is the lock inversion SWE-22 exists to prevent.

Parameters:

Name Type Description Default
cache_key tuple[str, str, str]

The key this thread wants to construct.

required
construction_lock Lock

Its construction lock, currently held by another thread.

required
identity int

This thread's id, as :func:threading.get_ident reports it.

required

Raises:

Type Description
SweetTeaError

When the wait would close a cycle of threads waiting on one another's constructions.

Source code in sweet_tea/singleton_factory.py
@classmethod
def __block_until_admitted(
    cls,
    cache_key: tuple[str, str, str],
    construction_lock: threading.Lock,
    identity: int,
) -> None:
    """
    Wait for another thread's construction of a key, unless that would deadlock.

    The wait is registered before it is judged, and both happen in one hold of the
    factory's lock. That ordering is what makes the detection complete rather than
    best-effort: two threads closing a cycle each register, and the one that
    registers second necessarily sees the other's registration, so the cycle cannot
    be missed by both. The lock is released before blocking, since blocking under it
    is the lock inversion SWE-22 exists to prevent.

    Args:
        cache_key: The key this thread wants to construct.
        construction_lock: Its construction lock, currently held by another thread.
        identity: This thread's id, as :func:`threading.get_ident` reports it.

    Raises:
        SweetTeaError: When the wait would close a cycle of threads waiting on one
            another's constructions.
    """
    with cls.__lock:
        cls.__blocked_on[identity] = cache_key
        chain = cls.__deadlock_chain(cache_key, identity)
        if chain is not None:
            del cls.__blocked_on[identity]

    if chain is not None:
        raise SweetTeaError(
            f"Deadlocked singleton construction for key '{cache_key[0]}': "
            f"{' -> '.join(chain + [cls.__describe(cache_key)])}. Two or more "
            f"threads are each constructing a singleton another of them is waiting "
            f"for, so none of them can finish. This is the same circular "
            f"dependency as the single-threaded case, entered from both ends at "
            f"once: break the cycle rather than retrying, since a retry will "
            f"deadlock again."
        )

    try:
        construction_lock.acquire()
    finally:
        # Cleared whether or not the wait succeeded, so a thread that is running
        # again is never read as still blocked.
        with cls.__lock:
            cls.__blocked_on.pop(identity, None)

__commit_token(cache_key) classmethod

What must still hold when a construction commits, for its instance to be kept.

Must be called with the factory's lock held, by both readers: the point of the token is that the state it samples cannot move between the sample and the comparison without a clear() or a pop() happening in between, and only the lock makes that true.

Parameters:

Name Type Description Default
cache_key tuple[str, str, str]

Identity of the entry being constructed.

required

Returns:

Type Description
int

The number of clear() calls so far, with the number of times this key has

int

been popped.

Source code in sweet_tea/singleton_factory.py
@classmethod
def __commit_token(cls, cache_key: tuple[str, str, str]) -> tuple[int, int]:
    """
    What must still hold when a construction commits, for its instance to be kept.

    Must be called with the factory's lock held, by both readers: the point of the
    token is that the state it samples cannot move between the sample and the
    comparison without a clear() or a pop() happening in between, and only the lock
    makes that true.

    Args:
        cache_key: Identity of the entry being constructed.

    Returns:
        The number of clear() calls so far, with the number of times this key has
        been popped.
    """
    return (cls.__clear_epoch, cls.__removals.get(cache_key, 0))

__comparable(entry, configuration) classmethod

Compute a comparison basis on the cache-hit path, where nothing may go wrong.

Nothing is being constructed here. The caller has its instance already and is owed it whatever this makes of the configuration, so every way of failing to reach a basis has to end in a return rather than a raise. Two of them end differently, and the distinction is the point of this method:

  • The schema refuses the configuration. Decidable, and decided: the cached instance's configuration validated, so one that does not validate cannot be the same request. That is drift, and it is reported (__REJECTED_BY_SCHEMA).
  • The schema cannot be applied at all — a validator raising AttributeError on a value of the wrong type, a KeyError, anything that is not pydantic saying no. Then there is no basis to compare and no verdict to give, so this falls silent, exactly as a value whose == is not boolean does. Previously such an exception escaped :meth:create on a call that only wanted the cached instance, which is the one thing a warning path must never do (SWE-42).

Telling the two apart by exception type is the honest reading of each: a ValidationError, which :meth:BaseFactory._validate_configuration has already wrapped as a :class:SweetTeaError, is the schema's own considered no. Anything else is a validator that broke, and a broken validator is evidence about the validator and none at all about the configuration.

Nothing here imports, and nothing here resolves a lazy entry: the cache-hit path runs where SWE-22 showed what an import under a lock costs. A still-lazy entry leaves drift undecidable.

Parameters:

Name Type Description Default
entry Entry

The resolved registry entry the cached instance was built from.

required
configuration dict[str, Any] | BaseModel | None

The configuration as given.

required

Returns:

Type Description
Any

The keyword arguments construction would spread; __REJECTED_BY_SCHEMA

Any

when a declared model refuses the configuration; None when no basis can be

Any

computed, which leaves drift undecidable.

Source code in sweet_tea/singleton_factory.py
@classmethod
def __comparable(
    cls, entry: Entry, configuration: dict[str, Any] | BaseModel | None
) -> Any:
    """
    Compute a comparison basis on the cache-hit path, where nothing may go wrong.

    Nothing is being constructed here. The caller has its instance already and is
    owed it whatever this makes of the configuration, so every way of failing to
    reach a basis has to end in a return rather than a raise. Two of them end
    differently, and the distinction is the point of this method:

    - **The schema refuses the configuration.** Decidable, and decided: the cached
      instance's configuration validated, so one that does not validate cannot be
      the same request. That is drift, and it is reported (``__REJECTED_BY_SCHEMA``).
    - **The schema cannot be applied at all** — a validator raising ``AttributeError``
      on a value of the wrong type, a ``KeyError``, anything that is not pydantic
      saying no. Then there is no basis to compare and no verdict to give, so this
      falls silent, exactly as a value whose ``==`` is not boolean does. Previously
      such an exception escaped :meth:`create` on a call that only wanted the cached
      instance, which is the one thing a warning path must never do (SWE-42).

    Telling the two apart by exception type is the honest reading of each: a
    ``ValidationError``, which :meth:`BaseFactory._validate_configuration` has
    already wrapped as a :class:`SweetTeaError`, is the schema's own considered no.
    Anything else is a validator that broke, and a broken validator is evidence
    about the validator and none at all about the configuration.

    Nothing here imports, and nothing here resolves a lazy entry: the cache-hit
    path runs where SWE-22 showed what an import under a lock costs. A still-lazy
    entry leaves drift undecidable.

    Args:
        entry: The resolved registry entry the cached instance was built from.
        configuration: The configuration as given.

    Returns:
        The keyword arguments construction would spread; ``__REJECTED_BY_SCHEMA``
        when a declared model refuses the configuration; None when no basis can be
        computed, which leaves drift undecidable.
    """
    class_def = entry.class_object
    if class_def is None:
        # The stored field, never ``entry.class_def``: that property resolves a lazy
        # entry by importing its module, and an import on this path is the lock
        # inversion SWE-22 exists to prevent. Resolution is for construction; a
        # warning has less standing than that and gives up instead. In practice the
        # entry arrives resolved, since looking it up is what resolved it.
        return None

    try:
        applied = cls.__applied(entry, class_def, configuration)
    except SweetTeaError:
        return cls.__REJECTED_BY_SCHEMA
    except Exception:
        return None

    return cls.__reduced(applied)

__deadlock_chain(wanted, identity) classmethod

Follow the wait-for graph from a key, looking for a way back to this thread.

Must be called with the factory's lock held and this thread's wait already registered, so that what the walk reads is the whole graph at one instant.

Each hop asks the same two questions: who is constructing the key being waited for, and is that thread itself waiting? An owner that is running, or a key with no owner at all, ends the walk — the chain terminates in work that can finish, which is contention and not deadlock. An owner that turns out to be this thread closes the circle: this thread cannot release what it is constructing while it waits, so nothing in the chain can ever proceed.

Parameters:

Name Type Description Default
wanted tuple[str, str, str]

The key this thread is waiting to construct.

required
identity int

This thread's id, as :func:threading.get_ident reports it.

required

Returns:

Type Description
list[str] | None

The chain of keys and the threads holding them, as text ready to join, when

list[str] | None

waiting would deadlock; None when the wait is ordinary contention.

Source code in sweet_tea/singleton_factory.py
@classmethod
def __deadlock_chain(
    cls, wanted: tuple[str, str, str], identity: int
) -> list[str] | None:
    """
    Follow the wait-for graph from a key, looking for a way back to this thread.

    Must be called with the factory's lock held and this thread's wait already
    registered, so that what the walk reads is the whole graph at one instant.

    Each hop asks the same two questions: who is constructing the key being waited
    for, and is that thread itself waiting? An owner that is running, or a key with
    no owner at all, ends the walk — the chain terminates in work that can finish,
    which is contention and not deadlock. An owner that turns out to be *this*
    thread closes the circle: this thread cannot release what it is constructing
    while it waits, so nothing in the chain can ever proceed.

    Args:
        wanted: The key this thread is waiting to construct.
        identity: This thread's id, as :func:`threading.get_ident` reports it.

    Returns:
        The chain of keys and the threads holding them, as text ready to join, when
        waiting would deadlock; None when the wait is ordinary contention.
    """
    chain: list[str] = []
    key = wanted
    # At most one hop per thread that could be holding a construction lock: a walk
    # longer than that has revisited a thread, which with every edge recorded under
    # one lock cannot happen without passing through this thread first.
    for _ in range(len(cls.__construction_owner) + 1):
        owner = cls.__construction_owner.get(key)
        if owner is None:
            return None

        owner_identity, owner_name = owner
        chain.append(f"{cls.__describe(key)} (being constructed on {owner_name})")
        if owner_identity == identity:
            return chain

        blocked_on = cls.__blocked_on.get(owner_identity)
        if blocked_on is None:
            return None
        key = blocked_on

    return None

__describe(cache_key) classmethod

Name a cache key the way a reader of a cycle message needs to see it.

The key alone reads best and is almost always enough, but two entries can share a key and differ by library or label — which is exactly the pair a cycle message would otherwise render as alpha -> alpha — so those are spelled out when they are set.

Parameters:

Name Type Description Default
cache_key tuple[str, str, str]

Identity of a registry entry, as (key, library, label).

required

Returns:

Type Description
str

A short description for a diagnostic message.

Source code in sweet_tea/singleton_factory.py
@classmethod
def __describe(cls, cache_key: tuple[str, str, str]) -> str:
    """
    Name a cache key the way a reader of a cycle message needs to see it.

    The key alone reads best and is almost always enough, but two entries can share
    a key and differ by library or label — which is exactly the pair a cycle message
    would otherwise render as ``alpha -> alpha`` — so those are spelled out when
    they are set.

    Args:
        cache_key: Identity of a registry entry, as (key, library, label).

    Returns:
        A short description for a diagnostic message.
    """
    key, library, label = cache_key
    if not library and not label:
        return key
    return f"{key}[{library}:{label}]"

__drifted(cache_key, entry, configuration) classmethod

Decide whether this call asked for something other than what is cached.

The answer is no whenever the question cannot be answered. Drift is a warning, not a diagnosis: where the comparison is undecidable, the instance is returned exactly as it would have been and the caller hears nothing, which is quieter than a warning fired on every call and far quieter than an exception out of :meth:create.

Parameters:

Name Type Description Default
cache_key tuple[str, str, str]

Identity of the cached entry.

required
entry Entry

The resolved entry, carrying the class whose declared configuration model the comparison is made through.

required
configuration dict[str, Any] | BaseModel | None

What this call passed.

required

Returns:

Type Description
bool

True when this call's configuration would have built a different instance.

Source code in sweet_tea/singleton_factory.py
@classmethod
def __drifted(
    cls,
    cache_key: tuple[str, str, str],
    entry: Entry,
    configuration: dict[str, Any] | BaseModel | None,
) -> bool:
    """
    Decide whether this call asked for something other than what is cached.

    The answer is no whenever the question cannot be answered. Drift is a warning,
    not a diagnosis: where the comparison is undecidable, the instance is returned
    exactly as it would have been and the caller hears nothing, which is quieter
    than a warning fired on every call and far quieter than an exception out of
    :meth:`create`.

    Args:
        cache_key: Identity of the cached entry.
        entry: The resolved entry, carrying the class whose declared configuration
            model the comparison is made through.
        configuration: What this call passed.

    Returns:
        True when this call's configuration would have built a different instance.
    """
    existing = cls.__configurations.get(cache_key)
    if existing is None:
        # The cached instance's own basis was never computable, so there is nothing
        # to disagree with.
        return False

    requested = cls.__comparable(entry, configuration)

    if requested is cls.__REJECTED_BY_SCHEMA:
        # Decidably different: the cached instance's configuration validated and
        # this one does not, so they cannot be the same request. Worth saying, since
        # this caller gets neither the configuration they passed nor the
        # SweetTeaError a first call would have raised for it.
        return True

    if requested is None:
        return False

    try:
        return not cls.__same_request(requested, existing)
    except Exception:
        # Values whose comparison is not boolean — a numpy array, say — make drift
        # undecidable. A warning on every call would be worse than none.
        return False

__keys_under_construction() classmethod

Get the calling thread's own stack of keys it is part-way through constructing.

Thread-local and therefore lock-free: the list is created on first use by the thread that will be its only reader and only writer.

Returns:

Type Description
list[tuple[str, str, str]]

The live list, outermost key first, to be appended to and popped by the

list[tuple[str, str, str]]

calling thread alone.

Source code in sweet_tea/singleton_factory.py
@classmethod
def __keys_under_construction(cls) -> list[tuple[str, str, str]]:
    """
    Get the calling thread's own stack of keys it is part-way through constructing.

    Thread-local and therefore lock-free: the list is created on first use by the
    thread that will be its only reader and only writer.

    Returns:
        The live list, outermost key first, to be appended to and popped by the
        calling thread alone.
    """
    stack: list[tuple[str, str, str]] | None = getattr(
        cls.__construction_stack, "keys", None
    )
    if stack is None:
        stack = []
        cls.__construction_stack.keys = stack
    return stack

__reduced(configuration) classmethod

Reduce a schema-applied configuration to the keyword arguments it spreads.

Drift is judged on what the class would actually be built with rather than on what the caller typed, because that is the question the warning answers: would this call have produced a different instance? Several spellings of one configuration reach the constructor identically — None and {}, a dict and the model a declared __configuration__ validates it into, a field omitted and the same field spelled out as the schema's own default — and all of them have to compare equal (see SWE-29).

This mirrors the tail of :meth:BaseFactory._construct step for step; if that reduction ever changes, this must change with it.

Parameters:

Name Type Description Default
configuration dict[str, Any] | BaseModel | None

The configuration with any declared schema already applied, as :meth:__applied returns it.

required

Returns:

Type Description
dict[str, Any] | None

The keyword arguments construction would spread, or None when the reduction

dict[str, Any] | None

cannot be made at all, which leaves drift undecidable.

Source code in sweet_tea/singleton_factory.py
@classmethod
def __reduced(
    cls, configuration: dict[str, Any] | BaseModel | None
) -> dict[str, Any] | None:
    """
    Reduce a schema-applied configuration to the keyword arguments it spreads.

    Drift is judged on what the class would actually be built with rather than on
    what the caller typed, because that is the question the warning answers: would
    this call have produced a different instance? Several spellings of one
    configuration reach the constructor identically — ``None`` and ``{}``, a dict
    and the model a declared ``__configuration__`` validates it into, a field
    omitted and the same field spelled out as the schema's own default — and all of
    them have to compare equal (see SWE-29).

    This mirrors the tail of :meth:`BaseFactory._construct` step for step; if that
    reduction ever changes, this must change with it.

    Args:
        configuration: The configuration with any declared schema already applied,
            as :meth:`__applied` returns it.

    Returns:
        The keyword arguments construction would spread, or None when the reduction
        cannot be made at all, which leaves drift undecidable.
    """
    try:
        if configuration is None:
            return {}
        return dict(configuration)
    except Exception:
        # A mapping that will not convert, a model whose iteration fails. This runs
        # for the sake of a warning; it must not be the reason a create fails, nor
        # raise on a cache-hit path that never raised before.
        return None

__same_request(requested, existing) classmethod

Decide whether two reduced configurations ask for the same thing.

Plain == answers that wrongly in two places, and both rulings here are deliberate (see SWE-29):

  • NaN is the same request as NaN. That float("nan") != float("nan") is a statement about arithmetic, not about intent: a caller who passes NaN twice asked for the same configuration both times. == already agreed whenever the two calls happened to share one NaN object, since container comparison short-circuits on identity, so reflexivity was never really the rule — only an accident of which object the caller was holding.
  • 1, 1.0 and True are different requests. == conflates them, but they arrive at the constructor as different types and behave differently once there, and 0 passed where False was meant is a plausible mistake worth reporting. Numbers are therefore compared type-sensitively. Where a class declares __configuration__ the schema has already coerced both spellings to the field's own type before this is reached, so an equivalence somebody declared is honoured and one nobody declared is not.

Dicts, lists and tuples are walked so that both rulings hold at any depth. Everything else is left to the value's own ==. Identity is taken as equality first, which is what lets an object with no useful == compare equal to itself.

Parameters:

Name Type Description Default
requested Any

What this call would build with.

required
existing Any

The recorded basis of the cached instance.

required

Returns:

Type Description
bool

True when the two are the same request.

Raises:

Type Description
Exception

Whatever a value's own == raises; the caller reads that as undecidable drift.

Source code in sweet_tea/singleton_factory.py
@classmethod
def __same_request(cls, requested: Any, existing: Any) -> bool:
    """
    Decide whether two reduced configurations ask for the same thing.

    Plain ``==`` answers that wrongly in two places, and both rulings here are
    deliberate (see SWE-29):

    - **NaN is the same request as NaN.** That ``float("nan") != float("nan")`` is
      a statement about arithmetic, not about intent: a caller who passes NaN twice
      asked for the same configuration both times. ``==`` already agreed whenever
      the two calls happened to share one NaN object, since container comparison
      short-circuits on identity, so reflexivity was never really the rule — only
      an accident of which object the caller was holding.
    - **1, 1.0 and True are different requests.** ``==`` conflates them, but they
      arrive at the constructor as different types and behave differently once
      there, and ``0`` passed where ``False`` was meant is a plausible mistake worth
      reporting. Numbers are therefore compared type-sensitively. Where a class
      declares ``__configuration__`` the schema has already coerced both spellings
      to the field's own type before this is reached, so an equivalence somebody
      declared is honoured and one nobody declared is not.

    Dicts, lists and tuples are walked so that both rulings hold at any depth.
    Everything else is left to the value's own ``==``. Identity is taken as equality
    first, which is what lets an object with no useful ``==`` compare equal to
    itself.

    Args:
        requested: What this call would build with.
        existing: The recorded basis of the cached instance.

    Returns:
        True when the two are the same request.

    Raises:
        Exception: Whatever a value's own ``==`` raises; the caller reads that as
            undecidable drift.
    """
    if requested is existing:
        return True

    if isinstance(requested, (int, float, complex)) and isinstance(
        existing, (int, float, complex)
    ):
        if type(requested) is not type(existing):
            return False
        if (
            isinstance(requested, float)
            # Already implied by the matching types above; spelled out because the
            # type checker reads the narrowing of each name separately.
            and isinstance(existing, float)
            and math.isnan(requested)
            and math.isnan(existing)
        ):
            return True
        return bool(requested == existing)

    if isinstance(requested, dict) and isinstance(existing, dict):
        if requested.keys() != existing.keys():
            return False
        return all(
            cls.__same_request(value, existing[name])
            for name, value in requested.items()
        )

    if isinstance(requested, (list, tuple)) and isinstance(existing, (list, tuple)):
        # A list and a tuple of the same items are not the same request, which is
        # also what == says; the length check is only to keep zip from truncating.
        if type(requested) is not type(existing) or len(requested) != len(existing):
            return False
        return all(
            cls.__same_request(left, right)
            for left, right in zip(requested, existing)
        )

    return bool(requested == existing)

__snapshot(kwargs) classmethod

Copy a comparison basis away from the caller's own objects.

The recorded basis has to keep answering what the first caller asked for, however long ago. A shallow copy cannot: it keeps the caller's own containers, so a later mutation of one rewrites history. That cuts both ways — a caller who passes an equal configuration gets blamed for a mutation someone else made, and a caller who mutates a nested value and asks again is told nothing, because both sides are reading the one object (see SWE-29).

Values are copied one at a time rather than the mapping as a whole, because copy.deepcopy raises on plenty of ordinary objects — an open file, a lock, a socket, some C extension types. A value that refuses to copy is kept by reference instead, which is no worse for that value than the old behaviour and leaves every other key in the configuration properly snapshotted; the alternative, discarding the whole basis, would cost the warning entirely for any configuration carrying a lock. Copying per value also drops any aliasing between two values of one configuration, which equality does not look at.

Parameters:

Name Type Description Default
kwargs dict[str, Any] | None

The keyword arguments to record, as :meth:__reduced computed them, or None when the configuration would not reduce at all.

required

Returns:

Type Description
dict[str, Any] | None

A snapshot safe to keep, or None when there were no keyword arguments to

dict[str, Any] | None

record — which records drift against this instance as undecidable.

Source code in sweet_tea/singleton_factory.py
@classmethod
def __snapshot(cls, kwargs: dict[str, Any] | None) -> dict[str, Any] | None:
    """
    Copy a comparison basis away from the caller's own objects.

    The recorded basis has to keep answering what the first caller asked for,
    however long ago. A shallow copy cannot: it keeps the caller's own containers,
    so a later mutation of one rewrites history. That cuts both ways — a caller who
    passes an equal configuration gets blamed for a mutation someone else made, and
    a caller who mutates a nested value and asks again is told nothing, because both
    sides are reading the one object (see SWE-29).

    Values are copied one at a time rather than the mapping as a whole, because
    ``copy.deepcopy`` raises on plenty of ordinary objects — an open file, a lock, a
    socket, some C extension types. A value that refuses to copy is kept by
    reference instead, which is no worse for that value than the old behaviour and
    leaves every other key in the configuration properly snapshotted; the
    alternative, discarding the whole basis, would cost the warning entirely for any
    configuration carrying a lock. Copying per value also drops any aliasing between
    two values of one configuration, which equality does not look at.

    Args:
        kwargs: The keyword arguments to record, as :meth:`__reduced` computed them,
            or None when the configuration would not reduce at all.

    Returns:
        A snapshot safe to keep, or None when there were no keyword arguments to
        record — which records drift against this instance as undecidable.
    """
    if kwargs is None:
        # The configuration would not reduce, so there is nothing a later call can
        # be compared against. Recording None is what makes drift against this
        # instance undecidable for good rather than guessed at.
        return None

    snapshot: dict[str, Any] = {}
    for name, value in kwargs.items():
        try:
            snapshot[name] = copy.deepcopy(value)
        except Exception:
            snapshot[name] = value
    return snapshot

__warn_on_configuration_drift(cache_key, entry, configuration) classmethod

Report a configuration that arrived too late to take effect.

A cached singleton is never rebuilt — other holders rely on the identity — so a configuration passed to a later call is discarded. That is the right behaviour and the wrong silence: a caller who believes they configured something gets an instance configured by whoever called first (see SWE-14).

Nothing is said when the caller passed no configuration, since that is the ordinary way to fetch an existing singleton. An empty dict says the same thing and is treated the same way: construction maps it and None alike to no keyword arguments, and someone writing configuration=options.get("cfg", {}) is fetching rather than configuring. Warning there was the false positive most likely to get the whole category filtered out (see SWE-29).

Parameters:

Name Type Description Default
cache_key tuple[str, str, str]

Identity of the cached entry.

required
entry Entry

The resolved entry, which carries the class whose declared configuration model the comparison is made through.

required
configuration dict[str, Any] | BaseModel | None

What this call passed.

required
Source code in sweet_tea/singleton_factory.py
@classmethod
def __warn_on_configuration_drift(
    cls,
    cache_key: tuple[str, str, str],
    entry: Entry,
    configuration: dict[str, Any] | BaseModel | None,
) -> None:
    """
    Report a configuration that arrived too late to take effect.

    A cached singleton is never rebuilt — other holders rely on the identity — so a
    configuration passed to a later call is discarded. That is the right behaviour
    and the wrong silence: a caller who believes they configured something gets an
    instance configured by whoever called first (see SWE-14).

    Nothing is said when the caller passed no configuration, since that is the
    ordinary way to fetch an existing singleton. An empty dict says the same thing
    and is treated the same way: construction maps it and ``None`` alike to no
    keyword arguments, and someone writing ``configuration=options.get("cfg", {})``
    is fetching rather than configuring. Warning there was the false positive most
    likely to get the whole category filtered out (see SWE-29).

    Args:
        cache_key: Identity of the cached entry.
        entry: The resolved entry, which carries the class whose declared
            configuration model the comparison is made through.
        configuration: What this call passed.
    """
    if configuration is None or (
        isinstance(configuration, dict) and not configuration
    ):
        return

    if not cls.__drifted(cache_key, entry, configuration):
        return

    warnings.warn(
        f"Ignoring the configuration passed for singleton {cache_key[0]!r}: an "
        f"instance is already cached and is not rebuilt, so the configuration from "
        f"the first call still applies. Use Factory for per-call configuration, or "
        f"SingletonFactory.pop to discard the cached instance first.",
        SweetTeaWarning,
        # 1 is this helper, 2 is create, 3 is the caller whose configuration was
        # dropped — which is the line worth pointing at.
        stacklevel=3,
    )

__warn_on_disowned_instance(cache_key) classmethod

Report an instance that was constructed but not cached.

A dropped instance is worth a warning rather than a log line, because the caller is left holding something the factory has disowned: it is not the singleton for its key, nothing else will ever be handed it, and the next create for that key constructs a second object the holder of this one has no way to learn about. Two objects that were both, briefly, "the" singleton is precisely the situation this class exists to prevent, and the reset is what caused it — so the report belongs with whoever's call lost, not only in a log nobody reads after the fact.

Returning the instance anyway is deliberate. The construction finished, the object is valid, and raising instead would turn a caller's correct call into a failure because another thread reset the factory while it ran. A caller who would rather fail has warnings.simplefilter("error", SweetTeaWarning), which is the same switch the rest of this library hands them.

Parameters:

Name Type Description Default
cache_key tuple[str, str, str]

Identity of the entry whose instance was dropped.

required
Source code in sweet_tea/singleton_factory.py
@classmethod
def __warn_on_disowned_instance(cls, cache_key: tuple[str, str, str]) -> None:
    """
    Report an instance that was constructed but not cached.

    A dropped instance is worth a warning rather than a log line, because the
    caller is left holding something the factory has disowned: it is not the
    singleton for its key, nothing else will ever be handed it, and the next
    ``create`` for that key constructs a second object the holder of this one has
    no way to learn about. Two objects that were both, briefly, "the" singleton is
    precisely the situation this class exists to prevent, and the reset is what
    caused it — so the report belongs with whoever's call lost, not only in a log
    nobody reads after the fact.

    Returning the instance anyway is deliberate. The construction finished, the
    object is valid, and raising instead would turn a caller's correct call into a
    failure because another thread reset the factory while it ran. A caller who
    would rather fail has ``warnings.simplefilter("error", SweetTeaWarning)``,
    which is the same switch the rest of this library hands them.

    Args:
        cache_key: Identity of the entry whose instance was dropped.
    """
    warnings.warn(
        f"The singleton instance just constructed for {cache_key[0]!r} was not "
        f"cached: SingletonFactory.clear() or pop() ran while it was being built, "
        f"so it was constructed against state that has since been reset. This "
        f"call returns it, but the factory does not hold it and the next create() "
        f"for this key will construct a new one — so this instance is not shared "
        f"with anything. Reset the factory before the constructions that depend "
        f"on it, not during them.",
        SweetTeaWarning,
        # 1 is this helper, 2 is create, 3 is the caller whose instance was
        # dropped — which is the line worth pointing at.
        stacklevel=3,
    )

clear() classmethod

Remove all registered instances.

This is primarily useful for testing or resetting the factory state.

The construction locks are deliberately left in place. Dropping them could not strand a construction — a lock a thread holds stays alive through that thread's own reference — but that was never the danger. The danger was the second lock: with the mapping emptied, the next caller for a key under construction created a fresh lock for it, took that one unopposed, and ran the constructor a second time in parallel with the first. Two instances of a singleton, from a reset API, is the one outcome this class must not have (SWE-39). The locks are keyed by resolved cache key and so bounded by the registry, which costs little to keep.

A construction already running is not cancelled either — nothing can cancel it — but it no longer installs its result over this reset. Clearing bumps the epoch every in-flight construction sampled before it began, so a construction that finishes after this call returns its instance to its caller with a warning and caches nothing (SWE-45). Without that, list_singletons() went [] and then back to ['slow'] with no create in between, holding an instance built against the registry as it was before the reset.

Source code in sweet_tea/singleton_factory.py
@classmethod
def clear(cls) -> None:
    """
    Remove all registered instances.

    This is primarily useful for testing or resetting the factory state.

    The construction locks are deliberately left in place. Dropping them could not
    strand a construction — a lock a thread holds stays alive through that thread's
    own reference — but that was never the danger. The danger was the *second* lock:
    with the mapping emptied, the next caller for a key under construction created a
    fresh lock for it, took that one unopposed, and ran the constructor a second time
    in parallel with the first. Two instances of a singleton, from a reset API, is
    the one outcome this class must not have (SWE-39). The locks are keyed by
    resolved cache key and so bounded by the registry, which costs little to keep.

    A construction already running is not cancelled either — nothing can cancel it
    — but it no longer installs its result over this reset. Clearing bumps the
    epoch every in-flight construction sampled before it began, so a construction
    that finishes after this call returns its instance to its caller with a warning
    and caches nothing (SWE-45). Without that, ``list_singletons()`` went ``[]``
    and then back to ``['slow']`` with no ``create`` in between, holding an
    instance built against the registry as it was before the reset.
    """
    with cls.__lock:
        count = len(cls.__instances)
        cls.__instances.clear()
        cls.__configurations.clear()
        # Emptied rather than bumped per key: every token in flight carries the
        # old epoch, so it loses whatever its per-key count says, and the mapping
        # starts again at nothing instead of growing across resets.
        cls.__clear_epoch += 1
        cls.__removals.clear()
        cls._logger.info(f"Cleared {count} singleton instances")

create(key, library='', label='', configuration=None) classmethod

Get an existing singleton instance or create a new one if it doesn't exist.

This method provides lazy initialization - instances are created only when first requested. Subsequent calls with the same key will return the same cached instance.

A :meth:clear or a :meth:pop of this key that lands while the constructor is running is honoured over this call: the instance is returned to the caller but not cached, with a warning, since an instance built against state that has since been reset must not install itself over the reset (SWE-45).

Parameters:

Name Type Description Default
key str

Name to reference the class from the registry.

required
library str

Optional library filter for the class.

''
label str

Optional label filter for the class.

''
configuration dict[str, Any] | BaseModel | None

Configuration parameters as keyword arguments, given as a dict or a pydantic model whose fields are spread into the constructor.

None

Returns:

Type Description
Any

The existing singleton instance, or a newly created instance — registered,

Any

unless the factory was reset while it was being constructed.

Raises:

Type Description
SweetTeaError

If the key is not found in the registry, filters don't match, or the key is already being constructed by the calling thread or by a thread this call would deadlock against (see SWE-36).

Source code in sweet_tea/singleton_factory.py
@classmethod
def create(
    cls,
    key: str,
    library: str = "",
    label: str = "",
    configuration: dict[str, Any] | BaseModel | None = None,
) -> Any:
    """
    Get an existing singleton instance or create a new one if it doesn't exist.

    This method provides lazy initialization - instances are created only when first requested.
    Subsequent calls with the same key will return the same cached instance.

    A :meth:`clear` or a :meth:`pop` of this key that lands while the constructor
    is running is honoured over this call: the instance is returned to the caller
    but not cached, with a warning, since an instance built against state that has
    since been reset must not install itself over the reset (SWE-45).

    Args:
        key: Name to reference the class from the registry.
        library: Optional library filter for the class.
        label: Optional label filter for the class.
        configuration: Configuration parameters as keyword arguments, given as a
            dict or a pydantic model whose fields are spread into the constructor.

    Returns:
        The existing singleton instance, or a newly created instance — registered,
        unless the factory was reset while it was being constructed.

    Raises:
        SweetTeaError: If the key is not found in the registry, filters don't
            match, or the key is already being constructed by the calling thread
            or by a thread this call would deadlock against (see SWE-36).
    """
    # Resolved before this factory's lock is taken. Resolution imports, and
    # taking both locks across an import is what let a module body that creates a
    # singleton deadlock against a singleton whose constructor looks one up
    # (SWE-22). The cache is still keyed on the resolved entry rather than on the
    # caller's spelling, which is what makes every spelling share one slot
    # (SWE-5).
    entry = cls._select_entry(cls._find_entries(key), key, library, label)
    cache_key = (entry.key, entry.library, entry.label)

    with cls.__lock:
        cached = cache_key in cls.__instances
        if cached:
            cached_instance = cls.__instances[cache_key]
        else:
            construction_lock = cls.__construction_locks.setdefault(
                cache_key, threading.Lock()
            )

    if cached:
        # Reported with no factory lock held, for the same reason construction is
        # not: judging drift applies a declared __configuration__, and a pydantic
        # validator is as much arbitrary code as a constructor (SWE-22, SWE-29).
        cls.__warn_on_configuration_drift(cache_key, entry, configuration)
        return cached_instance

    with cls.__admit_construction(cache_key, construction_lock):
        # Re-checked now that this thread owns construction for the key: another
        # thread may have finished between the miss above and this point.
        with cls.__lock:
            cached = cache_key in cls.__instances
            if cached:
                cached_instance = cls.__instances[cache_key]
            # Read here, in the same hold as the last look at the cache: from this
            # point on this call is building an instance of the registry as it is
            # now, and a clear() after it must not be undone by the result landing
            # (SWE-45). Taken before the configuration is applied rather than
            # after, because applying it is already this call's work.
            token = cls.__commit_token(cache_key)

        if cached:
            # Again outside the factory lock. The construction lock is still held,
            # which costs nothing: the instance exists now, so a call this helper
            # provoked for the same key takes the cache-hit path above and never
            # reaches for this lock.
            cls.__warn_on_configuration_drift(cache_key, entry, configuration)
            return cached_instance

        # Any declared __configuration__ is applied once, here, and construction is
        # handed the result. Both want it: construction builds from the validated
        # model, and the basis has to be of what construction would use rather than
        # of what the caller typed (SWE-29). Computing the basis by validating a
        # second time ran the consumer's validators twice for every singleton built,
        # and a validator is consumer code — it may be slow, and it may count
        # (SWE-42). There is nothing to repeat, because _validate_configuration
        # takes an instance of the declared model as already valid, so passing the
        # applied configuration on costs construction nothing.
        #
        # Whatever the schema raises is left to propagate. It would have come out of
        # _construct a moment later on the same configuration, so this hands the
        # caller nothing they were not already going to see — and the alternative,
        # swallowing it, would cache an instance built from a configuration the
        # class refused.
        #
        # The class is read through _class_of, which is what construction reads and
        # which resolves a lazy entry by importing. That is allowed here and nowhere
        # else in this comparison: no factory lock is held on this path, and
        # _construct is about to import the same module anyway. The cache-hit path
        # has no such licence — see __comparable.
        applied = cls.__applied(entry, cls._class_of(entry), configuration)

        # Recorded before the constructor runs, deliberately. A constructor is
        # entitled to mutate what it was handed — appending to a list it was given
        # is ordinary — and a basis captured afterwards would hold the constructor's
        # own edit. The next caller passing the configuration they actually passed
        # would then be blamed for a difference the first call introduced (SWE-29).
        comparison_basis = cls.__snapshot(cls.__reduced(applied))

        # No factory lock held here, by design: a constructor runs arbitrary code
        # and may itself resolve or create. The construction lock still admits
        # exactly one thread per key, so the single-instance guarantee holds. A
        # constructor that creates some *other* singleton is ordinary dependency
        # injection and proceeds; one that comes back for this key is a cycle, and
        # the admission above has it on this thread's stack so the nested call
        # raises instead of blocking on the lock this thread holds (SWE-36).
        new_instance = cls._construct(entry, applied)

        with cls.__lock:
            # The commit is where the race with clear() and pop() is settled,
            # because it is the only place left: the construction above cannot be
            # cancelled and cannot be run under this lock. An instance whose token
            # has moved was built against a registry that has since been reset, so
            # caching it would resurrect a key the caller was told was gone —
            # list_singletons() going [] and then ['slow'] with no create() in
            # between — and would hand the next caller an instance configured
            # before the reset (SWE-45).
            disowned = cls.__commit_token(cache_key) != token
            if not disowned:
                cls.__instances[cache_key] = new_instance
                cls.__configurations[cache_key] = comparison_basis
                cls._logger.info(
                    f"Created and registered singleton instance: {entry.key}"
                )
            else:
                cls._logger.info(
                    f"Discarded a singleton instance constructed across a reset: "
                    f"{entry.key}"
                )

    # Warned with neither lock held, and after the construction lock is released:
    # a filter that escalates SweetTeaWarning raises from here, and must not do so
    # holding a lock or leaving the key unconstructible. That escalation is the
    # right behaviour for whoever asks for it — a reset losing a construction is
    # exactly the kind of thing a strict caller wants to fail on — and it is the
    # reason the instance is returned on the line below rather than here.
    if disowned:
        cls.__warn_on_disowned_instance(cache_key)

    return new_instance

list_keys() classmethod

Get a list of all class keys that can be created.

Returns:

Type Description
list[str]

List of available class keys from the registry in alphabetical order.

Source code in sweet_tea/singleton_factory.py
@classmethod
def list_keys(cls) -> list[str]:
    """
    Get a list of all class keys that can be created.

    Returns:
        List of available class keys from the registry in alphabetical order.
    """
    # Return keys from the Registry that can be created
    entries = cls._registry.entries()
    return sorted([entry.key for entry in entries])

list_singletons() classmethod

Get a list of all cached singleton instance keys.

Returns:

Type Description
list[str]

List of cached singleton keys in alphabetical order. Entries that share a

list[str]

key across libraries or labels appear once per cached instance.

Source code in sweet_tea/singleton_factory.py
@classmethod
def list_singletons(cls) -> list[str]:
    """
    Get a list of all cached singleton instance keys.

    Returns:
        List of cached singleton keys in alphabetical order. Entries that share a
        key across libraries or labels appear once per cached instance.
    """
    with cls.__lock:
        return sorted(entry_key for entry_key, _, _ in cls.__instances)

pop(key, library='', label='') classmethod

Remove and return a cached instance.

Resolves through the same path as :meth:create, so any spelling of the key removes the instance that spelling would have returned.

A construction of this key that began before this call does not cache its result afterwards, for the reason :meth:clear gives: an instance built before a removal must not undo it (SWE-45). Only this key is affected — a construction of some other singleton is none of this call's business.

Parameters:

Name Type Description Default
key str

The key of the instance to remove, in any supported spelling.

required
library str

Optional library filter, matching the one passed to create.

''
label str

Optional label filter, matching the one passed to create.

''

Returns:

Type Description
Any

The removed instance.

Raises:

Type Description
SweetTeaError

If no instance is cached for the given key.

Source code in sweet_tea/singleton_factory.py
@classmethod
def pop(cls, key: str, library: str = "", label: str = "") -> Any:
    """
    Remove and return a cached instance.

    Resolves through the same path as :meth:`create`, so any spelling of the key
    removes the instance that spelling would have returned.

    A construction of this key that began before this call does not cache its
    result afterwards, for the reason :meth:`clear` gives: an instance built before
    a removal must not undo it (SWE-45). Only this key is affected — a construction
    of some other singleton is none of this call's business.

    Args:
        key: The key of the instance to remove, in any supported spelling.
        library: Optional library filter, matching the one passed to create.
        label: Optional label filter, matching the one passed to create.

    Returns:
        The removed instance.

    Raises:
        SweetTeaError: If no instance is cached for the given key.
    """
    with cls.__lock:
        try:
            entry = cls._select_entry(cls._find_entries(key), key, library, label)
        except SweetTeaError:
            # Unresolvable keys and resolvable-but-uncached ones report the same
            # way; from the caller's side both mean "nothing to remove".
            entry = None

        cache_key = (
            (entry.key, entry.library, entry.label) if entry is not None else None
        )

        if cache_key is None or cache_key not in cls.__instances:
            raise SweetTeaError(
                f"No singleton instance registered for key '{key}'. "
                f"Available keys: {cls.list_singletons()}"
            )

        instance = cls.__instances.pop(cache_key)
        cls.__configurations.pop(cache_key, None)

        # The same guard clear() gets, narrowed to this key: a construction that
        # began before this removal must not cache its result after it (SWE-45).
        # Only this key's count moves, so an unrelated construction is not disowned
        # by the removal of a neighbour. Bounded by the registry, as the
        # construction locks are.
        cls.__removals[cache_key] = cls.__removals.get(cache_key, 0) + 1

        # Python's garbage collector will handle destruction automatically
        # when all references are removed

        # Reported off cache_key rather than entry: the None check above narrows
        # cache_key but not entry, and both carry the same resolved key.
        cls._logger.info(f"Removed singleton instance: {key} (key: {cache_key[0]})")
        return instance

Methods

create()

sweet_tea.singleton_factory.SingletonFactory.create(key, library='', label='', configuration=None) classmethod

Get an existing singleton instance or create a new one if it doesn't exist.

This method provides lazy initialization - instances are created only when first requested. Subsequent calls with the same key will return the same cached instance.

A :meth:clear or a :meth:pop of this key that lands while the constructor is running is honoured over this call: the instance is returned to the caller but not cached, with a warning, since an instance built against state that has since been reset must not install itself over the reset (SWE-45).

Parameters:

Name Type Description Default
key str

Name to reference the class from the registry.

required
library str

Optional library filter for the class.

''
label str

Optional label filter for the class.

''
configuration dict[str, Any] | BaseModel | None

Configuration parameters as keyword arguments, given as a dict or a pydantic model whose fields are spread into the constructor.

None

Returns:

Type Description
Any

The existing singleton instance, or a newly created instance — registered,

Any

unless the factory was reset while it was being constructed.

Raises:

Type Description
SweetTeaError

If the key is not found in the registry, filters don't match, or the key is already being constructed by the calling thread or by a thread this call would deadlock against (see SWE-36).

Source code in sweet_tea/singleton_factory.py
@classmethod
def create(
    cls,
    key: str,
    library: str = "",
    label: str = "",
    configuration: dict[str, Any] | BaseModel | None = None,
) -> Any:
    """
    Get an existing singleton instance or create a new one if it doesn't exist.

    This method provides lazy initialization - instances are created only when first requested.
    Subsequent calls with the same key will return the same cached instance.

    A :meth:`clear` or a :meth:`pop` of this key that lands while the constructor
    is running is honoured over this call: the instance is returned to the caller
    but not cached, with a warning, since an instance built against state that has
    since been reset must not install itself over the reset (SWE-45).

    Args:
        key: Name to reference the class from the registry.
        library: Optional library filter for the class.
        label: Optional label filter for the class.
        configuration: Configuration parameters as keyword arguments, given as a
            dict or a pydantic model whose fields are spread into the constructor.

    Returns:
        The existing singleton instance, or a newly created instance — registered,
        unless the factory was reset while it was being constructed.

    Raises:
        SweetTeaError: If the key is not found in the registry, filters don't
            match, or the key is already being constructed by the calling thread
            or by a thread this call would deadlock against (see SWE-36).
    """
    # Resolved before this factory's lock is taken. Resolution imports, and
    # taking both locks across an import is what let a module body that creates a
    # singleton deadlock against a singleton whose constructor looks one up
    # (SWE-22). The cache is still keyed on the resolved entry rather than on the
    # caller's spelling, which is what makes every spelling share one slot
    # (SWE-5).
    entry = cls._select_entry(cls._find_entries(key), key, library, label)
    cache_key = (entry.key, entry.library, entry.label)

    with cls.__lock:
        cached = cache_key in cls.__instances
        if cached:
            cached_instance = cls.__instances[cache_key]
        else:
            construction_lock = cls.__construction_locks.setdefault(
                cache_key, threading.Lock()
            )

    if cached:
        # Reported with no factory lock held, for the same reason construction is
        # not: judging drift applies a declared __configuration__, and a pydantic
        # validator is as much arbitrary code as a constructor (SWE-22, SWE-29).
        cls.__warn_on_configuration_drift(cache_key, entry, configuration)
        return cached_instance

    with cls.__admit_construction(cache_key, construction_lock):
        # Re-checked now that this thread owns construction for the key: another
        # thread may have finished between the miss above and this point.
        with cls.__lock:
            cached = cache_key in cls.__instances
            if cached:
                cached_instance = cls.__instances[cache_key]
            # Read here, in the same hold as the last look at the cache: from this
            # point on this call is building an instance of the registry as it is
            # now, and a clear() after it must not be undone by the result landing
            # (SWE-45). Taken before the configuration is applied rather than
            # after, because applying it is already this call's work.
            token = cls.__commit_token(cache_key)

        if cached:
            # Again outside the factory lock. The construction lock is still held,
            # which costs nothing: the instance exists now, so a call this helper
            # provoked for the same key takes the cache-hit path above and never
            # reaches for this lock.
            cls.__warn_on_configuration_drift(cache_key, entry, configuration)
            return cached_instance

        # Any declared __configuration__ is applied once, here, and construction is
        # handed the result. Both want it: construction builds from the validated
        # model, and the basis has to be of what construction would use rather than
        # of what the caller typed (SWE-29). Computing the basis by validating a
        # second time ran the consumer's validators twice for every singleton built,
        # and a validator is consumer code — it may be slow, and it may count
        # (SWE-42). There is nothing to repeat, because _validate_configuration
        # takes an instance of the declared model as already valid, so passing the
        # applied configuration on costs construction nothing.
        #
        # Whatever the schema raises is left to propagate. It would have come out of
        # _construct a moment later on the same configuration, so this hands the
        # caller nothing they were not already going to see — and the alternative,
        # swallowing it, would cache an instance built from a configuration the
        # class refused.
        #
        # The class is read through _class_of, which is what construction reads and
        # which resolves a lazy entry by importing. That is allowed here and nowhere
        # else in this comparison: no factory lock is held on this path, and
        # _construct is about to import the same module anyway. The cache-hit path
        # has no such licence — see __comparable.
        applied = cls.__applied(entry, cls._class_of(entry), configuration)

        # Recorded before the constructor runs, deliberately. A constructor is
        # entitled to mutate what it was handed — appending to a list it was given
        # is ordinary — and a basis captured afterwards would hold the constructor's
        # own edit. The next caller passing the configuration they actually passed
        # would then be blamed for a difference the first call introduced (SWE-29).
        comparison_basis = cls.__snapshot(cls.__reduced(applied))

        # No factory lock held here, by design: a constructor runs arbitrary code
        # and may itself resolve or create. The construction lock still admits
        # exactly one thread per key, so the single-instance guarantee holds. A
        # constructor that creates some *other* singleton is ordinary dependency
        # injection and proceeds; one that comes back for this key is a cycle, and
        # the admission above has it on this thread's stack so the nested call
        # raises instead of blocking on the lock this thread holds (SWE-36).
        new_instance = cls._construct(entry, applied)

        with cls.__lock:
            # The commit is where the race with clear() and pop() is settled,
            # because it is the only place left: the construction above cannot be
            # cancelled and cannot be run under this lock. An instance whose token
            # has moved was built against a registry that has since been reset, so
            # caching it would resurrect a key the caller was told was gone —
            # list_singletons() going [] and then ['slow'] with no create() in
            # between — and would hand the next caller an instance configured
            # before the reset (SWE-45).
            disowned = cls.__commit_token(cache_key) != token
            if not disowned:
                cls.__instances[cache_key] = new_instance
                cls.__configurations[cache_key] = comparison_basis
                cls._logger.info(
                    f"Created and registered singleton instance: {entry.key}"
                )
            else:
                cls._logger.info(
                    f"Discarded a singleton instance constructed across a reset: "
                    f"{entry.key}"
                )

    # Warned with neither lock held, and after the construction lock is released:
    # a filter that escalates SweetTeaWarning raises from here, and must not do so
    # holding a lock or leaving the key unconstructible. That escalation is the
    # right behaviour for whoever asks for it — a reset losing a construction is
    # exactly the kind of thing a strict caller wants to fail on — and it is the
    # reason the instance is returned on the line below rather than here.
    if disowned:
        cls.__warn_on_disowned_instance(cache_key)

    return new_instance

clear()

sweet_tea.singleton_factory.SingletonFactory.clear() classmethod

Remove all registered instances.

This is primarily useful for testing or resetting the factory state.

The construction locks are deliberately left in place. Dropping them could not strand a construction — a lock a thread holds stays alive through that thread's own reference — but that was never the danger. The danger was the second lock: with the mapping emptied, the next caller for a key under construction created a fresh lock for it, took that one unopposed, and ran the constructor a second time in parallel with the first. Two instances of a singleton, from a reset API, is the one outcome this class must not have (SWE-39). The locks are keyed by resolved cache key and so bounded by the registry, which costs little to keep.

A construction already running is not cancelled either — nothing can cancel it — but it no longer installs its result over this reset. Clearing bumps the epoch every in-flight construction sampled before it began, so a construction that finishes after this call returns its instance to its caller with a warning and caches nothing (SWE-45). Without that, list_singletons() went [] and then back to ['slow'] with no create in between, holding an instance built against the registry as it was before the reset.

Source code in sweet_tea/singleton_factory.py
@classmethod
def clear(cls) -> None:
    """
    Remove all registered instances.

    This is primarily useful for testing or resetting the factory state.

    The construction locks are deliberately left in place. Dropping them could not
    strand a construction — a lock a thread holds stays alive through that thread's
    own reference — but that was never the danger. The danger was the *second* lock:
    with the mapping emptied, the next caller for a key under construction created a
    fresh lock for it, took that one unopposed, and ran the constructor a second time
    in parallel with the first. Two instances of a singleton, from a reset API, is
    the one outcome this class must not have (SWE-39). The locks are keyed by
    resolved cache key and so bounded by the registry, which costs little to keep.

    A construction already running is not cancelled either — nothing can cancel it
    — but it no longer installs its result over this reset. Clearing bumps the
    epoch every in-flight construction sampled before it began, so a construction
    that finishes after this call returns its instance to its caller with a warning
    and caches nothing (SWE-45). Without that, ``list_singletons()`` went ``[]``
    and then back to ``['slow']`` with no ``create`` in between, holding an
    instance built against the registry as it was before the reset.
    """
    with cls.__lock:
        count = len(cls.__instances)
        cls.__instances.clear()
        cls.__configurations.clear()
        # Emptied rather than bumped per key: every token in flight carries the
        # old epoch, so it loses whatever its per-key count says, and the mapping
        # starts again at nothing instead of growing across resets.
        cls.__clear_epoch += 1
        cls.__removals.clear()
        cls._logger.info(f"Cleared {count} singleton instances")

pop()

sweet_tea.singleton_factory.SingletonFactory.pop(key, library='', label='') classmethod

Remove and return a cached instance.

Resolves through the same path as :meth:create, so any spelling of the key removes the instance that spelling would have returned.

A construction of this key that began before this call does not cache its result afterwards, for the reason :meth:clear gives: an instance built before a removal must not undo it (SWE-45). Only this key is affected — a construction of some other singleton is none of this call's business.

Parameters:

Name Type Description Default
key str

The key of the instance to remove, in any supported spelling.

required
library str

Optional library filter, matching the one passed to create.

''
label str

Optional label filter, matching the one passed to create.

''

Returns:

Type Description
Any

The removed instance.

Raises:

Type Description
SweetTeaError

If no instance is cached for the given key.

Source code in sweet_tea/singleton_factory.py
@classmethod
def pop(cls, key: str, library: str = "", label: str = "") -> Any:
    """
    Remove and return a cached instance.

    Resolves through the same path as :meth:`create`, so any spelling of the key
    removes the instance that spelling would have returned.

    A construction of this key that began before this call does not cache its
    result afterwards, for the reason :meth:`clear` gives: an instance built before
    a removal must not undo it (SWE-45). Only this key is affected — a construction
    of some other singleton is none of this call's business.

    Args:
        key: The key of the instance to remove, in any supported spelling.
        library: Optional library filter, matching the one passed to create.
        label: Optional label filter, matching the one passed to create.

    Returns:
        The removed instance.

    Raises:
        SweetTeaError: If no instance is cached for the given key.
    """
    with cls.__lock:
        try:
            entry = cls._select_entry(cls._find_entries(key), key, library, label)
        except SweetTeaError:
            # Unresolvable keys and resolvable-but-uncached ones report the same
            # way; from the caller's side both mean "nothing to remove".
            entry = None

        cache_key = (
            (entry.key, entry.library, entry.label) if entry is not None else None
        )

        if cache_key is None or cache_key not in cls.__instances:
            raise SweetTeaError(
                f"No singleton instance registered for key '{key}'. "
                f"Available keys: {cls.list_singletons()}"
            )

        instance = cls.__instances.pop(cache_key)
        cls.__configurations.pop(cache_key, None)

        # The same guard clear() gets, narrowed to this key: a construction that
        # began before this removal must not cache its result after it (SWE-45).
        # Only this key's count moves, so an unrelated construction is not disowned
        # by the removal of a neighbour. Bounded by the registry, as the
        # construction locks are.
        cls.__removals[cache_key] = cls.__removals.get(cache_key, 0) + 1

        # Python's garbage collector will handle destruction automatically
        # when all references are removed

        # Reported off cache_key rather than entry: the None check above narrows
        # cache_key but not entry, and both carry the same resolved key.
        cls._logger.info(f"Removed singleton instance: {key} (key: {cache_key[0]})")
        return instance

list_keys()

sweet_tea.singleton_factory.SingletonFactory.list_keys() classmethod

Get a list of all class keys that can be created.

Returns:

Type Description
list[str]

List of available class keys from the registry in alphabetical order.

Source code in sweet_tea/singleton_factory.py
@classmethod
def list_keys(cls) -> list[str]:
    """
    Get a list of all class keys that can be created.

    Returns:
        List of available class keys from the registry in alphabetical order.
    """
    # Return keys from the Registry that can be created
    entries = cls._registry.entries()
    return sorted([entry.key for entry in entries])

list_singletons()

sweet_tea.singleton_factory.SingletonFactory.list_singletons() classmethod

Get a list of all cached singleton instance keys.

Returns:

Type Description
list[str]

List of cached singleton keys in alphabetical order. Entries that share a

list[str]

key across libraries or labels appear once per cached instance.

Source code in sweet_tea/singleton_factory.py
@classmethod
def list_singletons(cls) -> list[str]:
    """
    Get a list of all cached singleton instance keys.

    Returns:
        List of cached singleton keys in alphabetical order. Entries that share a
        key across libraries or labels appear once per cached instance.
    """
    with cls.__lock:
        return sorted(entry_key for entry_key, _, _ in cls.__instances)