ソースを参照

Bugfixes + add backreference support in datahandlers (untested)

Yann Weber 8年前
コミット
f572743c09

+ 5
- 0
README.txt ファイルの表示

@@ -0,0 +1,5 @@
1
+Doxygen documentation generation :
2
+	doxygen
3
+
4
+Dynamic code generation :
5
+	python3 scripts/refreshdyn.py examples/em_test.pickle OUTPUTFILE.py

+ 6
- 5
lodel/editorial_model/components.py ファイルの表示

@@ -11,6 +11,7 @@ from lodel.editorial_model.exceptions import *
11 11
 
12 12
 ## @brief Abstract class to represent editorial model components
13 13
 # @see EmClass EmField
14
+# @todo forbid '.' in uid
14 15
 class EmComponent(object):
15 16
     
16 17
     ## @brief Instanciate an EmComponent
@@ -43,8 +44,6 @@ class EmComponent(object):
43 44
 
44 45
 
45 46
 ## @brief Handles editorial model objects classes
46
-#
47
-# @note The inheritance system allow child classes to overwrite parents EmField. But it's maybe not a good idea
48 47
 class EmClass(EmComponent):
49 48
     
50 49
     ## @brief Instanciate a new EmClass
@@ -166,12 +165,14 @@ class EmField(EmComponent):
166 165
     def __init__(self, uid, data_handler, display_name = None, help_text = None, group = None, **handler_kwargs):
167 166
         from lodel.leapi.datahandlers.base_classes import DataHandler
168 167
         super().__init__(uid, display_name, help_text, group)
168
+        ## @brief The data handler name
169 169
         self.data_handler_name = data_handler
170
+        ## @brief The data handler class
170 171
         self.data_handler_cls = DataHandler.from_name(data_handler)
171
-        #if 'data_handler_kwargs' in handler_kwargs:
172
-        #    handler_kwargs = handler_kwargs['data_handler_kwargs']
173
-        self.data_handler_options = handler_kwargs
172
+        ## @brief The data handler instance associated with this EmField
174 173
         self.data_handler_instance = self.data_handler_cls(**handler_kwargs)
174
+        ## @brief Stores data handler instanciation options
175
+        self.data_handler_options = handler_kwargs
175 176
         ## @brief Stores the emclass that contains this field (set by EmClass.add_field() method)
176 177
         self._emclass = None
177 178
 

+ 21
- 0
lodel/editorial_model/model.py ファイルの表示

@@ -39,6 +39,27 @@ class EditorialModel(object):
39 39
             return self.__elt_getter(self.__groups, uid)
40 40
         except KeyError:
41 41
             raise EditorialModelException("EmGroup not found : '%s'" % uid)
42
+    
43
+    ## @brief EmField getter
44
+    # @param uid str : An EmField uid represented by "CLASSUID.FIELDUID"
45
+    # @return Fals or an EmField instance
46
+    #
47
+    # @todo delete it, useless...
48
+    def field(self, uid = None):
49
+        spl = uid.split('.')
50
+        if len(spl) != 2:
51
+            raise ValueError("Malformed EmField identifier : '%s'" % uid)
52
+        cls_uid = spl[0]
53
+        field_uid = spl[1]
54
+        try:
55
+            emclass = self.classes(cls_uid)
56
+        except KeyError:
57
+            return False
58
+        try:
59
+            return emclass.fields(field_uid)
60
+        except KeyError:
61
+            pass
62
+        return False
42 63
 
43 64
     ## @brief Add a class to the editorial model
44 65
     # @param emclass EmClass : the EmClass instance to add

+ 43
- 40
lodel/leapi/datahandlers/base_classes.py ファイルの表示

@@ -34,15 +34,23 @@ class DataHandler(object):
34 34
     #                         designed globally and immutable
35 35
     # @param **args
36 36
     # @throw NotImplementedError if it is instanciated directly
37
-    def __init__(self, internal=False, immutable=False, primary_key = False, **args):
37
+    def __init__(self, **kwargs):
38 38
         if self.__class__ == DataHandler:
39 39
             raise NotImplementedError("Abstract class")
40 40
         
41
-        self.primary_key = primary_key
42
-        self.internal = internal  # Check this value ?
43
-        self.immutable = bool(immutable)
41
+        self.__arguments = kwargs
44 42
 
45
-        for argname, argval in args.items():
43
+        self.nullable = True
44
+        self.uniq = False
45
+        self.immutable = False
46
+        self.primary_key = False
47
+        if 'defaults' in kwargs:
48
+            self.default, error = self.check_data_value(kwargs['default'])
49
+            if error:
50
+                raise error
51
+            del(args['default'])
52
+
53
+        for argname, argval in kwargs.items():
46 54
             setattr(self, argname, argval)
