Browse Source

Implements leapi dynamic classes enhancement with decorators classes

Yann Weber 8 years ago
parent
commit
a20f38cc78
5 changed files with 163 additions and 24 deletions
  1. 101
    3
      Lodel/plugins.py
  2. 40
    10
      leapi/lecrud.py
  3. 10
    10
      leapi/lefactory.py
  4. 11
    0
      plugins/dummy_leapi.py
  5. 1
    1
      runtest

+ 101
- 3
Lodel/plugins.py View File

@@ -5,9 +5,109 @@ import os, os.path
5 5
 import warnings
6 6
 
7 7
 from Lodel.settings import Settings
8
+from Lodel import logger
8 9
 from Lodel.hooks import LodelHook
9 10
 from Lodel.user import authentication_method, identification_method
10 11
 
12
+## @brief Handle registration and fetch of plugins functions that will be bound to LeCrud subclass
13
+#
14
+# @note This class exists because of strange behavior with decorator's class inheritance (static attributes was modified by child class)
15
+class LeapiPluginsMethods(object):
16
+
17
+    ## @brief Stores instance methods we want to bind to LeCrud subclass instances
18
+    __methods = {}
19
+    ## @brief Stores classmethods we want to bind to LeCrud subclass
20
+    __classmethods = {}
21
+    
22
+    ## @brief Register a new method for LeApi enhancement
23
+    # @param class_method bool : if True register a classmethod
24
+    # @param leapi_classname str : the classname
25
+    # @param method callable : the method to bind
26
+    # @param bind_name str|None : binding name, if None use the decorated function name
27
+    @classmethod
28
+    def register(cls, class_method, leapi_classname, method, bind_name = None):
29
+        if bind_name is None:
30
+            bind_name = method.__name__ # use method name if no bind_name specified
31
+
32
+        methods = cls.__classmethods if class_method else cls.__methods
33
+
34
+        if leapi_classname not in methods:
35
+            methods[leapi_classname] = dict()
36
+        elif bind_name in methods[leapi_classname]:
37
+            orig_fun = methods[leapi_classname][bind_name]
38
+            msg = 'Method overwriting : %s.%s will be registered as %s for %s in place of %s.%s' % (
39
+                inspect.getmodule(orig_fun).__name__,
40
+                orig_fun.__name__,
41
+                bind_name,
42
+                leapi_classname,
43
+                inspect.getmodule(method).__name__,
44
+                method.__name__
45
+            )
46
+            logger.warning(msg)
47
+        methods[leapi_classname][bind_name] = method
48
+        # I was thinking that dict are refrerences...
49
+        if class_method:
50
+            cls.__classmethods = methods
51
+        else:
52
+            cls.__methods = methods
53
+    
54
+    ## @brief Fetch all methods that has to be bound to a class
55
+    # @param class_method bool : if true returns classmethods
56
+    # @param call_cls LeCrud subclass : the targeted class
57
+    # @return a dict with wanted bind name as key and callable as value
58
+    @classmethod
59
+    def get_methods(cls, class_method, call_cls):
60
+        methods = cls.__classmethods if class_method else cls.__methods
61
+        result = dict()
62
+        for leapi_classname in methods:
63
+            leapi_cls = call_cls.name2class(leapi_classname)
64
+            # Strange tests are made on call_cls.__name__ because the name2class method
65
+            # is not working when called from LeObject at init time...
66
+            if leapi_cls is False and leapi_classname != call_cls.__name__:
67
+                logger.warning("Unable to find leapi class %s" % (leapi_classname))
68
+            elif leapi_classname == call_cls.__name__ or issubclass(call_cls, leapi_cls):
69
+                result.update(methods[leapi_classname])
70
+        return result
71
+
72
+
73
+## @brief Decorator class for leapi object enhancement
74
+#
75
+# This decorator allows plugins to bind methods to leapi
76
+# classes.
77
+#
78
+# The decorator take 2 arguments :
79
+# - leapi_cls_name str : the leapi class name
80
+# - method_name str (optional) : the method name once bind (if None the decorated function name will be used)
81
+#
82
+# @note there is another optionnal argument class_method (a bool), but you should not use it and use the leapi_classmethod decorator instead
83
+class leapi_method(object):
84
+    ## @brief Constructor for plugins leapi enhancement methods
85
+    # @param leapi_cls_name str : the classname we want to bind a method to
86
+    # @param method_name str|None : binding name, if None use the decorated function name
87
+    def __init__(self, leapi_cls_name, method_name = None, class_method = False):
88
+        self.__leapi_cls_name = leapi_cls_name
89
+        self.__method_name = method_name
90
+        self.__method = None
91
+        self.__class_method = bool(class_method)
92
+    
93
+    ## @brief Called at decoration time
94
+    # @param method callable : the decorated function
95
+    def __call__(self, method):
96
+        self.__method = method
97
+        if self.__method_name is None:
98
+            self.__method_name = method.__name__
99
+        self.__register()
100
+    
101
+    ## @biref Register a method to the method we want to bind
102
+    def __register(self):
103
+        LeapiPluginsMethods.register(self.__class_method, self.__leapi_cls_name, self.__method, self.__method_name)
104
+
105
+
106
+## @brief Same behavior thant leapi_method but registers classmethods
107
+class leapi_classmethod(leapi_method):
108
+    def __init__(self, leapi_cls_name, method_name = None):
109
+        super().__init__(leapi_cls_name, method_name, True)
110
+    
11 111
 
