66from collections .abc import Mapping , Sequence
77from types import FunctionType , MethodType
88from typing import Any , ClassVar , Optional
9+ from weakref import WeakValueDictionary
910
1011import cython
1112
@@ -541,7 +542,7 @@ def varkwargs(pattern=_any, typehint=EMPTY):
541542 return Parameter (kind = _VAR_KEYWORD , pattern = pattern , typehint = typehint )
542543
543544
544- __create__ = cython .declare (object , type .__call__ )
545+ __type_call__ = cython .declare (object , type .__call__ )
545546if cython .compiled :
546547 from cython .cimports .cpython .object import PyObject_GenericSetAttr as __setattr__
547548else :
@@ -555,6 +556,7 @@ class AnnotableSpec:
555556 initable = cython .declare (cython .bint , visibility = "readonly" )
556557 hashable = cython .declare (cython .bint , visibility = "readonly" )
557558 immutable = cython .declare (cython .bint , visibility = "readonly" )
559+ singleton = cython .declare (cython .bint , visibility = "readonly" )
558560 signature = cython .declare (Signature , visibility = "readonly" )
559561 attributes = cython .declare (dict [str , Attribute ], visibility = "readonly" )
560562 hasattribs = cython .declare (cython .bint , visibility = "readonly" )
@@ -564,44 +566,66 @@ def __init__(
564566 initable : bool ,
565567 hashable : bool ,
566568 immutable : bool ,
569+ singleton : bool ,
567570 signature : Signature ,
568571 attributes : dict [str , Attribute ],
569572 ):
570573 self .initable = initable
571574 self .hashable = hashable
572575 self .immutable = immutable
576+ self .singleton = singleton
573577 self .signature = signature
574578 self .attributes = attributes
575579 self .hasattribs = bool (attributes )
576580
577581 @cython .cfunc
578582 @cython .inline
579583 def new (self , cls : type , args : tuple [Any , ...], kwargs : dict [str , Any ]):
580- ctx : dict [str , Any ] = {}
581584 bound : dict [str , Any ]
582- param : Parameter
583-
584585 if not args and len (kwargs ) == self .signature .length :
585586 bound = kwargs
586587 else :
587588 bound = self .signature .bind (args , kwargs )
588589
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 )
594592 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
605629
606630 @cython .cfunc
607631 @cython .inline
@@ -627,8 +651,7 @@ def init_precomputes(self, this) -> cython.void:
627651class AbstractMeta (type ):
628652 """Base metaclass for many of the ibis core classes.
629653
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.
632655
633656 Support abstract methods without extending `abc.ABCMeta`. While it provides
634657 a reduced feature set compared to `abc.ABCMeta` (no way to register virtual
@@ -639,8 +662,8 @@ class AbstractMeta(type):
639662 __slots__ = ()
640663
641664 def __new__ (metacls , clsname , bases , dct , ** kwargs ):
642- # # enforce slot definitions
643- # dct.setdefault("__slots__", ())
665+ # enforce slot definitions
666+ dct .setdefault ("__slots__" , ())
644667
645668 # construct the class object
646669 cls = super ().__new__ (metacls , clsname , bases , dct , ** kwargs )
@@ -663,6 +686,10 @@ def __new__(metacls, clsname, bases, dct, **kwargs):
663686 return cls
664687
665688
689+ class Abstract (metaclass = AbstractMeta ):
690+ """Base class for many of the ibis core classes, see `AbstractMeta`."""
691+
692+
666693class AnnotableMeta (AbstractMeta ):
667694 def __new__ (
668695 metacls ,
@@ -672,6 +699,7 @@ def __new__(
672699 initable = None ,
673700 hashable = None ,
674701 immutable = None ,
702+ singleton = False ,
675703 allow_coercion = True ,
676704 ** kwargs ,
677705 ):
@@ -682,6 +710,7 @@ def __new__(
682710 is_initable : cython .bint
683711 is_hashable : cython .bint = hashable is True
684712 is_immutable : cython .bint = immutable is True
713+ is_singleton : cython .bint = singleton is True
685714 if initable is None :
686715 is_initable = "__init__" in dct or "__new__" in dct
687716 else :
@@ -713,6 +742,8 @@ def __new__(
713742 traits .append (Hashable )
714743 if immutable :
715744 traits .append (Immutable )
745+ if singleton :
746+ traits .append (Singleton )
716747
717748 # collect type annotations and convert them to patterns
718749 slots : list [str ] = list (dct .pop ("__slots__" , []))
@@ -757,6 +788,7 @@ def __new__(
757788 spec = AnnotableSpec (
758789 initable = is_initable ,
759790 hashable = is_hashable ,
791+ singleton = is_singleton ,
760792 immutable = is_immutable ,
761793 signature = signature ,
762794 attributes = attributes ,
@@ -778,9 +810,14 @@ def __call__(cls, *args, **kwargs):
778810 return spec .new (cython .cast (type , cls ), args , kwargs )
779811
780812
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__" ,)
783818
819+
820+ class Immutable (Abstract ):
784821 def __copy__ (self ):
785822 return self
786823
@@ -794,7 +831,7 @@ def __setattr__(self, name: str, _: Any) -> None:
794831 )
795832
796833
797- class Hashable :
834+ class Hashable ( Abstract ) :
798835 __slots__ = ("__args__" , "__precomputed_hash__" )
799836
800837 def __hash__ (self ) -> int :
@@ -809,13 +846,11 @@ def __eq__(self, other) -> bool:
809846 )
810847
811848
812- class Annotable (metaclass = AnnotableMeta , initable = False ):
849+ class Annotable (Abstract , metaclass = AnnotableMeta , initable = False ):
813850 __argnames__ : ClassVar [tuple [str , ...]]
814851 __match_args__ : ClassVar [tuple [str , ...]]
815852 __signature__ : ClassVar [Signature ]
816853
817- __slots__ = ("__weakref__" ,)
818-
819854 def __init__ (self , ** kwargs ):
820855 spec : AnnotableSpec = self .__spec__
821856 for name , value in kwargs .items ():
0 commit comments