Browse Source

LeFactory first implementation

This implementation is broken because of relation data_handler. Backreference is a broken concept and relation data_handler too.
Yann Weber 8 years ago
parent
commit
747205a0fe

+ 33
- 2
lodel/editorial_model/components.py View File

@@ -52,11 +52,15 @@ class EmClass(EmComponent):
52 52
     # @param uid str : uniq identifier
53 53
     # @param display_name MlString|str|dict : component display_name
54 54
     # @param abstract bool : set the class as asbtract if True
55
+    # @param pure_abstract bool : if True the EmClass will not be represented in leapi dyncode
55 56
     # @param parents list: parent EmClass list or uid list
56 57
     # @param help_text MlString|str|dict : help_text
57
-    def __init__(self, uid, display_name = None, help_text = None, abstract = False, parents = None, group = None):
58
+    def __init__(self, uid, display_name = None, help_text = None, abstract = False, parents = None, group = None, pure_abstract = False):
58 59
         super().__init__(uid, display_name, help_text, group)
59 60
         self.abstract = bool(abstract)
61
+        self.pure_abstract = bool(pure_abstract)
62
+        if self.pure_abstract:
63
+            self.abtract = True
60 64
         if parents is not None:
61 65
             if not isinstance(parents, list):
62 66
                 parents = [parents]
@@ -78,6 +82,19 @@ class EmClass(EmComponent):
78 82
         res.update(self.__fields)
79 83
         return res
80 84
 
85
+    ## @brief Return the list of all dependencies
86
+    #
87
+    # Reccursive parents listing
88
+    @property
89
+    def parents_recc(self):
90
+        if len(self.parents) == 0:
91
+            return set()
92
+
93
+        res = set(self.parents)
94
+        for parent in self.parents:
95
+            res |= parent.parents_recc
96
+        return res
97
+
81 98
     ## @brief EmField getter
82 99
     # @param uid None | str : If None returns an iterator on EmField instances else return an EmField instance
83 100
     # @param no_parents bool : If True returns only fields defined is this class and not the one defined in parents classes
@@ -96,7 +113,7 @@ class EmClass(EmComponent):
96 113
     # @todo End the override checks (needs methods in data_handlers)
97 114
     def add_field(self, emfield):
98 115
         if emfield.uid in self.__fields:
99
-            raise EditorialModelException("Duplicated uid '%s' for EmField in this class ( %s )" % (emfield.uid, self))
116
+            raise EditorialModelError("Duplicated uid '%s' for EmField in this class ( %s )" % (emfield.uid, self))
100 117
         # Incomplete field override check
101 118
         if emfield.uid in self.__all_fields:
102 119
             parent_field = self.__all_fields[emfield.uid]
@@ -124,6 +141,18 @@ class EmClass(EmComponent):
124 141
         m.update(bytes(payload, 'utf-8'))
125 142
         return int.from_bytes(m.digest(), byteorder='big')
126 143
 
144
+    def __str__(self):
145
+        return "<class EmClass %s>" % self.uid
146
+    
147
+    def __repr__(self):
148
+        if not self.abstract:
149
+            abstract = ''
150
+        elif self.pure_abstract:
151
+            abstract = 'PureAbstract'
152
+        else:
153
+            abstract = 'Abstract'
154
+        return "<class %s EmClass uid=%s>" % (abstract, repr(self.uid) )
155
+
127 156
 
128 157
 ## @brief Handles editorial model classes fields
129 158
 class EmField(EmComponent):
@@ -139,6 +168,8 @@ class EmField(EmComponent):
139 168
         super().__init__(uid, display_name, help_text, group)
140 169
         self.data_handler_name = data_handler
141 170
         self.data_handler_cls = FieldDataHandler.from_name(data_handler)
171
+        #if 'data_handler_kwargs' in handler_kwargs:
172
+        #    handler_kwargs = handler_kwargs['data_handler_kwargs']
142 173
         self.data_handler_options = handler_kwargs