47 55
 
48 56
     ## Fieldtype name
@@ -50,6 +58,10 @@ class DataHandler(object):
50 58
     def name(cls):
51 59
         return cls.__module__.split('.')[-1]
52 60
 
61
+    @classmethod
62
+    def is_reference(cls):
63
+        return issubclass(cls, Reference)
64
+
53 65
     def is_primary_key(self):
54 66
         return self.primary_key
55 67
 
@@ -61,10 +73,12 @@ class DataHandler(object):
61 73
     ## @brief calls the data_field defined _check_data_value() method
62 74
     # @return tuple (value, error|None)
63 75
     def check_data_value(self, value):
64
-        return self._check_data_value(value)
76
+        if value is None:
77
+            if not self.nullable:
78
+                return None, TypeError("'None' value but field is not nullable")
65 79
 
66
-    def _check_data_value(self, value):
67
-        return value, None
80
+            return None, None
81
+        return self._check_data_value(value)
68 82
 
69 83
     ## @brief checks if this class can override the given data handler
70 84
     # @param data_handler DataHandler
@@ -159,37 +173,7 @@ class DataHandler(object):
159 173
 
160 174
 ## @brief Base class for datas data handler (by opposition with references)
161 175
 class DataField(DataHandler):
162
-
163
-    ## @brief Instanciates a new fieldtype
164
-    # @param nullable bool : is None allowed as value ?
165
-    # @param uniq bool : Indicates if a field should handle a uniq value
166
-    # @param primary bool : If true the field is a primary key
167
-    # @param internal str|False: if False, that field is not internal. Other values cans be "autosql" or "internal"
168
-    # @param **kwargs : Other arguments
169
-    # @throw NotImplementedError if called from bad class
170
-    def __init__(self, internal=False, nullable=True, uniq=False, primary=False, **kwargs):
171
-        if self.__class__ == DataField:
172
-            raise NotImplementedError("Abstract class")
173
-
174
-        super().__init__(internal, **kwargs)
175
-
176
-        self.nullable = nullable
177
-        self.uniq = uniq
178
-        self.primary = primary
179
-        if 'defaults' in kwargs:
180
-            self.default, error = self.check_data_value(kwargs['default'])
181
-            if error:
182
-                raise error
183
-            del(args['default'])
184
-
185
-    def check_data_value(self, value):
186
-        if value is None:
187
-            if not self.nullable:
188
-                return None, TypeError("'None' value but field is not nullable")
189
-
190
-            return None, None
191
-        return super().check_data_value(value)
192
-
176
+    pass
193 177
 
194 178
 ## @brief Abstract class for all references
195 179
 #
@@ -199,11 +183,30 @@ class Reference(DataHandler):
199 183
 
200 184
     ## @brief Instanciation
201 185
     # @param allowed_classes list | None : list of allowed em classes if None no restriction
186
+    # @param back_reference tuple | None : tuple containing (EmClass name, EmField name)
202 187
     # @param internal bool : if False, the field is not internal
203 188
     # @param **kwargs : other arguments
204
-    def __init__(self, allowed_classes = None, internal=False, **kwargs):
189
+    def __init__(self, allowed_classes = None, back_reference = None, internal=False, **kwargs):
205 190
         self.__allowed_classes = None if allowed_classes is None else set(allowed_classes)
191
+        if back_reference is not None:
192
+            if len(back_reference) != 2:
193
+                raise ValueError("A tuple (classname, fieldname) expected but got '%s'" % back_reference)
194
+        self.__back_reference = back_reference
206 195
         super().__init__(internal=internal, **kwargs)
196
+    
197
+    @property
198
+    def back_reference(self):
199
+        return self.__back_reference
200
+
201
+    ## @brief Set the back reference for this field.
202
+    # 
203
+    # This method is designed to be called from LeObject child classes
204
+    # at dyncode load. LeObject dynamic childs classes are the objects that are
205
+    # able to fetch python classes from name.
206
+    def _set_back_reference(self, back_reference = None):
207
+        
208
+        pass
209
+        
207 210
 
208 211
     ## @brief Check value
209 212
     # @param value *

+ 2
- 3
lodel/leapi/datahandlers/datas.py ファイルの表示

@@ -65,10 +65,9 @@ class UniqID(Integer):
65 65
 
66 66
     ## @brief A uid field
67 67
     # @param **kwargs
68
-    def __init__(self, is_id_class, **kwargs):
69
-        self._is_id_class = is_id_class
68
+    def __init__(self, **kwargs):
70 69
         kwargs['internal'] = 'automatic'