12 112
 ## @brief Returns a list of human readable registered hooks
13 113
 # @param names list | None : optionnal filter on name
@@ -40,6 +140,7 @@ def list_hooks(names = None, plugins = None):
40 140
                 res += "\t- %s.%s ( priority %d )\n" % (module, hook.__name__, priority)
41 141
     return res
42 142
 
143
+
43 144
 ## @brief Return a human readable list of plugins
44 145
 # @param activated bool | None : Optionnal filter on activated or not plugin
45 146
 # @return a str
@@ -82,6 +183,3 @@ def _all_plugins():
82 183
             # plugin is a simple python sourcefile
83 184
             res.append('%s' % plugin_name)
84 185
     return res
85
-            
86
-            
87
-    

+ 40
- 10
leapi/lecrud.py View File

@@ -8,8 +8,10 @@ import copy
8 8
 import warnings
9 9
 import importlib
10 10
 import re
11
+import types
11 12
 
12 13
 from Lodel import logger
14
+from Lodel.plugins import LeapiPluginsMethods
13 15
 from Lodel.settings import Settings
14 16
 from EditorialModel.fieldtypes.generic import DatasConstructor
15 17
 from Lodel.hooks import LodelHook
@@ -44,9 +46,15 @@ class LeApiQueryError(LeApiErrors): pass
44 46
 
45 47
 ## @brief When an error concerns a datas
46 48
 class LeApiDataCheckError(LeApiErrors): pass
47
-    
49
+
50
+## @brief Metaclass for _LeCrud
51
+#
52
+# Implements __getattribute__ for static class attr access ( for the 
53
+# _datasource lazy instanciation from settings).
48 54
 class _MetaLeCrud(type):
49
-     def __getattribute__(self, name):
55
+    
56
+    ## @brief Handles the lazy instancation of datasource (from settings)
57
+    def __getattribute__(self, name):
50 58
         if name == '_datasource':
51 59
             if super().__getattribute__('_datasource') is None:
52 60
                 module = importlib.import_module("DataSource.{pkg_name}.leapidatasource".format(
@@ -57,17 +65,29 @@ class _MetaLeCrud(type):
57 65
                 super().__setattr__('_datasource', ds_cls(**Settings.datasource_options))
58 66
         return super().__getattribute__(name)
59 67
 
60
-
61
-        if 'leapidatasource' in globals():
62
-            if name == '_datasource' and isinstance(self._datasource, DummyDatasource):
63
-                #if name == '_datasource' and self._datasource == None:
64
-                leapids = globals()['leapidatasource']
65
-                if not isinstance(leapids, DummyDatasource):
66
-                    self._datasource = leapids({})
67
-        return super().__getattribute__(name)
68
+## @brief Metaclass for dynamically generated leapi objects
69
+#
70
+# Implements the __init__ method in order to bin registered plugins method
71
+# ( see @ref Lodel.plugins.leapi_method )
72
+class MetaDynLeapi(_MetaLeCrud):
73
+    def __init__(self, *args):
74
+        self.__bind_plugins_method()
75
+        super().__init__(*args)
76
+    
77
+    ## @brief Handles methods binding
78
+    def __bind_plugins_method(self):
79
+        to_bind = LeapiPluginsMethods.get_methods(class_method = True, call_cls = self)
80
+        for name, method in to_bind.items():
81
+            if hasattr(self, name):
82
+                logger.debug("Classmethod '%s' bind overwrite existing attribute (probably parent class bound method)" % name)
83
+            logger.info("Binding classmethod %s to class %s as %s" % (method.__name__, self, name))
84
+            bounded = types.MethodType(method, self)
85
+            setattr(self, name, bounded)
86
+        
68 87
 
69 88
 ## @brief Main class to handler lodel editorial components (relations and objects)
70 89
 class _LeCrud(object, metaclass = _MetaLeCrud):
90
+
71 91
     ## @brief The datasource
72 92
     _datasource = None
73 93
 
@@ -87,6 +107,7 @@ class _LeCrud(object, metaclass = _MetaLeCrud):
87 107
     # @param uid int : lodel_id if LeObject, id_relation if its a LeRelation
88 108
     # @param **kwargs : datas !
89 109
     # @throw NotImplementedError if trying to instanciate a class that cannot be instanciated
110
+    # @todo see if instance methods bindings HAS TO be in the constructor
90 111
     def __init__(self, uid, **kwargs):
91 112
         if len(kwargs) > 0:
92 113
             if not self.implements_leobject() and not self.implements_lerelation():
@@ -106,6 +127,15 @@ class _LeCrud(object, metaclass = _MetaLeCrud):
106 127
             warnings.warn("When instanciating the uid was given in the uid argument but was also provided in kwargs. Droping the kwargs uid")
107 128
             del(kwargs[uid_name])
108 129
         
130
+        # Binding instance methods from plugins
131
+        to_bind = LeapiPluginsMethods.get_methods(class_method = False, call_cls = self.__class__)
132
+        for name, method in to_bind.items():
133
+            if hasattr(self, name):
134
+                logger.debug("Method '%s' bind overwrite existing attribute" % name)
135
+            logger.info("Binding method %s to an instance of %s as %s" %(method.__name__, self.__class__, name))
136
+            bounded = types.MethodType(method, self)
137
+            setattr(self, name, bounded)
138
+
109 139
         # Populating the object with given datas
110 140
         errors = dict()
111 141
         for name, value in kwargs.items():

+ 10
- 10
leapi/lefactory.py View File

@@ -87,7 +87,7 @@ class LeFactory(object):
87 87
                 attr_l[attr.name] = LeFactory.fieldtype_construct_from_field(attr)
88 88
 
89 89
             rel_code = """
90
-class {classname}(LeRel2Type):
90
+class {classname}(LeRel2Type, metaclass = leapi.lecrud.MetaDynLeapi):
91 91
     _rel_attr_fieldtypes = {attr_dict}
92 92
     _superior_cls = {supcls}
93 93
     _subordinate_cls = {subcls}
@@ -218,12 +218,12 @@ from leapi.letype import _LeType
218 218
         result += """
219 219
 ## @brief _LeCrud concret class
220 220
 # @see leapi.lecrud._LeCrud
221
-class LeCrud(leapi.lecrud._LeCrud):
221
+class LeCrud(leapi.lecrud._LeCrud, metaclass = leapi.lecrud.MetaDynLeapi):
222 222
     _uid_fieldtype = None
223 223
 
224 224
 ## @brief _LeObject concret class
225 225
 # @see leapi.leobject._LeObject
226
-class LeObject(LeCrud, leapi.leobject._LeObject):
226
+class LeObject(LeCrud, leapi.leobject._LeObject, metaclass = leapi.lecrud.MetaDynLeapi):
227 227
     _me_uid = {me_uid_l}
228 228
     _me_uid_field_names = ({class_id}, {type_id})
229 229
     _uid_fieldtype = {leo_uid_fieldtype}
@@ -231,7 +231,7 @@ class LeObject(LeCrud, leapi.leobject._LeObject):
231 231
 
232 232
 ## @brief _LeRelation concret class
233 233
 # @see leapi.lerelation._LeRelation
234
-class LeRelation(LeCrud, leapi.lerelation._LeRelation):
234
+class LeRelation(LeCrud, leapi.lerelation._LeRelation, metaclass = leapi.lecrud.MetaDynLeapi):
235 235
     _uid_fieldtype = {lerel_uid_fieldtype}
236 236
     _rel_fieldtypes = {lerel_fieldtypes}
237 237
     ## WARNING !!!! OBSOLETE ! DON'T USE IT
@@ -239,16 +239,16 @@ class LeRelation(LeCrud, leapi.lerelation._LeRelation):
239 239
     ## WARNING !!!! OBSOLETE ! DON'T USE IT
240 240
     _subordinate_field_name = {lesub_name}
241 241
 
242
-class LeHierarch(LeRelation, leapi.lerelation._LeHierarch):
242
+class LeHierarch(LeRelation, leapi.lerelation._LeHierarch, metaclass = leapi.lecrud.MetaDynLeapi):
243 243
     pass
244 244
 
245
-class LeRel2Type(LeRelation, leapi.lerelation._LeRel2Type):
245
+class LeRel2Type(LeRelation, leapi.lerelation._LeRel2Type, metaclass = leapi.lecrud.MetaDynLeapi):
246 246
     pass
247 247
 
248
-class LeClass(LeObject, _LeClass):
248
+class LeClass(LeObject, _LeClass, metaclass = leapi.lecrud.MetaDynLeapi):
249 249
     pass
250 250
 
251
-class LeType(LeClass, _LeType):
251
+class LeType(LeClass, _LeType, metaclass = leapi.lecrud.MetaDynLeapi):
252 252
     pass
253 253
 """.format(
254 254
             me_uid_l = repr(leobj_me_uid),
@@ -272,7 +272,7 @@ class LeType(LeClass, _LeType):
272 272
             result += """
273 273
 ## @brief EmClass {name} LeClass child class
274 274
 # @see leapi.leclass.LeClass
275
-class {name}(LeClass, LeObject):
275
+class {name}(LeClass, LeObject, metaclass = leapi.lecrud.MetaDynLeapi):
276 276
     _class_id = {uid}
277 277
     ml_string = MlString({name_translations})
278 278
 
@@ -288,7 +288,7 @@ class {name}(LeClass, LeObject):
288 288
             result += """
289 289
 ## @brief EmType {name} LeType child class
290 290
 # @see leobject::letype::LeType
291
-class {name}(LeType, {leclass}):
291
+class {name}(LeType, {leclass}, metaclass = leapi.lecrud.MetaDynLeapi):
292 292
     _type_id = {uid}
293 293
     ml_string = MlString({name_translations})
294 294
 

+ 11
- 0
plugins/dummy_leapi.py View File

@@ -0,0 +1,11 @@
1
+#-*- coding: utf-8 -*-
2
+
3
+from Lodel.plugins import leapi_method, leapi_classmethod
4
+
5
+@leapi_method('LeObject', 'test_method')
6
+def a_name(self, arg1):
7
+    print("Hello from %s. arg1 = %s" % (self, arg1))
8
+
9
+@leapi_classmethod('LeObject', 'test_classmethod')
10
+def another_name(cls):
11
+    print("I'm a super class method from class %s" % cls)

+ 1
- 1
runtest View File

@@ -7,6 +7,6 @@ test_package_name='test'
7 7
 
8 8
 for d in $(find . -type d -name "$test_package_name")
9 9
 do
10
-	echo -e "\tTesting\t$(dirname $d)\n"
10
+	echo -e "\n\tTesting\t$(dirname $d)"
11 11
 	python -W ignore -m unittest discover -qf -s "$d" || break
12 12
 done

Loading…
Cancel
Save