143 174
         self.data_handler_instance = self.data_handler_cls(**handler_kwargs)
144 175
         ## @brief Stores the emclass that contains this field (set by EmClass.add_field() method)

+ 19
- 0
lodel/editorial_model/model.py View File

@@ -1,6 +1,7 @@
1 1
 #-*- coding:utf-8 -*-
2 2
 
3 3
 import hashlib
4
+import importlib
4 5
 
5 6
 from lodel.utils.mlstring import MlString
6 7
 
@@ -77,6 +78,8 @@ class EditorialModel(object):
77 78
     # @param translator module : The translator module to use
78 79
     # @param **translator_args
79 80
     def save(self, translator, **translator_kwargs):
81
+        if isinstance(translator, str):
82
+            translator = self.translator_from_name(translator)
80 83
         return translator.save(self, **translator_kwargs)
81 84
     
82 85
     ## @brief Load a model
@@ -84,7 +87,23 @@ class EditorialModel(object):
84 87
     # @param **translator_args
85 88
     @classmethod
86 89
     def load(cls, translator, **translator_kwargs):
90
+        if isinstance(translator, str):
91
+            translator = cls.translator_from_name(translator)
87 92
         return translator.load(**translator_kwargs)
93
+
94
+    ## @brief Return a translator module given a translator name
95
+    # @param translator_name str : The translator name
96
+    # @return the translator python module
97
+    # @throw NameError if the translator does not exists
98
+    @staticmethod
99
+    def translator_from_name(translator_name):
100
+        pkg_name = 'lodel.editorial_model.translator.%s' % translator_name
101
+        try:
102
+            mod = importlib.import_module(pkg_name)
103
+        except ImportError:
104
+            raise NameError("No translator named %s")
105
+        return mod
106
+        
88 107
     
89 108
     ## @brief Private getter for __groups or __classes
90 109
     # @see classes() groups()

+ 0
- 1
lodel/editorial_model/translator/pickle.py View File

@@ -1 +0,0 @@
1
-#-*- coding: utf-8 -*-

+ 22
- 2
lodel/leapi/datahandlers/field_data_handler.py View File

@@ -15,10 +15,11 @@ class FieldDataHandler(object):
15 15
     #                         designed globally and immutable
16 16
     # @param **args
17 17
     # @throw NotImplementedError if it is instanciated directly
18
-    def __init__(self, internal=False, immutable=False, **args):
18
+    def __init__(self, internal=False, immutable=False, primary_key = False, **args):
19 19
         if self.__class__ == FieldDataHandler:
20 20
             raise NotImplementedError("Abstract class")
21
-
21
+        
22
+        self.primary_key = primary_key
22 23
         self.internal = internal  # Check this value ?
23 24
         self.immutable = bool(immutable)
24 25
 
@@ -31,6 +32,9 @@ class FieldDataHandler(object):
31 32
     def name(cls):
32 33
         return cls.__module__.split('.')[-1]
33 34
 
35
+    def is_primary_key(self):
36
+        return self.primary_key
37
+
34 38
     ## @brief checks if a fieldtype is internal
35 39
     # @return bool
36 40
     def is_internal(self):
@@ -98,6 +102,22 @@ class FieldDataHandler(object):
98 102
         if mod is None:
99 103
             raise NameError("Unknown data_handler name : '%s'" % data_handler_name)
100 104
         return mod.DataHandler
105
+    
106
+    ## @brief Return the module name to import in order to use the datahandler
107
+    # @param data_handler_name str : Data handler name
108
+    # @return a str
109
+    @staticmethod
110
+    def module_name(data_handler_name):
111
+        data_handler_name = data_handler_name.lower()
112
+        for mname in FieldDataHandler.modules_name(data_handler_name):
113
+            try:
114
+                mod = importlib.import_module(mname)
115
+                module_name = mname
116
+            except ImportError:
117
+                pass
118
+        if mod is None:
119
+            raise NameError("Unknown data_handler name : '%s'" % data_handler_name)
120
+        return module_name
101 121
 