71
-        super(self.__class__, self).__init__(is_id_class=is_id_class, **kwargs)
70
+        super(self.__class__, self).__init__(primary_key = True, **kwargs)
72 71
 
73 72
     def _check_data_value(self, value):
74 73
         return value, None

+ 1
- 1
lodel/leapi/datahandlers/datas_base.py ファイルの表示

@@ -30,7 +30,7 @@ class Integer(DataField):
30 30
     base_type = 'int'
31 31
 
32 32
     def __init__(self, **kwargs):
33
-        super().__init__(base_type='int', **kwargs)
33
+        super().__init__( **kwargs)
34 34
 
35 35
     def _check_data_value(self, value):
36 36
         error = None

+ 22
- 6
lodel/leapi/lefactory.py ファイルの表示

@@ -9,18 +9,30 @@ from lodel.leapi.datahandlers.base_classes import DataHandler
9 9
 # @param model lodel.editorial_model.model.EditorialModel
10 10
 def dyncode_from_em(model):
11 11
     
12
+    # Generation of LeObject child classes code
12 13
     cls_code, modules, bootstrap_instr = generate_classes(model)
14
+    # Completing bootstrap with back_reference bootstraping
15
+    for leoname in [ LeObject.name2objname(emcls.uid) for emcls in get_classes(model) ]:
16
+        bootstrap_instr += """
17
+{leobject}._backref_init()
18
+""".format(leobject = leoname)
19
+    bootstrap_instr += """
20
+del(LeObject._set__fields)
21
+del(LeObject._backref_init)
22
+"""
23
+
24
+    # Header
13 25
     imports = """from lodel.leapi.leobject import LeObject
14 26
 from lodel.leapi.datahandlers.base_classes import DataField
15 27
 """
16 28
     for module in modules:
17 29
         imports += "import %s\n" % module
