1212
1313__all__ = ["JsonStore" ]
1414
15+ STRING_TYPES = (str ,)
16+ INT_TYPES = (int ,)
17+ if sys .version_info < (3 ,):
18+ STRING_TYPES += (unicode ,)
19+ INT_TYPES += (long ,)
20+ VALUE_TYPES = (bool , int , float , type (None )) + INT_TYPES
21+
1522
1623class JsonStore (object ):
1724 """A class to provide object based access to a JSON file"""
@@ -81,9 +88,10 @@ def __getattr__(self, key):
8188 raise AttributeError (key )
8289
8390 @classmethod
84- def _valid_object (cls , obj , parents = None ):
91+ def _verify_object (cls , obj , parents = None ):
8592 """
86- Determine if the object can be encoded into JSON
93+ Raise an exception if the object is not suitable for assignment.
94+
8795 """
8896 # pylint: disable=unicode-builtin,long-builtin
8997 if isinstance (obj , (dict , list )):
@@ -94,85 +102,100 @@ def _valid_object(cls, obj, parents=None):
94102 parents .append (obj )
95103
96104 if isinstance (obj , dict ):
97- return all (
98- cls ._valid_string (k ) and cls ._valid_object (v , parents )
99- for k , v in obj .items ()
100- )
105+ for k , v in obj .items ():
106+ if not cls ._valid_string (k ):
107+ # this is necessary because of the JSON serialisation
108+ raise TypeError ("a dict has non-string keys" )
109+ cls ._verify_object (v , parents )
101110 elif isinstance (obj , (list , tuple )):
102- return all (cls ._valid_object (o , parents ) for o in obj )
111+ for o in obj :
112+ cls ._verify_object (o , parents )
103113 else :
104114 return cls ._valid_value (obj )
105115
106116 @classmethod
107117 def _valid_value (cls , value ):
108- if isinstance (value , (bool , int , float , type (None ))):
109- return True
110- elif sys .version_info < (3 ,) and isinstance (value , long ):
118+ if isinstance (value , VALUE_TYPES ):
111119 return True
112120 else :
113121 return cls ._valid_string (value )
114122
115123 @classmethod
116124 def _valid_string (cls , value ):
117- if isinstance (value , str ):
125+ if isinstance (value , STRING_TYPES ):
118126 return True
119- elif sys .version_info < (3 ,):
120- return isinstance (value , unicode )
121127 else :
122128 return False
123129
124- def __setattr__ (self , key , value ):
125- if not self ._valid_object (value ):
126- raise AttributeError
127- self ._data [key ] = deepcopy (value )
130+ @classmethod
131+ def _canonical_key (cls , key ):
132+ """Convert a set/get/del key into the canonical form."""
133+ if cls ._valid_string (key ):
134+ return tuple (key .split ("." ))
135+
136+ if isinstance (key , (tuple , list )):
137+ key = tuple (key )
138+ if not key :
139+ raise TypeError ("key must be a string or non-empty tuple/list" )
140+ return key
141+
142+ raise TypeError ("key must be a string or non-empty tuple/list" )
143+
144+ def __setattr__ (self , attr , value ):
145+ self ._verify_object (value )
146+ self ._data [attr ] = deepcopy (value )
128147 self ._do_auto_commit ()
129148
130- def __delattr__ (self , key ):
131- del self ._data [key ]
149+ def __delattr__ (self , attr ):
150+ del self ._data [attr ]
132151
133- def __get_obj (self , full_path ):
134- """
135- Returns the object which is under the given path
136- """
137- if isinstance (full_path , (tuple , list )):
138- steps = full_path
139- else :
140- steps = full_path .split ("." )
152+ def __get_obj (self , steps ):
153+ """Returns the object which is under the given path."""
141154 path = []
142155 obj = self ._data
143- if not full_path :
144- return obj
145156 for step in steps :
146- path .append (step )
157+ if isinstance (obj , dict ) and not self ._valid_string (step ):
158+ # this is necessary because of the JSON serialisation
159+ raise TypeError ("%s is a dict and %s is not a string" % (path , step ))
147160 try :
148161 obj = obj [step ]
149- except KeyError :
150- raise KeyError ("." .join (path ))
162+ except (KeyError , IndexError , TypeError ) as e :
163+ raise type (e )("unable to get %s from %s: %s" % (step , path , e ))
164+ path .append (step )
151165 return obj
152166
153- def __setitem__ (self , name , value ):
154- path , _ , key = name .rpartition ("." )
155- if self ._valid_object (value ):
156- dictionary = self .__get_obj (path )
157- dictionary [key ] = deepcopy (value )
158- self ._do_auto_commit ()
159- else :
160- raise AttributeError
167+ def __setitem__ (self , key , value ):
168+ steps = self ._canonical_key (key )
169+ path , step = steps [:- 1 ], steps [- 1 ]
170+ self ._verify_object (value )
171+ container = self .__get_obj (path )
172+ if isinstance (container , dict ) and not self ._valid_string (step ):
173+ raise TypeError ("%s is a dict and %s is not a string" % (path , step ))
174+ try :
175+ container [step ] = deepcopy (value )
176+ except (IndexError , TypeError ) as e :
177+ raise type (e )("unable to set %s from %s: %s" % (step , path , e ))
178+ self ._do_auto_commit ()
161179
162180 def __getitem__ (self , key ):
163- obj = self .__get_obj (key )
164- if obj is self ._data :
165- raise KeyError
181+ steps = self ._canonical_key (key )
182+ obj = self .__get_obj (steps )
166183 return deepcopy (obj )
167184
168- def __delitem__ (self , name ):
169- if isinstance (name , (tuple , list )):
170- path = name [:- 1 ]
171- key = name [- 1 ]
172- else :
173- path , _ , key = name .rpartition ("." )
185+ def __delitem__ (self , key ):
186+ steps = self ._canonical_key (key )
187+ path , step = steps [:- 1 ], steps [- 1 ]
174188 obj = self .__get_obj (path )
175- del obj [key ]
189+ try :
190+ del obj [step ]
191+ except (KeyError , IndexError , TypeError ) as e :
192+ raise type (e )("unable to delete %s from %s: %s" % (step , path , e ))
176193
177194 def __contains__ (self , key ):
178- return key in self ._data
195+ steps = self ._canonical_key (key )
196+ try :
197+ self .__get_obj (steps )
198+ return True
199+ except (KeyError , IndexError , TypeError ):
200+ # this is rather permissive as the types are dynamic
201+ return False
0 commit comments