102 122
     ## @brief get a module name given a fieldtype name
103 123
     # @param fieldtype_name str : a field type name

+ 122
- 0
lodel/leapi/lefactory.py View File

@@ -0,0 +1,122 @@
1
+#-*- coding: utf-8 -*-
2
+
3
+import functools
4
+from lodel.editorial_model.components import *
5
+from lodel.leapi.leobject import LeObject
6
+from lodel.leapi.datahandlers.field_data_handler import FieldDataHandler
7
+
8
+## @brief Generate python module code from a given model
9
+# @param model lodel.editorial_model.model.EditorialModel
10
+def dyncode_from_em(model):
11
+    
12
+    cls_code, modules, bootstrap_instr = generate_classes(model)
13
+    imports = "from lodel.leapi.leobject import LeObject\n"
14
+    for module in modules:
15
+        imports += "import %s\n" % module
16
+
17
+    res_code = """#-*- coding: utf-8 -*-
18
+{imports}
19
+{classes}
20
+{bootstrap_instr}
21
+del(LeObject._set__fields)
22
+""".format(
23
+            imports = imports,
24
+            classes = cls_code,
25
+            bootstrap_instr = bootstrap_instr,
26
+    )
27
+    return res_code
28
+
29
+## @brief return A list of EmClass sorted by dependencies
30
+#
31
+# The first elts in the list depends on nothing, etc.
32
+# @return a list of EmClass instances
33
+def emclass_sorted_by_deps(emclass_list):
34
+    def emclass_deps_cmp(cls_a, cls_b):
35
+        if len(cls_a.parents) + len(cls_b.parents) == 0:
36
+            return 0
37
+        elif len(cls_a.parents) == 0:
38
+            return -1
39
+        elif len(cls_b.parents) == 0:
40
+            return 1
41
+
42
+        if cls_a in cls_b.parents_recc:
43
+            return -1
44
+        elif cls_b in cls_a.parents_recc:
45
+            return 1
46
+        else:
47
+            return 0
48
+    return sorted(emclass_list, key = functools.cmp_to_key(emclass_deps_cmp))
49
+
50
+## @brief Given an EmField returns the data_handler constructor suitable for dynamic code
51
+def data_handler_constructor(emfield):
52
+    dh_module_name = FieldDataHandler.module_name(emfield.data_handler_name)+'.DataHandler'
53
+    options = []
54
+
55
+    #dh_kwargs =  '{' + (', '.join(['%s: %s' % (repr(name), forge_optval(val)) for name, val in emfield.data_handler_options.items()])) + '}'
56
+    return ('%s(**{' % dh_module_name)+(', '.join([repr(name)+': '+forge_optval(val) for name, val in emfield.data_handler_options.items()])) + '})'
57
+            
58
+
59
+def forge_optval(optval):
60
+    if isinstance(optval, dict):
61
+        return '{' + (', '.join( [ '%s: %s' % (repr(name), forge_optval(val)) for name, val in optval.items()])) + '}'
62
+
63
+    if isinstance(optval, (set, list, tuple)):
64
+        return '[' + (', '.join([forge_optval(val) for val in optval])) + ']'
65
+        
66
+    if isinstance(optval, EmField):
67
+        return "{leobject}.data_handler({fieldname})".format(
68
+                leobject = LeObject.name2objname(optval._emclass.uid),
69
+                fieldname = repr(optval.uid)
70
+            )
71
+    elif isinstance(optval, EmClass):
72
+        return LeObject.name2objname(optval.uid)
73
+    else:
74
+        return repr(optval)
75
+
76
+## @brief Generate dyncode from an EmClass
77
+# @param model EditorialModel : 
78
+# @param emclass EmClass : EmClass instance
79
+# @return a tuple with emclass python code, a set containing modules name to import, and a list of python instruction to bootstrap dynamic code, in this order
80
+def generate_classes(model):
81
+    res = ""
82
+    imports = list()
83
+    bootstrap = ""
84
+    # Generating field list for LeObjects generated from EmClass
85
+    for em_class in [ cls for cls in emclass_sorted_by_deps(model.classes()) if not cls.pure_abstract ]:
86
+        uid = list()        # List of fieldnames that are part of the EmClass primary key
87
+        parents = list()    # List of parents EmClass
88
+        # Determine pk
89
+        for field in em_class.fields():
90
+            imports.append(FieldDataHandler.module_name(field.data_handler_name))
91
+            if field.data_handler_instance.is_primary_key():
92
+                uid.append(field.uid)
93
+        # Determine parent for inheritance
94
+        if len(em_class.parents) > 0:
95
+            for parent in em_class.parents:
96
+               parents.append(LeObject.name2objname(parent.uid))
97
+        else:
98
+            parents.append('LeObject')
99
+        
100
+        # Dynamic code generation for LeObject childs classes
101
+        em_cls_code = """
102
+class {clsname}({parents}):
103
+    __abstract = {abstract}
104
+    __fields = None
105
+    __uid = {uid_list}
106
+
107
+""".format(
108
+    clsname = LeObject.name2objname(em_class.uid),
109
+    parents = ', '.join(parents),
110
+    abstract = 'True' if em_class.abstract else 'False',
111
+    uid_list = repr(uid),
112
+)
113
+        res += em_cls_code
114
+        # Dyncode bootstrap instructions
115
+        bootstrap += """{classname}._set__fields({fields})
116
+#del({classname}._set__fields)
117
+""".format(
118
+    classname = LeObject.name2objname(em_class.uid),
119
+    fields = '{' + (', '.join(['\n\t%s: %s' % (repr(emfield.uid),data_handler_constructor(emfield)) for emfield in em_class.fields()])) + '}',
120
+)
121
+    return res, set(imports), bootstrap
122
+    