18
-
30
+    
31
+    # formating all components of output
19 32
     res_code = """#-*- coding: utf-8 -*-
20 33
 {imports}
21 34
 {classes}
22 35
 {bootstrap_instr}
23
-del(LeObject._set__fields)
24 36
 """.format(
25 37
             imports = imports,
26 38
             classes = cls_code,
@@ -49,6 +61,10 @@ def emclass_sorted_by_deps(emclass_list):
49 61
             return 0
50 62
     return sorted(emclass_list, key = functools.cmp_to_key(emclass_deps_cmp))
51 63
 
64
+## @brief Returns a list of EmClass that will be represented as LeObject child classes
65
+def get_classes(model):
66
+    return [ cls for cls in emclass_sorted_by_deps(model.classes()) if not cls.pure_abstract ]
67
+
52 68
 ## @brief Given an EmField returns the data_handler constructor suitable for dynamic code
53 69
 def data_handler_constructor(emfield):
54 70
     #dh_module_name = DataHandler.module_name(emfield.data_handler_name)+'.DataHandler'
@@ -84,7 +100,7 @@ def generate_classes(model):
84 100
     imports = list()
85 101
     bootstrap = ""
86 102
     # Generating field list for LeObjects generated from EmClass
87
-    for em_class in [ cls for cls in emclass_sorted_by_deps(model.classes()) if not cls.pure_abstract ]:
103
+    for em_class in get_classes(model):
88 104
         uid = list()        # List of fieldnames that are part of the EmClass primary key
89 105
         parents = list()    # List of parents EmClass
90 106
         # Determine pk
@@ -101,9 +117,9 @@ def generate_classes(model):
101 117
         # Dynamic code generation for LeObject childs classes
102 118
         em_cls_code = """
103 119
 class {clsname}({parents}):
104
-    __abstract = {abstract}
105
-    __fields = None
106
-    __uid = {uid_list}
120
+    _abstract = {abstract}
121
+    _fields = None
122
+    _uid = {uid_list}
107 123
 
108 124
 """.format(
109 125
     clsname = LeObject.name2objname(em_class.uid),

+ 58
- 23
lodel/leapi/leobject.py ファイルの表示

@@ -1,5 +1,7 @@
1 1
 #-*- coding: utf-8 -*-
2 2
 
3
+import importlib
4
+
3 5
 class LeApiErrors(Exception):
4 6
     ## @brief Instanciate a new exceptions handling multiple exceptions
5 7
     # @param msg str : Exception message
@@ -64,26 +66,26 @@ class LeObjectValues(object):
64 66
 class LeObject(object):
65 67
  
66 68
     ## @brief boolean that tells if an object is abtract or not
67
-    __abtract = None
69
+    _abstract = None
68 70
     ## @brief A dict that stores DataHandler instances indexed by field name
69
-    __fields = None
71
+    _fields = None
70 72
     ## @brief A tuple of fieldname (or a uniq fieldname) representing uid
71
-    __uid = None 
73
+    _uid = None 
72 74
 
73 75
     ## @brief Construct an object representing an Editorial component
74 76
     # @note Can be considered as EmClass instance
75 77
     def __init__(self, **kwargs):
76
-        if self.__abstract:
78
+        if self._abstract:
77 79
             raise NotImplementedError("%s is abstract, you cannot instanciate it." % self.__class__.__name__ )
78 80
         ## @brief A dict that stores fieldvalues indexed by fieldname
79
-        self.__datas = { fname:None for fname in self.__fields }
81
+        self.__datas = { fname:None for fname in self._fields }
80 82
         ## @brief Store a list of initianilized fields when instanciation not complete else store True
81 83
         self.__initialized = list()
82 84
         ## @brief Datas accessor. Instance of @ref LeObjectValues
83 85
         self.d = LeObjectValues(self.fieldnames, self.set_data, self.data)
84 86
 
85 87
         # Checks that uid is given
86
-        for uid_name in self.__uid:
88
+        for uid_name in self._uid:
87 89
             if uid_name not in kwargs:
88 90
                 raise AttributeError("Cannot instanciate a LeObject without it's identifier")
89 91
             self.__datas[uid_name] = kwargs[uid_name]
@@ -95,7 +97,7 @@ class LeObject(object):
95 97
         err_list = list()
96 98
         for fieldname, fieldval in kwargs.items():
97 99
             if fieldname not in allowed_fieldnames:
98
-                if fieldname in self.__fields:
100
+                if fieldname in self._fields:
99 101
                     err_list.append(
100 102
                         AttributeError("Value given for internal field : '%s'" % fieldname)
101 103
                     )
@@ -123,9 +125,9 @@ class LeObject(object):
123 125
     @classmethod
124 126
     def fieldnames(cls, include_ro = False):
125 127
         if not include_ro:
126
-            return [ fname for fname in self.__fields if not self.__fields[fname].is_internal() ]
128
+            return [ fname for fname in self._fields if not self._fields[fname].is_internal() ]
127 129
         else:
128
-            return list(self.__fields.keys())
130
+            return list(self._fields.keys())
129 131
  
130 132
     @classmethod
131 133
     def name2objname(cls, name):
@@ -136,10 +138,43 @@ class LeObject(object):
136 138
     # @return A data handler instance
137 139
     @classmethod
138 140
     def data_handler(cls, fieldname):
139
-        if not fieldname in cls.__fields:
141
+        if not fieldname in cls._fields:
140 142
             raise NameError("No field named '%s' in %s" % (fieldname, cls.__name__))
141
-        return cls.__fields[fieldname]
142
-        
143
+        return cls._fields[fieldname]
144
+ 
145
+    ## @brief Return a LeObject child class from a name
146
+    # @warning This method has to be called from dynamically generated LeObjects
147
+    # @param leobject_name str : LeObject name
148
+    # @return A LeObject child class
149
+    # @throw NameError if invalid name given
150
+    @classmethod
151
+    def name2class(cls, leobject_name):
152
+        if cls.__module__ == 'lodel.leapi.leobject':
153
+            raise NotImplementedError("Abstract method")
154
+        mod = importlib.import_module(cls.__module__)
155
+        try:
156
+            return getattr(mod, leobject_name)
157
+        except AttributeError:
158
+            raise NameError("No LeObject named '%s'" % leobject_name)
159
+    
160
+    ## @brief Method designed to "bootstrap" fields by settings their back references
161
+    # 
162
+    # @note called once at dyncode load
163
+    # @note doesn't do any consistency checks, it assume that checks has been done EM side
164
+    # @warning after dyncode load this method is deleted
165
+    @classmethod
166
+    def _backref_init(cls):
167
+        for fname,field in cls._fields.items():
168
+            if field.is_reference() and field.back_reference is not None:
169
+                cls_name, field_name = field.back_reference
170
+                bref_leobject = cls.name2class(cls.name2objname(cls_name))
171
+                if field_name not in bref_leobject._fields:
172
+                    raise NameError("LeObject %s doesn't have a field named '%s'" % (cls_name, field_name))
173
+                field.set_backreference(bref_leobject._fields[field_name])
174
+
175
+    @classmethod
176
+    def is_abstract(cls):
177
+        return cls._abstract
143 178
 
144 179
     ## @brief Read only access to all datas
145 180
     # @note for fancy data accessor use @ref LeObject.g attribute @ref LeObjectValues instance
@@ -148,7 +183,7 @@ class LeObject(object):
148 183
     # @throw RuntimeError if the field is not initialized yet
149 184
     # @throw NameError if name is not an existing field name
150 185
     def data(self, field_name):
151
-        if field_name not in self.__fields.keys():
186
+        if field_name not in self._fields.keys():
152 187
             raise NameError("No such field in %s : %s" % (self.__class__.__name__, name))
153 188
         if not self.initialized and name not in self.__initialized:
154 189
             raise RuntimeError("The field %s is not initialized yet (and have no value)" % name)
@@ -163,7 +198,7 @@ class LeObject(object):
163 198
     # @throw AttributeError if the field is not writtable
164 199
     def set_data(self, fname, fval):
165 200
         if field_name not in self.fieldnames(include_ro = False):
166
-            if field_name not in self.__fields.keys():
201
+            if field_name not in self._fields.keys():
167 202
                 raise NameError("No such field in %s : %s" % (self.__class__.__name__, name))
168 203
             else:
169 204
                 raise AttributeError("The field %s is read only" % fname)
@@ -182,7 +217,7 @@ class LeObject(object):
182 217
         else:
183 218
             # Doing value check on modified field
184 219
             # We skip full validation here because the LeObject is not fully initialized yet
185
-            val, err = self.__fields[fname].check_data_value(fval)
220
+            val, err = self._fields[fname].check_data_value(fval)
186 221
             if isinstance(err, Exception):
187 222
                 #Revert change to be in valid state
188 223
                 del(self.__datas[fname])
@@ -196,7 +231,7 @@ class LeObject(object):
196 231
     # Check the list of initialized fields and set __initialized to True if all fields initialized
197 232
     def __set_initialized(self):
198 233
         if isinstance(self.__initialized, list):
199
-            expected_fields = self.fieldnames(include_ro = False) + self.__uid
234
+            expected_fields = self.fieldnames(include_ro = False) + self._uid
200 235
             if set(expected_fields) == set(self.__initialized):
201 236
                 self.__initialized = True
202 237
 
@@ -209,7 +244,7 @@ class LeObject(object):
209 244
         if self.__initialized is True:
210 245
             # Data value check
211 246
             for fname in self.fieldnames(include_ro = False):
212
-                val, err = self.__fields[fname].check_data_value(self.__datas[fname])
247
+                val, err = self._fields[fname].check_data_value(self.__datas[fname])
213 248
                 if err is not None:
214 249
                     err_list[fname] = err
215 250
                 else:
@@ -218,7 +253,7 @@ class LeObject(object):
218 253
             if len(err_list) == 0:
219 254
                 for fname in self.fieldnames(include_ro = True):
220 255
                     try:
221
-                        field = self.__fields[fname]
256
+                        field = self._fields[fname]
222 257
                         self.__datas[fname] = fields.construct_data(    self,
223 258
                                                                         fname,
224 259
                                                                         self.__datas,
@@ -229,14 +264,14 @@ class LeObject(object):
229 264
             # Datas consistency check
230 265
             if len(err_list) == 0:
231 266
                 for fname in self.fieldnames(include_ro = True):
232
-                    field = self.__fields[fname]
267
+                    field = self._fields[fname]
233 268
                     ret = field.check_data_consistency(self, fname, self.__datas)
234 269
                     if isinstance(ret, Exception):
235 270
                         err_list[fname] = ret
236 271
         else:
237 272
             # Data value check for initialized datas
238 273
             for fname in self.__initialized:
239
-                val, err = self.__fields[fname].check_data_value(self.__datas[fname])
274
+                val, err = self._fields[fname].check_data_value(self.__datas[fname])
240 275
                 if err is not None:
241 276
                     err_list[fname] = err
242 277
                 else:
@@ -249,13 +284,13 @@ class LeObject(object):
249 284
     
250 285
     ## @brief Temporary method to set private fields attribute at dynamic code generation
251 286
     #
252
-    # This method is used in the generated dynamic code to set the __fields attribute
287
+    # This method is used in the generated dynamic code to set the _fields attribute
253 288
     # at the end of the dyncode parse
254
-    # @warning This method is deleted once the dynamic code is parsed
289
+    # @warning This method is deleted once the dynamic code loaded
255 290
     # @param field_list list : list of EmField instance
256 291
     @classmethod
257 292
     def _set__fields(cls, field_list):
258
-        cls.__fields = field_list
293
+        cls._fields = field_list
259 294
         
260 295
 
261 296
 

読み込み中…
キャンセル
保存