6
6
from collections .abc import Mapping , Sequence
7
7
from types import FunctionType , MethodType
8
8
from typing import Any , ClassVar , Optional
9
+ from weakref import WeakValueDictionary
9
10
10
11
import cython
11
12
@@ -541,7 +542,7 @@ def varkwargs(pattern=_any, typehint=EMPTY):
541
542
return Parameter (kind = _VAR_KEYWORD , pattern = pattern , typehint = typehint )
542
543
543
544
544
- __create__ = cython .declare (object , type .__call__ )
545
+ __type_call__ = cython .declare (object , type .__call__ )
545
546
if cython .compiled :
546
547
from cython .cimports .cpython .object import PyObject_GenericSetAttr as __setattr__
547
548
else :
@@ -555,6 +556,7 @@ class AnnotableSpec:
555
556
initable = cython .declare (cython .bint , visibility = "readonly" )
556
557
hashable = cython .declare (cython .bint , visibility = "readonly" )
557
558
immutable = cython .declare (cython .bint , visibility = "readonly" )
559
+ singleton = cython .declare (cython .bint , visibility = "readonly" )
558
560
signature = cython .declare (Signature , visibility = "readonly" )
559
561
attributes = cython .declare (dict [str , Attribute ], visibility = "readonly" )
560
562
hasattribs = cython .declare (cython .bint , visibility = "readonly" )
@@ -564,44 +566,66 @@ def __init__(
564
566
initable : bool ,
565
567
hashable : bool ,
566
568
immutable : bool ,
569
+ singleton : bool ,
567
570
signature : Signature ,
568
571
attributes : dict [str , Attribute ],
569
572
):
570
573
self .initable = initable
571
574
self .hashable = hashable
572
575
self .immutable = immutable
576
+ self .singleton = singleton
573
577
self .signature = signature
574
578
self .attributes = attributes
575
579
self .hasattribs = bool (attributes )
576
580
577
581
@cython .cfunc
578
582
@cython .inline
579
583
def new (self , cls : type , args : tuple [Any , ...], kwargs : dict [str , Any ]):
580
- ctx : dict [str , Any ] = {}
581
584
bound : dict [str , Any ]
582
- param : Parameter
583
-
584
585
if not args and len (kwargs ) == self .signature .length :
585
586
bound = kwargs
586
587
else :
587
588
bound = self .signature .bind (args , kwargs )
588
589
589
- if self .initable :
590
- # slow initialization calling __init__
591
- for name , param in self .signature .parameters .items ():
592
- bound [name ] = param .pattern .match (bound [name ], ctx )
593
- return __create__ (cls , ** bound )
590
+ if self .singleton or self .initable :
591
+ return self .new_slow (cls , bound )
594
592
else :
595
- # fast initialization directly setting the arguments
596
- this = cls .__new__ (cls )
597
- for name , param in self .signature .parameters .items ():
598
- __setattr__ (this , name , param .pattern .match (bound [name ], ctx ))
599
- # TODO(kszucs): test order ot precomputes and attributes calculations
600
- if self .hashable :
601
- self .init_precomputes (this )
602
- if self .hasattribs :
603
- self .init_attributes (this )
604
- return this
593
+ return self .new_fast (cls , bound )
594
+
595
+ @cython .cfunc
596
+ @cython .inline
597
+ def new_slow (self , cls : type , bound : dict [str , Any ]):
598
+ # slow initialization calling __init__
599
+ ctx : dict [str , Any ] = {}
600
+ param : Parameter
601
+ for name , param in self .signature .parameters .items ():
602
+ bound [name ] = param .pattern .match (bound [name ], ctx )
603
+
604
+ if self .singleton :
605
+ key = (cls , * bound .items ())
606
+ try :
607
+ return cls .__instances__ [key ]
608
+ except KeyError :
609
+ this = __type_call__ (cls , ** bound )
610
+ cls .__instances__ [key ] = this
611
+ return this
612
+
613
+ return __type_call__ (cls , ** bound )
614
+
615
+ @cython .cfunc
616
+ @cython .inline
617
+ def new_fast (self , cls : type , bound : dict [str , Any ]):
618
+ # fast initialization directly setting the arguments
619
+ ctx : dict [str , Any ] = {}
620
+ param : Parameter
621
+ this = cls .__new__ (cls )
622
+ for name , param in self .signature .parameters .items ():
623
+ __setattr__ (this , name , param .pattern .match (bound [name ], ctx ))
624
+ if self .hashable :
625
+ self .init_precomputes (this )
626
+ if self .hasattribs :
627
+ self .init_attributes (this )
628
+ return this
605
629
606
630
@cython .cfunc
607
631
@cython .inline
@@ -627,8 +651,7 @@ def init_precomputes(self, this) -> cython.void:
627
651
class AbstractMeta (type ):
628
652
"""Base metaclass for many of the ibis core classes.
629
653
630
- Enforce the subclasses to define a `__slots__` attribute and provide a
631
- `__create__` classmethod to change the instantiation behavior of the class.
654
+ Enforce the subclasses to define a `__slots__` attribute.
632
655
633
656
Support abstract methods without extending `abc.ABCMeta`. While it provides
634
657
a reduced feature set compared to `abc.ABCMeta` (no way to register virtual
@@ -639,8 +662,8 @@ class AbstractMeta(type):
639
662
__slots__ = ()
640
663
641
664
def __new__ (metacls , clsname , bases , dct , ** kwargs ):
642
- # # enforce slot definitions
643
- # dct.setdefault("__slots__", ())
665
+ # enforce slot definitions
666
+ dct .setdefault ("__slots__" , ())
644
667
645
668
# construct the class object
646
669
cls = super ().__new__ (metacls , clsname , bases , dct , ** kwargs )
@@ -663,6 +686,10 @@ def __new__(metacls, clsname, bases, dct, **kwargs):
663
686
return cls
664
687
665
688
689
+ class Abstract (metaclass = AbstractMeta ):
690
+ """Base class for many of the ibis core classes, see `AbstractMeta`."""
691
+
692
+
666
693
class AnnotableMeta (AbstractMeta ):
667
694
def __new__ (
668
695
metacls ,
@@ -672,6 +699,7 @@ def __new__(
672
699
initable = None ,
673
700
hashable = None ,
674
701
immutable = None ,
702
+ singleton = False ,
675
703
allow_coercion = True ,
676
704
** kwargs ,
677
705
):
@@ -682,6 +710,7 @@ def __new__(
682
710
is_initable : cython .bint
683
711
is_hashable : cython .bint = hashable is True
684
712
is_immutable : cython .bint = immutable is True
713
+ is_singleton : cython .bint = singleton is True
685
714
if initable is None :
686
715
is_initable = "__init__" in dct or "__new__" in dct
687
716
else :
@@ -713,6 +742,8 @@ def __new__(
713
742
traits .append (Hashable )
714
743
if immutable :
715
744
traits .append (Immutable )
745
+ if singleton :
746
+ traits .append (Singleton )
716
747
717
748
# collect type annotations and convert them to patterns
718
749
slots : list [str ] = list (dct .pop ("__slots__" , []))
@@ -757,6 +788,7 @@ def __new__(
757
788
spec = AnnotableSpec (
758
789
initable = is_initable ,
759
790
hashable = is_hashable ,
791
+ singleton = is_singleton ,
760
792
immutable = is_immutable ,
761
793
signature = signature ,
762
794
attributes = attributes ,
@@ -778,9 +810,14 @@ def __call__(cls, *args, **kwargs):
778
810
return spec .new (cython .cast (type , cls ), args , kwargs )
779
811
780
812
781
- class Immutable :
782
- __slots__ = ()
813
+ class Singleton (Abstract ):
814
+ """Cache instances of the class based on instantiation arguments."""
815
+
816
+ __instances__ : Mapping [Any , Self ] = WeakValueDictionary ()
817
+ __slots__ = ("__weakref__" ,)
783
818
819
+
820
+ class Immutable (Abstract ):
784
821
def __copy__ (self ):
785
822
return self
786
823
@@ -794,7 +831,7 @@ def __setattr__(self, name: str, _: Any) -> None:
794
831
)
795
832
796
833
797
- class Hashable :
834
+ class Hashable ( Abstract ) :
798
835
__slots__ = ("__args__" , "__precomputed_hash__" )
799
836
800
837
def __hash__ (self ) -> int :
@@ -809,13 +846,11 @@ def __eq__(self, other) -> bool:
809
846
)
810
847
811
848
812
- class Annotable (metaclass = AnnotableMeta , initable = False ):
849
+ class Annotable (Abstract , metaclass = AnnotableMeta , initable = False ):
813
850
__argnames__ : ClassVar [tuple [str , ...]]
814
851
__match_args__ : ClassVar [tuple [str , ...]]
815
852
__signature__ : ClassVar [Signature ]
816
853
817
- __slots__ = ("__weakref__" ,)
818
-
819
854
def __init__ (self , ** kwargs ):
820
855
spec : AnnotableSpec = self .__spec__
821
856
for name , value in kwargs .items ():
0 commit comments