+ 44
- 9
lodel/leapi/leobject.py View File

@@ -24,11 +24,13 @@ class LeApiErrors(Exception):
24 24
 
25 25
 
26 26
 ## @brief When an error concern a query
27
-class LeApiQueryError(LeApiErrors): pass
27
+class LeApiQueryError(LeApiErrors):
28
+    pass
28 29
 
29 30
 
30 31
 ## @brief When an error concerns a datas
31
-class LeApiDataCheckError(LeApiErrors): pass
32
+class LeApiDataCheckError(LeApiErrors):
33
+    pass
32 34
 
33 35
 
34 36
 ## @brief Wrapper class for LeObject getter & setter
@@ -60,15 +62,21 @@ class LeObjectValues(object):
60 62
         
61 63
 
62 64
 class LeObject(object):
63
-    
65
+ 
66
+    ## @brief boolean that tells if an object is abtract or not
67
+    __abtract = None
64 68
     ## @brief A dict that stores DataHandler instances indexed by field name
65 69
     __fields = None
66 70
     ## @brief A tuple of fieldname (or a uniq fieldname) representing uid
67 71
     __uid = None 
68 72
 
73
+    ## @brief Construct an object representing an Editorial component
74
+    # @note Can be considered as EmClass instance
69 75
     def __init__(self, **kwargs):
76
+        if self.__abstract:
77
+            raise NotImplementedError("%s is abstract, you cannot instanciate it." % self.__class__.__name__ )
70 78
         ## @brief A dict that stores fieldvalues indexed by fieldname
71
-        self.__datas = dict(fname:None for fname in self.__fields)
79
+        self.__datas = { fname:None for fname in self.__fields }
72 80
         ## @brief Store a list of initianilized fields when instanciation not complete else store True
73 81
         self.__initialized = list()
74 82
         ## @brief Datas accessor. Instance of @ref LeObjectValues
@@ -81,7 +89,8 @@ class LeObject(object):
81 89
             self.__datas[uid_name] = kwargs[uid_name]
82 90
             del(kwargs[uid_name])
83 91
             self.__initialized.append(uid_name)
