@@ -80,6 +80,8 @@ class Column(object):
8080
8181 instance_counter = 0
8282
83+ _python_type_hashable = True
84+
8385 primary_key = False
8486 """
8587 bool flag, indicates this column is a primary key. The first primary key defined
@@ -611,7 +613,6 @@ class BaseContainerColumn(Column):
611613
612614 https://cassandra.apache.org/doc/cql3/CQL.html#collections
613615 """
614-
615616 def __init__ (self , types , ** kwargs ):
616617 """
617618 :param types: a sequence of sub types in this collection
@@ -621,14 +622,14 @@ def __init__(self, types, **kwargs):
621622 inheritance_comparator = issubclass if isinstance (t , type ) else isinstance
622623 if not inheritance_comparator (t , Column ):
623624 raise ValidationError ("%s is not a column class" % (t ,))
624- if inheritance_comparator (t , BaseContainerColumn ): # should go away with PYTHON-478
625- raise ValidationError ('container types cannot be nested' )
626625 if t .db_type is None :
627626 raise ValidationError ("%s is an abstract type" % (t ,))
627+ inst = t () if isinstance (t , type ) else t
628+ if isinstance (t , BaseContainerColumn ):
629+ inst ._freeze_db_type ()
630+ instances .append (inst )
628631
629- instances .append (t () if isinstance (t , type ) else t )
630632 self .types = instances
631-
632633 super (BaseContainerColumn , self ).__init__ (** kwargs )
633634
634635 def validate (self , value ):
@@ -642,6 +643,10 @@ def validate(self, value):
642643 def _val_is_null (self , val ):
643644 return not val
644645
646+ def _freeze_db_type (self ):
647+ if not self .db_type .startswith ('frozen' ):
648+ self .db_type = "frozen<%s>" % (self .db_type ,)
649+
645650 @property
646651 def sub_types (self ):
647652 return self .types
@@ -653,24 +658,27 @@ class Set(BaseContainerColumn):
653658
654659 http://www.datastax.com/documentation/cql/3.1/cql/cql_using/use_set_t.html
655660 """
661+
662+ _python_type_hashable = False
663+
656664 def __init__ (self , value_type , strict = True , default = set , ** kwargs ):
657665 """
658666 :param value_type: a column class indicating the types of the value
659667 :param strict: sets whether non set values will be coerced to set
660668 type on validation, or raise a validation error, defaults to True
661669 """
662670 self .strict = strict
663- self .db_type = 'set<{0}>' .format (value_type .db_type )
664-
665671 super (Set , self ).__init__ ((value_type ,), default = default , ** kwargs )
666-
667672 self .value_col = self .types [0 ]
673+ if not self .value_col ._python_type_hashable :
674+ raise ValidationError ("Cannot create a Set with unhashable value type (see PYTHON-494)" )
675+ self .db_type = 'set<{0}>' .format (self .value_col .db_type )
668676
669677 def validate (self , value ):
670678 val = super (Set , self ).validate (value )
671679 if val is None :
672680 return
673- types = (set ,) if self .strict else (set , list , tuple )
681+ types = (set , util . SortedSet ) if self .strict else (set , util . SortedSet , list , tuple )
674682 if not isinstance (val , types ):
675683 if self .strict :
676684 raise ValidationError ('{0} {1} is not a set object' .format (self .column_name , val ))
@@ -679,7 +687,8 @@ def validate(self, value):
679687
680688 if None in val :
681689 raise ValidationError ("{0} None not allowed in a set" .format (self .column_name ))
682-
690+ # TODO: stop doing this conversion because it doesn't support non-hashable collections as keys (cassandra does)
691+ # will need to start using the cassandra.util types in the next major rev (PYTHON-494)
683692 return set (self .value_col .validate (v ) for v in val )
684693
685694 def to_python (self , value ):
@@ -699,15 +708,16 @@ class List(BaseContainerColumn):
699708
700709 http://www.datastax.com/documentation/cql/3.1/cql/cql_using/use_list_t.html
701710 """
711+
712+ _python_type_hashable = False
713+
702714 def __init__ (self , value_type , default = list , ** kwargs ):
703715 """
704716 :param value_type: a column class indicating the types of the value
705717 """
706- self .db_type = 'list<{0}>' .format (value_type .db_type )
707-
708718 super (List , self ).__init__ ((value_type ,), default = default , ** kwargs )
709-
710719 self .value_col = self .types [0 ]
720+ self .db_type = 'list<{0}>' .format (self .value_col .db_type )
711721
712722 def validate (self , value ):
713723 val = super (List , self ).validate (value )
@@ -736,27 +746,33 @@ class Map(BaseContainerColumn):
736746
737747 http://www.datastax.com/documentation/cql/3.1/cql/cql_using/use_map_t.html
738748 """
749+
750+ _python_type_hashable = False
751+
739752 def __init__ (self , key_type , value_type , default = dict , ** kwargs ):
740753 """
741754 :param key_type: a column class indicating the types of the key
742755 :param value_type: a column class indicating the types of the value
743756 """
744-
745- self .db_type = 'map<{0}, {1}>' .format (key_type .db_type , value_type .db_type )
746-
747757 super (Map , self ).__init__ ((key_type , value_type ), default = default , ** kwargs )
748-
749758 self .key_col = self .types [0 ]
750759 self .value_col = self .types [1 ]
751760
761+ if not self .key_col ._python_type_hashable :
762+ raise ValidationError ("Cannot create a Map with unhashable key type (see PYTHON-494)" )
763+
764+ self .db_type = 'map<{0}, {1}>' .format (self .key_col .db_type , self .value_col .db_type )
765+
752766 def validate (self , value ):
753767 val = super (Map , self ).validate (value )
754768 if val is None :
755769 return
756- if not isinstance (val , dict ):
770+ if not isinstance (val , ( dict , util . OrderedMap ) ):
757771 raise ValidationError ('{0} {1} is not a dict object' .format (self .column_name , val ))
758772 if None in val :
759773 raise ValidationError ("{0} None is not allowed in a map" .format (self .column_name ))
774+ # TODO: stop doing this conversion because it doesn't support non-hashable collections as keys (cassandra does)
775+ # will need to start using the cassandra.util types in the next major rev (PYTHON-494)
760776 return dict ((self .key_col .validate (k ), self .value_col .validate (v )) for k , v in val .items ())
761777
762778 def to_python (self , value ):
@@ -771,17 +787,6 @@ def to_database(self, value):
771787 return dict ((self .key_col .to_database (k ), self .value_col .to_database (v )) for k , v in value .items ())
772788
773789
774- class UDTValueManager (BaseValueManager ):
775- @property
776- def changed (self ):
777- return self .value != self .previous_value or (self .value is not None and self .value .has_changed_fields ())
778-
779- def reset_previous_value (self ):
780- if self .value is not None :
781- self .value .reset_changed_fields ()
782- self .previous_value = copy (self .value )
783-
784-
785790class Tuple (BaseContainerColumn ):
786791 """
787792 Stores a fixed-length set of positional values
@@ -792,13 +797,10 @@ def __init__(self, *args, **kwargs):
792797 """
793798 :param args: column types representing tuple composition
794799 """
795-
796- self .db_type = 'tuple<{0}>' .format (', ' .join (typ .db_type for typ in args ))
797-
798800 if not args :
799801 raise ValueError ("Tuple must specify at least one inner type" )
800-
801802 super (Tuple , self ).__init__ (args , ** kwargs )
803+ self .db_type = 'tuple<{0}>' .format (', ' .join (typ .db_type for typ in self .types ))
802804
803805 def validate (self , value ):
804806 val = super (Tuple , self ).validate (value )
@@ -820,6 +822,17 @@ def to_database(self, value):
820822 return tuple (t .to_database (v ) for t , v in zip (self .types , value ))
821823
822824
825+ class UDTValueManager (BaseValueManager ):
826+ @property
827+ def changed (self ):
828+ return self .value != self .previous_value or (self .value is not None and self .value .has_changed_fields ())
829+
830+ def reset_previous_value (self ):
831+ if self .value is not None :
832+ self .value .reset_changed_fields ()
833+ self .previous_value = copy (self .value )
834+
835+
823836class UserDefinedType (Column ):
824837 """
825838 User Defined Type column
0 commit comments