84
-
92
+        
93
+        # Processing given fields
85 94
         allowed_fieldnames = self.fieldnames(include_ro = False)
86 95
         err_list = list()
87 96
         for fieldname, fieldval in kwargs.items():
@@ -118,6 +127,20 @@ class LeObject(object):
118 127
         else:
119 128
             return list(self.__fields.keys())
120 129
  
130
+    @classmethod
131
+    def name2objname(cls, name):
132
+        return name.title()
133
+    
134
+    ## @brief Return the datahandler asssociated with a LeObject field
135
+    # @param fieldname str : The fieldname
136
+    # @return A data handler instance
137
+    @classmethod
138
+    def data_handler(cls, fieldname):
139
+        if not fieldname in cls.__fields:
140
+            raise NameError("No field named '%s' in %s" % (fieldname, cls.__name__))
141
+        return cls.__fields[fieldname]
142
+        
143
+
121 144
     ## @brief Read only access to all datas
122 145
     # @note for fancy data accessor use @ref LeObject.g attribute @ref LeObjectValues instance
123 146
     # @param name str : field name
@@ -126,7 +149,7 @@ class LeObject(object):
126 149
     # @throw NameError if name is not an existing field name
127 150
     def data(self, field_name):
128 151
         if field_name not in self.__fields.keys():
129
-            raise NameError("No such field in %s : %s" % (self.__class__.__name__, name)
152
+            raise NameError("No such field in %s : %s" % (self.__class__.__name__, name))
130 153
         if not self.initialized and name not in self.__initialized:
131 154
             raise RuntimeError("The field %s is not initialized yet (and have no value)" % name)
132 155
         return self.__datas[name]
@@ -220,8 +243,20 @@ class LeObject(object):
220 243
                     self.__datas[fname] = val
221 244
         return err_list if len(err_list) > 0 else None
222 245
 
223
-    #-------------------#
224
-    #   Crud methods    #
225
-    #-------------------#
246
+    #--------------------#
247
+    #   Other methods    #
248
+    #--------------------#
249
+    
250
+    ## @brief Temporary method to set private fields attribute at dynamic code generation
251
+    #
252
+    # This method is used in the generated dynamic code to set the __fields attribute
253
+    # at the end of the dyncode parse
254
+    # @warning This method is deleted once the dynamic code is parsed
255
+    # @param field_list list : list of EmField instance
256
+    @classmethod
257
+    def _set__fields(cls, field_list):
258
+        cls.__fields = field_list
259
+        
260
+
226 261
 
227 262
     

+ 1
- 0
test_em.py View File

@@ -244,3 +244,4 @@ text.new_field( 'linked_persons',
244 244
                 group = editorial_person_group,
245 245
 )
246 246
 
247
+em.save('picklefile', filename = '/tmp/em_test.pickle')

+ 37
- 0
tests/editorial_model/test_model.py View File

@@ -32,6 +32,25 @@ class EditorialModelTestCase(unittest.TestCase):
32 32
         c2f1.uid = 'c2testfield1'
33 33
         self.assertEqual(model.d_hash(), e_hash)
34 34
 
35
+    def test_translator_from_name(self):
36
+        """ Test the translator_from_name method """
37
+        import lodel.editorial_model.translator.picklefile as expected
38
+        translator = EditorialModel.translator_from_name('picklefile')
39
+        self.assertEqual(translator, expected)
40
+
41
+    def test_invalid_translator_from_name(self):
42
+        """ Test the translator_from_name method when invalid names given as argument """
43
+        import lodel.editorial_model.translator.picklefile
44
+        invalid_names = [
45
+            lodel.editorial_model.translator.picklefile,
46
+            'foobar',
47
+            42,
48
+        ]
49
+
50
+        for name in invalid_names:
51
+            with self.assertRaises(NameError):
52
+                EditorialModel.translator_from_name(name)
53
+
35 54
 
36 55
 class EmComponentTestCase(unittest.TestCase):
37 56
     
@@ -105,6 +124,24 @@ class EmClassTestCase(unittest.TestCase):
105 124
         with self.assertRaises(AttributeError):
106 125
             cls2.new_field('test', data_handler = 'varchar', max_length = 2)
107 126
 
127
+    def test_parents_recc(self):
128
+        """ Test the reccursive parents property """
129
+        model = EditorialModel(
130
+                                    "test_model",
131
+                                    description = "Model for LeFactoryTestCase"
132
+        )
133
+        cls1 = model.new_class('testclass1')
134
+        cls2 = model.new_class('testclass2')
135
+        cls3 = model.new_class('testclass3', parents = [cls2])
136
+        cls4 = model.new_class('testclass4', parents = [cls1, cls3])
137
+        cls5 = model.new_class('testclass5', parents = [cls4])
138
+        cls6 = model.new_class('testclass6')
139
+
140
+        self.assertEqual(cls5.parents_recc, set((cls4, cls1, cls2, cls3)))
141
+        self.assertEqual(cls1.parents_recc, set())
142
+        self.assertEqual(cls4.parents_recc, set((cls1, cls2, cls3)))
143
+        self.assertEqual(cls3.parents_recc, set((cls2,)))
144
+
108 145
 class EmGroupTestCase(unittest.TestCase):
109 146
     
110 147
     def test_init(self):

+ 0
- 0
tests/leapi/__init__.py View File


+ 52
- 0
tests/leapi/test_lefactory.py View File

@@ -0,0 +1,52 @@
1
+#-*- coding: utf-8 -*-
2
+
3
+import unittest
4
+from lodel.editorial_model.model import EditorialModel
5
+from lodel.leapi import lefactory
6
+
7
+class LeFactoryTestCase(unittest.TestCase):
8
+    
9
+    model = None
10
+
11
+    @classmethod
12
+    def setUpClass(cls):
13
+        """ Generate an example model for this TestCase """
14
+        model = EditorialModel(
15
+                                    "test_model",
16
+                                    description = "Model for LeFactoryTestCase"
17
+        )
18
+        cls1 = model.new_class('testclass1')
19
+        cls2 = model.new_class('testclass2')
20
+        cls3 = model.new_class('testclass3', parents = [cls2])
21
+        cls4 = model.new_class('testclass4', parents = [cls1, cls3])
22
+        cls5 = model.new_class('testclass5', parents = [cls4])
23
+        cls6 = model.new_class('testclass6')
24
+
25
+        cls1.new_field('testfield1', data_handler='varchar')
26
+        cls1.new_field('testfield2', data_handler='varchar', nullable = True)
27
+        cls1.new_field('testfield3', data_handler='varchar', max_length=64)
28
+        cls1.new_field('testfield4', data_handler='varchar', max_length=64, nullable=False, uniq=True)
29
+        cls1.new_field('id', data_handler='integer', primary_key = True)
30
+
31
+        cls5.new_field('id2', data_handler='varchar', primary_key = True)
32
+
33
+        cls.model = model
34
+    
35
+    def test_emclass_sorted_by_deps(self):
36
+        """ Test the function that sort EmClass by dependencies """
37
+        cls_list = self.model.classes()
38
+        sorted_cls = lefactory.emclass_sorted_by_deps(cls_list)
39
+        seen = set() # Stores the EmClass allready seen will walking through array
40
+        
41
+        for _ in range(10): #Bad sorts algorithm can have random behavior
42
+            for emclass in sorted_cls:
43
+                for parent in emclass.parents:
44
+                    self.assertIn(parent, seen)
45
+                seen |= set((emclass,))
46
+
47
+    def test_generated_code_syntax(self):
48
+        """ Test the function that generate LeObject childs classes code"""
49
+        pycode = lefactory.dyncode_from_em(self.model)
50
+        pycomp_code = compile(pycode, "dyn.py", 'exec')
51
+        exec(pycomp_code, globals())
52
+        

Loading…
Cancel
Save