Merge lp:~openerp-dev/openobject-server/trunk-base-model-thu into lp:openobject-server

Proposed by Antony Lesuisse (OpenERP) on 2013-04-16
Status: Work in progress
Proposed branch: lp:~openerp-dev/openobject-server/trunk-base-model-thu
Merge into: lp:openobject-server
Diff against target: 1565 lines (+602/-362)
31 files modified
doc/changelog.rst (+5/-0)
openerp/addons/__init__.py (+4/-12)
openerp/addons/base/__init__.py (+1/-0)
openerp/addons/base/ir/ir_actions.py (+2/-1)
openerp/addons/base/ir/ir_attachment.py (+2/-1)
openerp/addons/base/ir/ir_filters.py (+2/-1)
openerp/addons/base/ir/ir_model.py (+9/-4)
openerp/addons/base/ir/ir_translation.py (+3/-1)
openerp/addons/base/ir/ir_ui_view.py (+6/-3)
openerp/addons/base/ir/ir_values.py (+2/-1)
openerp/addons/base/ir/workflow/workflow.py (+4/-2)
openerp/addons/base/models.py (+19/-0)
openerp/addons/base/module/module.py (+6/-6)
openerp/conf/deprecation.py (+7/-0)
openerp/modules/__init__.py (+3/-7)
openerp/modules/graph.py (+1/-1)
openerp/modules/loading.py (+30/-6)
openerp/modules/module.py (+182/-158)
openerp/modules/registry.py (+30/-17)
openerp/osv/orm.py (+82/-140)
openerp/service/__init__.py (+6/-1)
openerp/tests/addons/test_base_model_a/__init__.py (+3/-0)
openerp/tests/addons/test_base_model_a/__openerp__.py (+15/-0)
openerp/tests/addons/test_base_model_a/models.py (+26/-0)
openerp/tests/addons/test_base_model_a/tests/__init__.py (+13/-0)
openerp/tests/addons/test_base_model_a/tests/test_base_model.py (+40/-0)
openerp/tests/addons/test_base_model_b/__init__.py (+3/-0)
openerp/tests/addons/test_base_model_b/__openerp__.py (+15/-0)
openerp/tests/addons/test_base_model_b/models.py (+18/-0)
openerp/tests/addons/test_base_model_b/tests/__init__.py (+13/-0)
openerp/tests/addons/test_base_model_b/tests/test_base_model.py (+50/-0)
To merge this branch: bzr merge lp:~openerp-dev/openobject-server/trunk-base-model-thu
Reviewer Review Type Date Requested Status
Vo Minh Thu (community) Needs Fixing on 2013-07-11
Raphael Collet (OpenERP) (community) 2013-04-16 Needs Fixing on 2013-07-11
Review via email: mp+159245@code.launchpad.net

Description of the change

It seems this should fix https://bugs.launchpad.net/openobject-server/+bug/996816.

This patch does two important things (in addition to some cleaning):
- make "copied" (_inherit + new _name) models inherit from the consolidated parent model, instead of inherit of the parent as-is at installation time (i.e. when not all modules have possibly changed the copied parent).
- introduce a 'base.model' model that is, by default, always a copied parent of any model. (I.e. the default value of _inherit is 'base.model' instead of None.)

'base.model' might be renamed to simply 'base'.

Other changes:
- properly return the super()._auto_init() result (which is a list of stored fields that must be initialized after the table creation).
(The trunk addons are already fixed, so the code calling _auto_init()
can now rightfuly assume that the return type is indeed a list.)
- Removed some exports (or their uses) in openerp.addons and openerp.modules.
- Introduce a deprecation warning (and its controlling flag and comment in openerp.conf.deprecation) for manual instanciation of models. (When such manual instanciations are completely removed, models will be able to be instanciated with a simple model() call, instead of using object.__new__().__init__().)
- The previous point is possible because now __new__() is useless: everything it was doing is handled by the MetaModel meta class.
- Removed load_openerp_module(). Instead, using simply import or __import__ is enough.
- The previous point means that zip files are no longer supported (the code for those was probably broken anyway).
- The ImportHook was needed in the past to lookup OpenERP modules in the different addons paths. Now it is also used to recursively import module dependencies (as-if the imported module had some import statements for its dependencies), and perform some bookkeeping about modules and models (rank modules and models, store the content of the __openerp__.py descriptor file, ...).
- The _name of a model is set by its meta class, instead of sometimes being left to None.
- The support for the _sql model attribute is gone (it was not used, and its purpose is already served with _auto_init()/init() overriding).

Note:
Custom (a.k.a. manual) models seem to be broken in this branch but in trunk too; I hope/guess I haven't made the situation worse.

To post a comment you must log in.
4866. By Vo Minh Thu on 2013-04-17

[FIX] _auto_init(): when _auto_init() is overridden it does not return the super() result, which might brake functional fields initialization.

4867. By Vo Minh Thu on 2013-04-17

[REF] loading: init_module_models() moved to loading.py, slightly cleaner.

4868. By Vo Minh Thu on 2013-04-17

[REF] orm: renamed todo_end into something more sensible (matching the variable used in loading.py).

4869. By Vo Minh Thu on 2013-04-17

[REM] orm: removed unused _sql feature.

4870. By Vo Minh Thu on 2013-04-17

[REF] module: get_module_resource() is no longer in addons.

4871. By Vo Minh Thu on 2013-04-17

[REF] orm: removed create_instance (used by ir_module.py).

4872. By Vo Minh Thu on 2013-04-17

[FIX] registry: re-consolidate all previously built models whenever a new module is loaded in the registry.

This is needed so we can recompute the full class hierarchy of
models copying parents that are changed by the currently being
loaded module.

4873. By Vo Minh Thu on 2013-05-08

[MERGE] merged trunk.

4874. By Vo Minh Thu on 2013-05-08

[IMP] registry: avoid consolidating and instanciating the exact same model again and again.

4875. By Vo Minh Thu on 2013-05-08

[DOC] changelog: base.model and new _inherit/_name behavior.

4876. By Vo Minh Thu on 2013-07-08

[MERGE] merged trunk.

4877. By Vo Minh Thu on 2013-07-08

[ADD] base.model: added tests.

4878. By Vo Minh Thu on 2013-07-08

[ADD] base.model: added tests.

4879. By Vo Minh Thu on 2013-07-10

[IMP] base.model: disabled tests when run from within openerp.

It is still possible to run them via oe though.

Some tests are broken (see runbot).

I think you should split this branch in two:
 - the refactoring of the consolidation of models,
 - the other stuff, that is mostly orthogonal.

Please add documentation to your functions to explain what they do (stuff like "rank_modules() must have been called." does not help).

Your code is recursive-imperative: it traverses nodes and update them (openerp_rank, _parents, _rank, etc.) The problem is that it introduces *race conditions* whenever your server serves multiple databases! The ranking of modules is possibly independent from which modules are installed on a database, but the ranking of classes is not! The attributes _number, _parents, _children, _rank that you assign to addons classes depend on which modules are installed.

I suggest you to use memoized functions (which may be database-specific) instead of assigning the objects you are ranking. For instance, the sorting of modules by rank may be written as follows:

@memoize
def get_module_rank(name):
    """ return the rank of the module with the given name """
    module = get_module(name)
    depends = module.openerp_descriptor['depends']
    return max(map(get_module_rank, depends)) + 1 if depends else 0

# sorting module names by rank and name
modules = list(sorted(modules, key=lambda name: (get_module_rank(name), name)))

Raphael

review: Needs Fixing
Vo Minh Thu (thu) wrote :

Thanks for the review. I will check what you said properly, but quickly my first thoughts:

It seems you're right it is a bad idea to store registry-specific data in sys.modules :)

I'm not sure I could memoize them. I think I re-run them as each module is installed.

The other "orthogonal" stuff: I'm not they are that much orthogonal. I'm pretty sure some of them were possible because of the main change of this merge prop, and some other were actually necessary. I don't want to play with three branches (the orthogonal stuff that are not necessary for this branch, those that depends on this branch, and this branch) and re-base all the resulting commits.

review: Approve
Vo Minh Thu (thu) wrote :

About the runbot. Tests are not broken per se. They just can't be run all together at the same time, as some (more than one) of them assume a clean base model. That's boring, I don't know what I will do to the runbot to run them separately (and load the modules they belong to separately to). It seems I need a 'base' database, an 'all' one, and then a new one for the new tests.

Vo Minh Thu (thu) :
review: Needs Fixing

Unmerged revisions

4879. By Vo Minh Thu on 2013-07-10

[IMP] base.model: disabled tests when run from within openerp.

It is still possible to run them via oe though.

4878. By Vo Minh Thu on 2013-07-08

[ADD] base.model: added tests.

4877. By Vo Minh Thu on 2013-07-08

[ADD] base.model: added tests.

4876. By Vo Minh Thu on 2013-07-08

[MERGE] merged trunk.

4875. By Vo Minh Thu on 2013-05-08

[DOC] changelog: base.model and new _inherit/_name behavior.

4874. By Vo Minh Thu on 2013-05-08

[IMP] registry: avoid consolidating and instanciating the exact same model again and again.

4873. By Vo Minh Thu on 2013-05-08

[MERGE] merged trunk.

4872. By Vo Minh Thu on 2013-04-17

[FIX] registry: re-consolidate all previously built models whenever a new module is loaded in the registry.

This is needed so we can recompute the full class hierarchy of
models copying parents that are changed by the currently being
loaded module.

4871. By Vo Minh Thu on 2013-04-17

[REF] orm: removed create_instance (used by ir_module.py).

4870. By Vo Minh Thu on 2013-04-17

[REF] module: get_module_resource() is no longer in addons.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'doc/changelog.rst'
2--- doc/changelog.rst 2013-06-21 07:44:28 +0000
3+++ doc/changelog.rst 2013-07-10 14:52:28 +0000
4@@ -6,6 +6,11 @@
5 `trunk`
6 -------
7
8+- Introduced a ``base.model``. It is to OpenERP models what ``object`` is
9+ to Python classes. To make this possible, it means that now copied models
10+ (ie. models using ``_inherit`` and providing a new ``_name``) will copy
11+ the fully consolidated parent, instead of the parent as-is as the time
12+ of module installation.
13 - Almost removed ``LocalService()``. For reports,
14 ``openerp.osv.orm.Model.print_report()`` can be used. For workflows, see
15 :ref:`orm-workflows`.
16
17=== modified file 'openerp/addons/__init__.py'
18--- openerp/addons/__init__.py 2012-01-09 12:41:20 +0000
19+++ openerp/addons/__init__.py 2013-07-10 14:52:28 +0000
20@@ -23,18 +23,10 @@
21 """ Addons module.
22
23 This module serves to contain all OpenERP addons, across all configured addons
24-paths. For the code to manage those addons, see openerp.modules.
25-
26-Addons are made available under `openerp.addons` after
27-openerp.tools.config.parse_config() is called (so that the addons paths are
28-known).
29-
30-This module also conveniently reexports some symbols from openerp.modules.
31-Importing them from here is deprecated.
32-
33+paths. For the code to manage those addons, see `openerp.modules`.
34+
35+Addons can be imported from `openerp.addons` after
36+`openerp.modules.module.initialize_sys_path()` has been called.
37 """
38
39-# get_module_path is used only by base_module_quality
40-from openerp.modules import get_module_resource, get_module_path
41-
42 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
43
44=== modified file 'openerp/addons/base/__init__.py'
45--- openerp/addons/base/__init__.py 2012-08-22 06:28:23 +0000
46+++ openerp/addons/base/__init__.py 2013-07-10 14:52:28 +0000
47@@ -19,6 +19,7 @@
48 #
49 ##############################################################################
50
51+import models
52 import ir
53 import module
54 import res
55
56=== modified file 'openerp/addons/base/ir/ir_actions.py'
57--- openerp/addons/base/ir/ir_actions.py 2013-06-13 17:39:00 +0000
58+++ openerp/addons/base/ir/ir_actions.py 2013-07-10 14:52:28 +0000
59@@ -354,10 +354,11 @@
60 'multi': False,
61 }
62 def _auto_init(self, cr, context=None):
63- super(act_window_view, self)._auto_init(cr, context)
64+ res = super(act_window_view, self)._auto_init(cr, context)
65 cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'act_window_view_unique_mode_per_action\'')
66 if not cr.fetchone():
67 cr.execute('CREATE UNIQUE INDEX act_window_view_unique_mode_per_action ON ir_act_window_view (act_window_id, view_mode)')
68+ return res
69 act_window_view()
70
71 class act_wizard(osv.osv):
72
73=== modified file 'openerp/addons/base/ir/ir_attachment.py'
74--- openerp/addons/base/ir/ir_attachment.py 2013-06-12 15:28:26 +0000
75+++ openerp/addons/base/ir/ir_attachment.py 2013-07-10 14:52:28 +0000
76@@ -175,11 +175,12 @@
77 }
78
79 def _auto_init(self, cr, context=None):
80- super(ir_attachment, self)._auto_init(cr, context)
81+ res = super(ir_attachment, self)._auto_init(cr, context)
82 cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = %s', ('ir_attachment_res_idx',))
83 if not cr.fetchone():
84 cr.execute('CREATE INDEX ir_attachment_res_idx ON ir_attachment (res_model, res_id)')
85 cr.commit()
86+ return res
87
88 def check(self, cr, uid, ids, mode, context=None, values=None):
89 """Restricts the access to an ir.attachment, according to referred model
90
91=== modified file 'openerp/addons/base/ir/ir_filters.py'
92--- openerp/addons/base/ir/ir_filters.py 2013-02-20 10:34:59 +0000
93+++ openerp/addons/base/ir/ir_filters.py 2013-07-10 14:52:28 +0000
94@@ -118,12 +118,13 @@
95 ]
96
97 def _auto_init(self, cr, context=None):
98- super(ir_filters, self)._auto_init(cr, context)
99+ res = super(ir_filters, self)._auto_init(cr, context)
100 # Use unique index to implement unique constraint on the lowercase name (not possible using a constraint)
101 cr.execute("SELECT indexname FROM pg_indexes WHERE indexname = 'ir_filters_name_model_uid_unique_index'")
102 if not cr.fetchone():
103 cr.execute("""CREATE UNIQUE INDEX "ir_filters_name_model_uid_unique_index" ON ir_filters
104 (lower(name), model_id, COALESCE(user_id,-1))""")
105+ return res
106
107 _columns = {
108 'name': fields.char('Filter Name', size=64, translate=True, required=True),
109
110=== modified file 'openerp/addons/base/ir/ir_model.py'
111--- openerp/addons/base/ir/ir_model.py 2013-06-19 09:13:32 +0000
112+++ openerp/addons/base/ir/ir_model.py 2013-07-10 14:52:28 +0000
113@@ -203,9 +203,13 @@
114 def instanciate(self, cr, user, model, context=None):
115 class x_custom_model(osv.osv):
116 _custom = True
117- x_custom_model._name = model
118- x_custom_model._module = False
119- a = x_custom_model.create_instance(self.pool, cr)
120+ _name = model
121+ _module = False
122+ classes = list(openerp.modules.module.get_model_classes('base.model'))
123+ classes = [x_custom_model] + classes
124+ cls = openerp.osv.orm.consolidate(model, classes)
125+ a = object.__new__(cls)
126+ a.__init__(self.pool, cr)
127 if not a._columns:
128 x_name = 'id'
129 elif 'x_name' in a._columns.keys():
130@@ -842,10 +846,11 @@
131 self.loads = self.pool.model_data_reference_ids
132
133 def _auto_init(self, cr, context=None):
134- super(ir_model_data, self)._auto_init(cr, context)
135+ res = super(ir_model_data, self)._auto_init(cr, context)
136 cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'ir_model_data_module_name_index\'')
137 if not cr.fetchone():
138 cr.execute('CREATE INDEX ir_model_data_module_name_index ON ir_model_data (module, name)')
139+ return res
140
141 @tools.ormcache()
142 def _get_id(self, cr, uid, module, xml_id):
143
144=== modified file 'openerp/addons/base/ir/ir_translation.py'
145--- openerp/addons/base/ir/ir_translation.py 2013-04-18 11:46:27 +0000
146+++ openerp/addons/base/ir/ir_translation.py 2013-07-10 14:52:28 +0000
147@@ -223,7 +223,7 @@
148 'Language code of translation item must be among known languages' ), ]
149
150 def _auto_init(self, cr, context=None):
151- super(ir_translation, self)._auto_init(cr, context)
152+ res = super(ir_translation, self)._auto_init(cr, context)
153
154 # FIXME: there is a size limit on btree indexed values so we can't index src column with normal btree.
155 cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = %s', ('ir_translation_ltns',))
156@@ -247,6 +247,8 @@
157 cr.execute('CREATE INDEX ir_translation_ltn ON ir_translation (name, lang, type)')
158 cr.commit()
159
160+ return res
161+
162 def _check_selection_field_value(self, cr, uid, field, value, context=None):
163 if field == 'lang':
164 return
165
166=== modified file 'openerp/addons/base/ir/ir_ui_view.py'
167--- openerp/addons/base/ir/ir_ui_view.py 2013-06-05 12:11:43 +0000
168+++ openerp/addons/base/ir/ir_ui_view.py 2013-07-10 14:52:28 +0000
169@@ -41,10 +41,11 @@
170 }
171
172 def _auto_init(self, cr, context=None):
173- super(view_custom, self)._auto_init(cr, context)
174+ res = super(view_custom, self)._auto_init(cr, context)
175 cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'ir_ui_view_custom_user_id_ref_id\'')
176 if not cr.fetchone():
177 cr.execute('CREATE INDEX ir_ui_view_custom_user_id_ref_id ON ir_ui_view_custom (user_id, ref_id)')
178+ return res
179
180 class view(osv.osv):
181 _name = 'ir.ui.view'
182@@ -165,10 +166,11 @@
183 ]
184
185 def _auto_init(self, cr, context=None):
186- super(view, self)._auto_init(cr, context)
187+ res = super(view, self)._auto_init(cr, context)
188 cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'ir_ui_view_model_type_inherit_id\'')
189 if not cr.fetchone():
190 cr.execute('CREATE INDEX ir_ui_view_model_type_inherit_id ON ir_ui_view (model, inherit_id)')
191+ return res
192
193 def get_inheriting_views_arch(self, cr, uid, view_id, model, context=None):
194 """Retrieves the architecture of views that inherit from the given view, from the sets of
195@@ -293,10 +295,11 @@
196 }
197
198 def _auto_init(self, cr, context=None):
199- super(view_sc, self)._auto_init(cr, context)
200+ res = super(view_sc, self)._auto_init(cr, context)
201 cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'ir_ui_view_sc_user_id_resource\'')
202 if not cr.fetchone():
203 cr.execute('CREATE INDEX ir_ui_view_sc_user_id_resource ON ir_ui_view_sc (user_id, resource)')
204+ return res
205
206 def get_sc(self, cr, uid, user_id, model='ir.ui.menu', context=None):
207 ids = self.search(cr, uid, [('user_id','=',user_id),('resource','=',model)], context=context)
208
209=== modified file 'openerp/addons/base/ir/ir_values.py'
210--- openerp/addons/base/ir/ir_values.py 2013-06-13 17:39:00 +0000
211+++ openerp/addons/base/ir/ir_values.py 2013-07-10 14:52:28 +0000
212@@ -183,10 +183,11 @@
213 }
214
215 def _auto_init(self, cr, context=None):
216- super(ir_values, self)._auto_init(cr, context)
217+ res = super(ir_values, self)._auto_init(cr, context)
218 cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'ir_values_key_model_key2_res_id_user_id_idx\'')
219 if not cr.fetchone():
220 cr.execute('CREATE INDEX ir_values_key_model_key2_res_id_user_id_idx ON ir_values (key, model, key2, res_id, user_id)')
221+ return res
222
223 def set_default(self, cr, uid, model, field_name, value, for_all_users=True, company_id=False, condition=False):
224 """Defines a default value for the given model and field_name. Any previous
225
226=== modified file 'openerp/addons/base/ir/workflow/workflow.py'
227--- openerp/addons/base/ir/workflow/workflow.py 2013-06-13 17:39:00 +0000
228+++ openerp/addons/base/ir/workflow/workflow.py 2013-07-10 14:52:28 +0000
229@@ -137,13 +137,14 @@
230 'state': fields.char('Status', size=32),
231 }
232 def _auto_init(self, cr, context=None):
233- super(wkf_instance, self)._auto_init(cr, context)
234+ res = super(wkf_instance, self)._auto_init(cr, context)
235 cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'wkf_instance_res_type_res_id_state_index\'')
236 if not cr.fetchone():
237 cr.execute('CREATE INDEX wkf_instance_res_type_res_id_state_index ON wkf_instance (res_type, res_id, state)')
238 cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'wkf_instance_res_id_wkf_id_index\'')
239 if not cr.fetchone():
240 cr.execute('CREATE INDEX wkf_instance_res_id_wkf_id_index ON wkf_instance (res_id, wkf_id)')
241+ return res
242
243 wkf_instance()
244
245@@ -172,10 +173,11 @@
246 'workitem_id': fields.many2one('workflow.workitem', 'Workitem', required=True, ondelete="cascade"),
247 }
248 def _auto_init(self, cr, context=None):
249- super(wkf_triggers, self)._auto_init(cr, context)
250+ res = super(wkf_triggers, self)._auto_init(cr, context)
251 cr.execute('SELECT indexname FROM pg_indexes WHERE indexname = \'wkf_triggers_res_id_model_index\'')
252 if not cr.fetchone():
253 cr.execute('CREATE INDEX wkf_triggers_res_id_model_index ON wkf_triggers (res_id, model)')
254+ return res
255 wkf_triggers()
256
257
258
259=== added file 'openerp/addons/base/models.py'
260--- openerp/addons/base/models.py 1970-01-01 00:00:00 +0000
261+++ openerp/addons/base/models.py 2013-07-10 14:52:28 +0000
262@@ -0,0 +1,19 @@
263+from openerp.osv.orm import BaseModel
264+
265+# Lovely: `base.model` inherits from `BaseModel`.
266+#
267+# `BaseModel` and `Model` are exactly the same, but with diamond inheritance of
268+# `base.model`, if `Model` was used instead, `Model` would be higher in the MRO
269+# than `AbstractModel`, e.g. with models inheriting from `mail.message` or
270+# `mail.thread`. By using `BaseModel`, we keep `Model` below `AbstractModel`
271+# and its `_auto=True` attribute "wins" over the `_auto=False` of
272+# `AbstractModel`.
273+class base_model(BaseModel):
274+ _inherit = None
275+ _name = 'base.model'
276+
277+ _auto = False # No table in database.
278+ _transient = False
279+
280+ _columns = {
281+ }
282
283=== modified file 'openerp/addons/base/module/module.py'
284--- openerp/addons/base/module/module.py 2013-06-28 10:04:07 +0000
285+++ openerp/addons/base/module/module.py 2013-07-10 14:52:28 +0000
286@@ -40,7 +40,7 @@
287 from StringIO import StringIO # NOQA
288
289 import openerp
290-from openerp import modules, tools, addons
291+from openerp import modules, tools
292 from openerp.modules.db import create_categories
293 from openerp.tools.parse_version import parse_version
294 from openerp.tools.translate import _
295@@ -153,7 +153,7 @@
296 def _get_desc(self, cr, uid, ids, field_name=None, arg=None, context=None):
297 res = dict.fromkeys(ids, '')
298 for module in self.browse(cr, uid, ids, context=context):
299- path = addons.get_module_resource(module.name, 'static/description/index.html')
300+ path = openerp.modules.module.get_module_resource(module.name, 'static/description/index.html')
301 if path:
302 with tools.file_open(path, 'rb') as desc_file:
303 doc = desc_file.read()
304@@ -169,7 +169,7 @@
305 return res
306
307 def _get_latest_version(self, cr, uid, ids, field_name=None, arg=None, context=None):
308- default_version = modules.adapt_version('1.0')
309+ default_version = modules.module.adapt_version('1.0')
310 res = dict.fromkeys(ids, default_version)
311 for m in self.browse(cr, uid, ids):
312 res[m.id] = self.get_module_info(m.name).get('version', default_version)
313@@ -243,7 +243,7 @@
314 def _get_icon_image(self, cr, uid, ids, field_name=None, arg=None, context=None):
315 res = dict.fromkeys(ids, '')
316 for module in self.browse(cr, uid, ids, context=context):
317- path = addons.get_module_resource(module.name, 'static', 'description', 'icon.png')
318+ path = openerp.modules.module.get_module_resource(module.name, 'static', 'description', 'icon.png')
319 if path:
320 image_file = tools.file_open(path, 'rb')
321 try:
322@@ -584,7 +584,7 @@
323 def update_list(self, cr, uid, context=None):
324 res = [0, 0] # [update, add]
325
326- default_version = modules.adapt_version('1.0')
327+ default_version = modules.module.adapt_version('1.0')
328 known_mods = self.browse(cr, uid, self.search(cr, uid, []))
329 known_mods_names = dict([(m.name, m) for m in known_mods])
330
331@@ -631,7 +631,7 @@
332
333 def download(self, cr, uid, ids, download=True, context=None):
334 res = []
335- default_version = modules.adapt_version('1.0')
336+ default_version = modules.module.adapt_version('1.0')
337 for mod in self.browse(cr, uid, ids, context=context):
338 if not mod.url:
339 continue
340
341=== modified file 'openerp/conf/deprecation.py'
342--- openerp/conf/deprecation.py 2013-03-27 16:40:45 +0000
343+++ openerp/conf/deprecation.py 2013-07-10 14:52:28 +0000
344@@ -58,4 +58,11 @@
345 # but no warning was dispayed in the logs).
346 openerp_pooler = True
347
348+# If True, calling a model class after defining it is allowed.
349+# class my_model(Model):
350+# pass
351+# my_model()
352+# Introduced around 2013.04.
353+allow_explicit_model_call = True
354+
355 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
356
357=== modified file 'openerp/modules/__init__.py'
358--- openerp/modules/__init__.py 2012-08-24 14:23:23 +0000
359+++ openerp/modules/__init__.py 2013-07-10 14:52:28 +0000
360@@ -28,14 +28,10 @@
361
362 # TODO temporarily expose those things
363 from openerp.modules.module import \
364- get_modules, get_modules_with_version, \
365+ get_modules, \
366 load_information_from_description_file, \
367- get_module_resource, zip_directory, \
368- get_module_path, initialize_sys_path, \
369- load_openerp_module, init_module_models, \
370- adapt_version
371-
372-from openerp.modules.loading import load_modules
373+ get_module_resource, \
374+ get_module_path
375
376
377 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
378
379=== modified file 'openerp/modules/graph.py'
380--- openerp/modules/graph.py 2013-03-27 17:06:39 +0000
381+++ openerp/modules/graph.py 2013-07-10 14:52:28 +0000
382@@ -98,7 +98,7 @@
383 # done by db.initialize, so it is possible to not do it again here.
384 info = openerp.modules.module.load_information_from_description_file(module)
385 if info and info['installable']:
386- packages.append((module, info)) # TODO directly a dict, like in get_modules_with_version
387+ packages.append((module, info)) # TODO directly a dict
388 else:
389 _logger.warning('module %s: not installable, skipped', module)
390
391
392=== modified file 'openerp/modules/loading.py'
393--- openerp/modules/loading.py 2013-05-07 11:20:24 +0000
394+++ openerp/modules/loading.py 2013-07-10 14:52:28 +0000
395@@ -40,8 +40,7 @@
396 from openerp import SUPERUSER_ID
397
398 from openerp.tools.translate import _
399-from openerp.modules.module import initialize_sys_path, \
400- load_openerp_module, init_module_models, adapt_version
401+from openerp.modules.module import adapt_version
402
403 _logger = logging.getLogger(__name__)
404 _test_logger = logging.getLogger('openerp.tests')
405@@ -149,9 +148,14 @@
406
407 _logger.debug('module %s: loading objects', package.name)
408 migrations.migrate_module(package, 'pre')
409- load_openerp_module(package.name)
410-
411- models = registry.load(cr, package)
412+ __import__('openerp.addons.' + package.name)
413+ mod = sys.modules['openerp.addons.' + package.name]
414+ post_load = mod.openerp_descriptor['post_load']
415+ if post_load:
416+ getattr(mod, post_load)()
417+
418+
419+ models = registry.add_module(cr, mod)
420
421 loaded_modules.append(package.name)
422 if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'):
423@@ -223,6 +227,27 @@
424
425 return loaded_modules, processed_modules
426
427+def init_module_models(cr, module_name, models):
428+ """ Initialize a list of models.
429+
430+ Call _auto_init and init on each model to create or update the
431+ database tables supporting the models. Call _auto_end to create
432+ foreign keys. Call _update_stored_functional_field to initialize
433+ stored functional fields.
434+ """
435+ _logger.info('module %s: creating or updating database tables', module_name)
436+ stored_fields = []
437+ for model in models:
438+ stored_fields += model._auto_init(cr, {'module': module_name})
439+ model.init(cr)
440+ cr.commit()
441+ for model in models:
442+ model._auto_end(cr, {'module': module_name})
443+ cr.commit()
444+ for order, model, field_name in sorted(stored_fields):
445+ model._update_stored_functional_field(cr, field_name)
446+ cr.commit()
447+
448 def _check_module_names(cr, module_names):
449 mod_names = set(module_names)
450 if 'base' in mod_names:
451@@ -257,7 +282,6 @@
452 # TODO status['progress'] reporting is broken: used twice (and reset each
453 # time to zero) in load_module_graph, not fine-grained enough.
454 # It should be a method exposed by the registry.
455- initialize_sys_path()
456
457 force = []
458 if force_demo:
459
460=== modified file 'openerp/modules/module.py'
461--- openerp/modules/module.py 2013-06-28 15:07:55 +0000
462+++ openerp/modules/module.py 2013-07-10 14:52:28 +0000
463@@ -26,7 +26,6 @@
464 from os.path import join as opj
465 import sys
466 import types
467-import zipimport
468
469 import openerp.tools as tools
470 import openerp.tools.osutil as osutil
471@@ -36,9 +35,6 @@
472 import openerp.release as release
473
474 import re
475-import base64
476-from zipfile import PyZipFile, ZIP_DEFLATED
477-from cStringIO import StringIO
478
479 import logging
480
481@@ -48,21 +44,177 @@
482 _ad = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'addons') # default addons path (base)
483 ad_paths = []
484
485-# Modules already loaded
486-loaded = []
487-
488-_logger = logging.getLogger(__name__)
489+
490+class Module(object):
491+ modules = []
492+ all_classes = []
493+ dirty = False
494+
495+def derive_module_info():
496+ rank_modules()
497+ number_classes()
498+ find_all_parents()
499+ find_all_new_parents()
500+ rank_classes()
501+
502+ Module.dirty = False
503+
504+def get_module(module_name):
505+ return sys.modules['openerp.addons.' + module_name]
506+
507+def rank_modules():
508+ for module in map(get_module, Module.modules):
509+ module.rank = None
510+ for module in map(get_module, Module.modules):
511+ rank_module(module)
512+ Module.modules = list(sorted(Module.modules, cmp=lambda x, y: cmp((get_module(x).openerp_rank, x), (get_module(y).openerp_rank, y))))
513+
514+def number_classes():
515+ """rank_modules() must have been called."""
516+ i = 0
517+ Module.all_classes = []
518+ for m in map(get_module, Module.modules):
519+ for cls in m.openerp_classes:
520+ Module.all_classes.append(cls)
521+ cls._number = i
522+ i += 1
523+
524+def rank_module(module):
525+ if module.openerp_rank is None:
526+ depends = module.openerp_descriptor['depends']
527+ if depends:
528+ module.openerp_rank = max(map(lambda d: rank_module(sys.modules['openerp.addons.' + d]), depends)) + 1
529+ else:
530+ module.openerp_rank = 0
531+
532+ return module.openerp_rank
533+
534+def rank_classes():
535+ """find_all_new_parents() must have been called."""
536+ for cls in Module.all_classes:
537+ cls._rank = None
538+ for cls in Module.all_classes:
539+ rank_class(cls)
540+
541+ Module.ranked_classes = sorted(Module.all_classes, cmp=lambda x, y: cmp((x._rank, x._name), (y._rank, y._name)))
542+
543+def rank_class(cls):
544+ if cls._rank is None:
545+ if cls._parents:
546+ cls._rank = max(map(lambda c: rank_class(c), cls._parents)) + 1
547+ parent_modules = [p._original_module for p in cls._parents]
548+ cls._original_module = [k for k in Module.modules if k in parent_modules][0]
549+ else:
550+ cls._rank = 0
551+ cls._original_module = cls._module
552+
553+ return cls._rank
554+
555+def find_all_parents():
556+ """number_classes() must have been called."""
557+ for cls in Module.all_classes:
558+ find_parents(cls)
559+
560+def find_parents(cls):
561+ if isinstance(cls._inherit, list):
562+ parents = cls._inherit
563+ elif cls._inherit:
564+ parents = [cls._inherit]
565+ else:
566+ parents = []
567+ cls._parents = map(find_parent(cls), parents)
568+ return cls._parents
569+
570+def find_parent(cls):
571+ def f(name):
572+ parent = None
573+ for c in reversed(Module.all_classes[:cls._number]):
574+ if c._name == name:
575+ if not hasattr(c, '_children'):
576+ c._children = []
577+ c._children.append(cls)
578+ parent = c
579+ break
580+ assert parent, "Cannot find class for model `%s`, specified as a parent of `%s`." % (name, cls._name)
581+ return c
582+ return f
583+
584+def find_all_new_parents():
585+ """find_all_parents() must have been called."""
586+ for cls in Module.all_classes:
587+ cls._parents = map(find_new_parent(cls), cls._parents)
588+
589+def find_new_parent(cls):
590+ def f(parent):
591+ assert cls._name
592+ assert parent._name
593+ if cls._name != parent._name:
594+ # cls is a copy of parent
595+ for c in reversed(Module.all_classes):
596+ if c._name == parent._name:
597+ return c
598+ return parent
599+ return f
600+
601+def get_model_classes(name):
602+ if Module.dirty:
603+ derive_module_info()
604+
605+ last = None
606+ for cls in reversed(Module.ranked_classes):
607+ if cls._name == name:
608+ last = cls
609+ break
610+ assert last, "No class found defining model `%s`.\n%s" % (name, map(lambda x: x._name, Module.ranked_classes))
611+
612+ return get_parents(cls, [])
613+
614+def get_parents(cls, accum):
615+ if cls in accum:
616+ accum.remove(cls) # Not accurate, we need to replicate Python MRO.
617+ accum.append(cls)
618+ for p in cls._parents:
619+ get_parents(p, accum)
620+ return accum
621+
622+def display_dot():
623+ print 'digraph classes {'
624+ print ' rankdir=BT;'
625+ for cls in Module.all_classes:
626+ print ' %s [label = "%s|%s"];' % (cls._number, cls._module, cls._name)
627+
628+ for cls in Module.all_classes:
629+ for p in cls._parents:
630+ if cls._name != p._name:
631+ # cls is a copy of p
632+ print ' %s -> %s [color="#1ED5FA"];' % (cls._number, p._number)
633+ else:
634+ print ' %s -> %s;' % (cls._number, p._number)
635+
636+ for m in map(get_module, Module.modules):
637+ print ' subgraph %s {' % m.name
638+ for cls in m.classes:
639+ print ' %s;' % cls._number
640+ print ' }'
641+ print '}'
642+
643+# Temporary list for each OpenERP module all its model classes.
644+# The list is available on the `openerp_classes` attribute
645+# (on `sys.modules[module_name]`) but it doesn't exist yet
646+# when the classes are registered.
647+module_to_models = {}
648
649 class AddonsImportHook(object):
650 """
651- Import hook to load OpenERP addons from multiple paths.
652+ Import hook to load OpenERP addons from multiple paths. The dependencies
653+ (declared in the `__openerp__.py` file) are imported too. A few attributes
654+ are set on the module.
655
656 OpenERP implements its own import-hook to load its addons. OpenERP
657- addons are Python modules. Originally, they were each living in their
658- own top-level namespace, e.g. the sale module, or the hr module. For
659- backward compatibility, `import <module>` is still supported. Now they
660- are living in `openerp.addons`. The good way to import such modules is
661- thus `import openerp.addons.module`.
662+ addons are Python modules. They must be imported from the `openerp.addons`
663+ namespace. E.g. to import the `sale` module, use::
664+
665+ > import openerp.addons.sale
666 """
667
668 def find_module(self, module_name, package_path):
669@@ -71,27 +223,38 @@
670 return self # We act as a loader too.
671
672 def load_module(self, module_name):
673-
674 module_parts = module_name.split('.')
675 if len(module_parts) == 3 and module_name.startswith('openerp.addons.'):
676 module_part = module_parts[2]
677 if module_name in sys.modules:
678 return sys.modules[module_name]
679
680+ # Import the dependencies.
681+ info = load_information_from_description_file(module_part)
682+ for d in info['depends']:
683+ __import__('openerp.addons.' + d)
684+
685+ # Import the module.
686 # Note: we don't support circular import.
687 f, path, descr = imp.find_module(module_part, ad_paths)
688 mod = imp.load_module('openerp.addons.' + module_part, f, path, descr)
689+
690+ # We also use the import-hook to do some bookkeeping directly on
691+ # the module itself.
692+ mod.openerp_name = module_part
693+ mod.openerp_descriptor = info
694+ mod.openerp_classes = module_to_models.get(module_part, [])
695+ mod.openerp_rank = None
696 sys.modules['openerp.addons.' + module_part] = mod
697+ Module.modules.append(module_part)
698+ Module.dirty = True
699+
700 return mod
701
702 def initialize_sys_path():
703 """
704 Setup an import-hook to be able to import OpenERP addons from the different
705 addons paths.
706-
707- This ensures something like ``import crm`` (or even
708- ``import openerp.addons.crm``) works even if the addons are not in the
709- PYTHONPATH.
710 """
711 global ad_paths
712 if ad_paths:
713@@ -154,68 +317,6 @@
714
715 return tree
716
717-def zip_directory(directory, b64enc=True, src=True):
718- """Compress a directory
719-
720- @param directory: The directory to compress
721- @param base64enc: if True the function will encode the zip file with base64
722- @param src: Integrate the source files
723-
724- @return: a string containing the zip file
725- """
726-
727- RE_exclude = re.compile('(?:^\..+\.swp$)|(?:\.py[oc]$)|(?:\.bak$)|(?:\.~.~$)', re.I)
728-
729- def _zippy(archive, path, src=True):
730- path = os.path.abspath(path)
731- base = os.path.basename(path)
732- for f in osutil.listdir(path, True):
733- bf = os.path.basename(f)
734- if not RE_exclude.search(bf) and (src or bf == '__openerp__.py' or not bf.endswith('.py')):
735- archive.write(os.path.join(path, f), os.path.join(base, f))
736-
737- archname = StringIO()
738- archive = PyZipFile(archname, "w", ZIP_DEFLATED)
739-
740- # for Python 2.5, ZipFile.write() still expects 8-bit strings (2.6 converts to utf-8)
741- directory = tools.ustr(directory).encode('utf-8')
742-
743- archive.writepy(directory)
744- _zippy(archive, directory, src=src)
745- archive.close()
746- archive_data = archname.getvalue()
747- archname.close()
748-
749- if b64enc:
750- return base64.encodestring(archive_data)
751-
752- return archive_data
753-
754-def get_module_as_zip(modulename, b64enc=True, src=True):
755- """Generate a module as zip file with the source or not and can do a base64 encoding
756-
757- @param modulename: The module name
758- @param b64enc: if True the function will encode the zip file with base64
759- @param src: Integrate the source files
760-
761- @return: a stream to store in a file-like object
762- """
763-
764- ap = get_module_path(str(modulename))
765- if not ap:
766- raise Exception('Unable to find path for module %s' % modulename)
767-
768- ap = ap.encode('utf8')
769- if os.path.isfile(ap + '.zip'):
770- val = file(ap + '.zip', 'rb').read()
771- if b64enc:
772- val = base64.encodestring(val)
773- else:
774- val = zip_directory(ap, b64enc, src)
775-
776- return val
777-
778-
779 def get_module_resource(module, *args):
780 """Return the full path of a resource of the given module.
781
782@@ -295,76 +396,10 @@
783 info['version'] = adapt_version(info['version'])
784 return info
785
786- #TODO: refactor the logger in this file to follow the logging guidelines
787- # for 6.0
788 _logger.debug('module %s: no __openerp__.py file found.', module)
789 return {}
790
791
792-def init_module_models(cr, module_name, obj_list):
793- """ Initialize a list of models.
794-
795- Call _auto_init and init on each model to create or update the
796- database tables supporting the models.
797-
798- TODO better explanation of _auto_init and init.
799-
800- """
801- _logger.info('module %s: creating or updating database tables', module_name)
802- todo = []
803- for obj in obj_list:
804- result = obj._auto_init(cr, {'module': module_name})
805- if result:
806- todo += result
807- if hasattr(obj, 'init'):
808- obj.init(cr)
809- cr.commit()
810- for obj in obj_list:
811- obj._auto_end(cr, {'module': module_name})
812- cr.commit()
813- todo.sort()
814- for t in todo:
815- t[1](cr, *t[2])
816- cr.commit()
817-
818-def load_openerp_module(module_name):
819- """ Load an OpenERP module, if not already loaded.
820-
821- This loads the module and register all of its models, thanks to either
822- the MetaModel metaclass, or the explicit instantiation of the model.
823- This is also used to load server-wide module (i.e. it is also used
824- when there is no model to register).
825- """
826- global loaded
827- if module_name in loaded:
828- return
829-
830- initialize_sys_path()
831- try:
832- mod_path = get_module_path(module_name)
833- zip_mod_path = '' if not mod_path else mod_path + '.zip'
834- if not os.path.isfile(zip_mod_path):
835- __import__('openerp.addons.' + module_name)
836- else:
837- zimp = zipimport.zipimporter(zip_mod_path)
838- zimp.load_module(module_name)
839-
840- # Call the module's post-load hook. This can done before any model or
841- # data has been initialized. This is ok as the post-load hook is for
842- # server-wide (instead of registry-specific) functionalities.
843- info = load_information_from_description_file(module_name)
844- if info['post_load']:
845- getattr(sys.modules['openerp.addons.' + module_name], info['post_load'])()
846-
847- except Exception, e:
848- mt = isinstance(e, zipimport.ZipImportError) and 'zip ' or ''
849- msg = "Couldn't load %smodule %s" % (mt, module_name)
850- _logger.critical(msg)
851- _logger.critical(e)
852- raise
853- else:
854- loaded.append(module_name)
855-
856 def get_modules():
857 """Returns the list of module names
858 """
859@@ -388,17 +423,6 @@
860 return list(set(plist))
861
862
863-def get_modules_with_version():
864- modules = get_modules()
865- res = dict.fromkeys(modules, adapt_version('1.0'))
866- for module in modules:
867- try:
868- info = load_information_from_description_file(module)
869- res[module] = info['version']
870- except Exception:
871- continue
872- return res
873-
874 def adapt_version(version):
875 serie = release.major_version
876 if version == serie or not version.startswith(serie + '.'):
877
878=== modified file 'openerp/modules/registry.py'
879--- openerp/modules/registry.py 2013-06-19 10:53:35 +0000
880+++ openerp/modules/registry.py 2013-07-10 14:52:28 +0000
881@@ -24,13 +24,16 @@
882 """
883 from collections import Mapping
884 from contextlib import contextmanager
885+import itertools
886 import logging
887+import sys
888 import threading
889
890 import openerp.sql_db
891 import openerp.osv.orm
892 import openerp.tools
893 import openerp.modules.db
894+import openerp.modules.loading
895 import openerp.tools.config
896 from openerp.tools import assertion_report
897
898@@ -53,6 +56,8 @@
899 self._init_parent = {}
900 self._assertion_report = assertion_report.assertion_report()
901 self.fields_by_model = None
902+ # Set of (set of classes) already consolidated and instanciated.
903+ self.identities = set()
904
905 # modules fully loaded (maintained during init phase by `loading` module)
906 self._init_modules = set()
907@@ -115,24 +120,32 @@
908 """ Add or replace a model in the registry."""
909 self.models[model_name] = model
910
911- def load(self, cr, module):
912- """ Load a given module in the registry.
913-
914- At the Python level, the modules are already loaded, but not yet on a
915- per-registry level. This method populates a registry with the given
916- modules, i.e. it instanciates all the classes of a the given module
917- and registers them in the registry.
918-
919- """
920+ def add_module(self, cr, module):
921+ module_names = [module.openerp_name] + list(self._init_modules)
922 models_to_load = [] # need to preserve loading order
923- # Instantiate registered classes (via the MetaModel automatic discovery
924- # or via explicit constructor call), and add them to the pool.
925- for cls in openerp.osv.orm.MetaModel.module_to_models.get(module.name, []):
926- # models register themselves in self.models
927- model = cls.create_instance(self, cr)
928- if model._name not in models_to_load:
929+ modules = [sys.modules['openerp.addons.' + m] for m in module_names]
930+ # Re-consolidate all previously consolidated (and instanciated)
931+ # models, instead of only the one from the current module.
932+ # We end up re-creating more models than strictly necessary;
933+ # normaly, only the parents (and their children) must be re-done.
934+ for model in itertools.chain(*tuple(module.openerp_classes for module in modules)):
935+ if model._name in models_to_load:
936 # avoid double-loading models whose declaration is split
937- models_to_load.append(model._name)
938+ continue
939+ classes = list(openerp.modules.module.get_model_classes(model._name))
940+ classes = filter(lambda ext: ext._module in module_names, classes)
941+ # Derive an 'identity' for this set of classes so we can avoid
942+ # reloading the model if it has not been changed by this module.
943+ # There would be nothing wrong doing it, it would just take
944+ # additional time.
945+ i = tuple([id(c) for c in classes])
946+ if i in self.identities:
947+ continue
948+ self.identities.add(i)
949+ models_to_load.append(model._name)
950+ cls = openerp.osv.orm.consolidate(model._name, classes)
951+ obj = object.__new__(cls)
952+ obj.__init__(self, cr)
953 return [self.models[m] for m in models_to_load]
954
955 def clear_caches(self):
956@@ -229,7 +242,7 @@
957 cls.registries[db_name] = registry
958 try:
959 # This should be a method on Registry
960- openerp.modules.load_modules(registry.db, force_demo, status, update_module)
961+ openerp.modules.loading.load_modules(registry.db, force_demo, status, update_module)
962 except Exception:
963 del cls.registries[db_name]
964 raise
965
966=== modified file 'openerp/osv/orm.py'
967--- openerp/osv/orm.py 2013-07-02 14:47:45 +0000
968+++ openerp/osv/orm.py 2013-07-10 14:52:28 +0000
969@@ -599,6 +599,53 @@
970 return pg_type
971
972
973+def consolidate(model_name, classes):
974+ """ Create a consolidated model defined by the provided classes.
975+ """
976+ attributes = ['_columns', '_defaults', '_inherits', '_constraints',
977+ '_sql_constraints']
978+
979+ assert classes, "Model `%s` consolidation cannot be done because there is no class to define it." % model_name
980+ nattr = {'_register': False}
981+ for s in attributes:
982+ new = copy.copy(getattr(classes[-1], s))
983+ for parent in list(reversed(classes))[1:]:
984+ if s == '_columns':
985+ # Duplicate float fields because they have a .digits
986+ # cache (which must be per-registry, not server-wide).
987+ for c in new.keys():
988+ if new[c]._type == 'float':
989+ new[c] = copy.copy(new[c])
990+ if hasattr(new, 'update'):
991+ new.update(parent.__dict__.get(s, {}))
992+ elif s=='_constraints':
993+ for c in parent.__dict__.get(s, []):
994+ exist = False
995+ for c2 in range(len(new)):
996+ #For _constraints, we should check field and methods as well
997+ if new[c2][2]==c[2] and (new[c2][0] == c[0] \
998+ or getattr(new[c2][0],'__name__', True) == \
999+ getattr(c[0],'__name__', False)):
1000+ # If new class defines a constraint with
1001+ # same function name, we let it override
1002+ # the old one.
1003+
1004+ new[c2] = c
1005+ exist = True
1006+ break
1007+ if not exist:
1008+ new.append(c)
1009+ else:
1010+ new.extend(parent.__dict__.get(s, []))
1011+ nattr[s] = new
1012+
1013+ # Keep links to non-inherited constraints, e.g. useful when exporting translations
1014+ nattr['_local_constraints'] = classes[0].__dict__.get('_constraints', [])
1015+ nattr['_local_sql_constraints'] = classes[0].__dict__.get('_sql_constraints', [])
1016+ cls = type(model_name, tuple(classes), nattr)
1017+ return cls
1018+
1019+
1020 class MetaModel(type):
1021 """ Metaclass for the Model.
1022
1023@@ -609,14 +656,23 @@
1024
1025 """
1026
1027- module_to_models = {}
1028-
1029 def __init__(self, name, bases, attrs):
1030 if not self._register:
1031 self._register = True
1032 super(MetaModel, self).__init__(name, bases, attrs)
1033 return
1034
1035+ # Derive the _name of the class.
1036+ if not self._name and isinstance(self._inherit, list) and len(self._inherit) == 1:
1037+ self._name = self._inherit[0]
1038+ elif not self._name and isinstance(self._inherit, list):
1039+ raise Exception, "_name must be provide when there are multiple _inherit parents."
1040+ elif not self._name:
1041+ self._name = self._inherit
1042+ assert self._name
1043+ # Sets the instance _inherit attribute (with the classs attribute).
1044+ self._inherit = self._inherit
1045+
1046 # The (OpenERP) module name can be in the `openerp.addons` namespace
1047 # or not. For instance module `sale` can be imported as
1048 # `openerp.addons.sale` (the good way) or `sale` (for backward
1049@@ -627,12 +683,15 @@
1050 module_name = self.__module__.split('.')[2]
1051 else:
1052 module_name = self.__module__.split('.')[0]
1053+ _logger.warning('OpenERP module `%s` is not in the `openerp.addons` namespace.', module_name)
1054+
1055 if not hasattr(self, '_module'):
1056 self._module = module_name
1057
1058 # Remember which models to instanciate for this module.
1059 if not self._custom:
1060- self.module_to_models.setdefault(self._module, []).append(self)
1061+ module_model_list = openerp.modules.module.module_to_models.setdefault(self._module, [])
1062+ module_model_list.append(self)
1063
1064
1065 # Definition of log access columns, automatically added to models if
1066@@ -688,6 +747,8 @@
1067 # Transience
1068 _transient = False # True in a TransientModel
1069
1070+ _inherit = 'base.model'
1071+
1072 # structure:
1073 # { 'parent_model': 'm2o_field', ... }
1074 _inherits = {}
1075@@ -835,119 +896,10 @@
1076 break
1077 cr.commit()
1078
1079- #
1080- # Goal: try to apply inheritance at the instanciation level and
1081- # put objects in the pool var
1082- #
1083- @classmethod
1084- def create_instance(cls, pool, cr):
1085- """ Instanciate a given model.
1086-
1087- This class method instanciates the class of some model (i.e. a class
1088- deriving from osv or osv_memory). The class might be the class passed
1089- in argument or, if it inherits from another class, a class constructed
1090- by combining the two classes.
1091-
1092- The ``attributes`` argument specifies which parent class attributes
1093- have to be combined.
1094-
1095- TODO: the creation of the combined class is repeated at each call of
1096- this method. This is probably unnecessary.
1097-
1098- """
1099- attributes = ['_columns', '_defaults', '_inherits', '_constraints',
1100- '_sql_constraints']
1101-
1102- parent_names = getattr(cls, '_inherit', None)
1103- if parent_names:
1104- if isinstance(parent_names, (str, unicode)):
1105- name = cls._name or parent_names
1106- parent_names = [parent_names]
1107- else:
1108- name = cls._name
1109- if not name:
1110- raise TypeError('_name is mandatory in case of multiple inheritance')
1111-
1112- for parent_name in ((type(parent_names)==list) and parent_names or [parent_names]):
1113- if parent_name not in pool:
1114- raise TypeError('The model "%s" specifies an unexisting parent class "%s"\n'
1115- 'You may need to add a dependency on the parent class\' module.' % (name, parent_name))
1116- parent_model = pool[parent_name]
1117- if not getattr(cls, '_original_module', None) and name == parent_model._name:
1118- cls._original_module = parent_model._original_module
1119- parent_class = parent_model.__class__
1120- nattr = {}
1121- for s in attributes:
1122- new = copy.copy(getattr(parent_model, s, {}))
1123- if s == '_columns':
1124- # Don't _inherit custom fields.
1125- for c in new.keys():
1126- if new[c].manual:
1127- del new[c]
1128- # Duplicate float fields because they have a .digits
1129- # cache (which must be per-registry, not server-wide).
1130- for c in new.keys():
1131- if new[c]._type == 'float':
1132- new[c] = copy.copy(new[c])
1133- if hasattr(new, 'update'):
1134- new.update(cls.__dict__.get(s, {}))
1135- elif s=='_constraints':
1136- for c in cls.__dict__.get(s, []):
1137- exist = False
1138- for c2 in range(len(new)):
1139- #For _constraints, we should check field and methods as well
1140- if new[c2][2]==c[2] and (new[c2][0] == c[0] \
1141- or getattr(new[c2][0],'__name__', True) == \
1142- getattr(c[0],'__name__', False)):
1143- # If new class defines a constraint with
1144- # same function name, we let it override
1145- # the old one.
1146-
1147- new[c2] = c
1148- exist = True
1149- break
1150- if not exist:
1151- new.append(c)
1152- else:
1153- new.extend(cls.__dict__.get(s, []))
1154- nattr[s] = new
1155-
1156- # Keep links to non-inherited constraints, e.g. useful when exporting translations
1157- nattr['_local_constraints'] = cls.__dict__.get('_constraints', [])
1158- nattr['_local_sql_constraints'] = cls.__dict__.get('_sql_constraints', [])
1159-
1160- cls = type(name, (cls, parent_class), dict(nattr, _register=False))
1161- else:
1162- cls._local_constraints = getattr(cls, '_constraints', [])
1163- cls._local_sql_constraints = getattr(cls, '_sql_constraints', [])
1164-
1165- if not getattr(cls, '_original_module', None):
1166- cls._original_module = cls._module
1167- obj = object.__new__(cls)
1168- obj.__init__(pool, cr)
1169- return obj
1170-
1171 def __new__(cls):
1172- """Register this model.
1173-
1174- This doesn't create an instance but simply register the model
1175- as being part of the module where it is defined.
1176-
1177- """
1178-
1179-
1180- # Set the module name (e.g. base, sale, accounting, ...) on the class.
1181- module = cls.__module__.split('.')[0]
1182- if not hasattr(cls, '_module'):
1183- cls._module = module
1184-
1185- # Record this class in the list of models to instantiate for this module,
1186- # managed by the metaclass.
1187- module_model_list = MetaModel.module_to_models.setdefault(cls._module, [])
1188- if cls not in module_model_list:
1189- if not cls._custom:
1190- module_model_list.append(cls)
1191-
1192+ assert openerp.conf.deprecation.allow_explicit_model_call
1193+ _logger.warning("Instanciating a model (here `%s`) after defining it"
1194+ " is deprecated.", cls._name or cls._inherit)
1195 # Since we don't return an instance here, the __init__
1196 # method won't be called.
1197 return None
1198@@ -1064,8 +1016,6 @@
1199 self._inherits_reload()
1200 if not self._sequence:
1201 self._sequence = self._table + '_id_seq'
1202- for k in self._defaults:
1203- assert (k in self._columns) or (k in self._inherit_fields), 'Default function defined in %s but field %s does not exist !' % (self._name, k,)
1204 for f in self._columns:
1205 self._columns[f].restart()
1206
1207@@ -2776,9 +2726,10 @@
1208 pos = browse_rec(root, pos)
1209 return True
1210
1211- def _update_store(self, cr, f, k):
1212- _logger.info("storing computed values of fields.function '%s'", k)
1213- ss = self._columns[k]._symbol_set
1214+ def _update_stored_functional_field(self, cr, k):
1215+ _logger.info("Computing and storing functional field `%s`.", k)
1216+ f = self._columns[k]
1217+ ss = f._symbol_set
1218 update_query = 'UPDATE "%s" SET "%s"=%s WHERE id=%%s' % (self._table, k, ss[0])
1219 cr.execute('select id from '+self._table)
1220 ids_lst = map(lambda x: x[0], cr.fetchall())
1221@@ -2970,7 +2921,7 @@
1222 if context is None:
1223 context = {}
1224 store_compute = False
1225- todo_end = []
1226+ stored_fields = []
1227 update_custom_fields = context.get('update_custom_fields', False)
1228 self._field_create(cr, context=context)
1229 create = not self._table_exist(cr)
1230@@ -3172,10 +3123,11 @@
1231
1232 # remember the functions to call for the stored fields
1233 if isinstance(f, fields.function):
1234- order = 10
1235 if f.store is not True: # i.e. if f.store is a dict
1236 order = f.store[f.store.keys()[0]][2]
1237- todo_end.append((order, self._update_store, (f, k)))
1238+ else:
1239+ order = 10
1240+ stored_fields.append((order, self, k))
1241
1242 # and add constraints if needed
1243 if isinstance(f, fields.many2one):
1244@@ -3202,23 +3154,19 @@
1245 _logger.warning(msg, k, self._table, self._table, k)
1246 cr.commit()
1247
1248- else:
1249- cr.execute("SELECT relname FROM pg_class WHERE relkind IN ('r','v') AND relname=%s", (self._table,))
1250- create = not bool(cr.fetchone())
1251-
1252 cr.commit() # start a new transaction
1253
1254 if self._auto:
1255 self._add_sql_constraints(cr)
1256
1257- if create:
1258- self._execute_sql(cr)
1259-
1260 if store_compute:
1261 self._parent_store_compute(cr)
1262 cr.commit()
1263
1264- return todo_end
1265+ return stored_fields
1266+
1267+ def init(self, cr):
1268+ pass
1269
1270 def _auto_end(self, cr, context=None):
1271 """ Create the foreign keys recorded by _auto_init. """
1272@@ -3228,6 +3176,9 @@
1273 cr.commit()
1274 del self._foreign_keys
1275
1276+ for k in self._defaults:
1277+ assert (k in self._columns) or (k in self._inherit_fields), 'Default function defined in %s but field %s does not exist !' % (self._name, k,)
1278+
1279
1280 def _table_exist(self, cr):
1281 cr.execute("SELECT relname FROM pg_class WHERE relkind IN ('r','v') AND relname=%s", (self._table,))
1282@@ -3393,15 +3344,6 @@
1283 cr.rollback()
1284
1285
1286- def _execute_sql(self, cr):
1287- """ Execute the SQL code from the _sql attribute (if any)."""
1288- if hasattr(self, "_sql"):
1289- for line in self._sql.split(';'):
1290- line2 = line.replace('\n', '').strip()
1291- if line2:
1292- cr.execute(line2)
1293- cr.commit()
1294-
1295 #
1296 # Update objects that uses this one to update their _inherits fields
1297 #
1298
1299=== modified file 'openerp/service/__init__.py'
1300--- openerp/service/__init__.py 2013-06-12 15:28:26 +0000
1301+++ openerp/service/__init__.py 2013-07-10 14:52:28 +0000
1302@@ -60,7 +60,12 @@
1303 def load_server_wide_modules():
1304 for m in openerp.conf.server_wide_modules:
1305 try:
1306- openerp.modules.module.load_openerp_module(m)
1307+ __import__('openerp.addons.' + m)
1308+ mod = sys.modules['openerp.addons.' + m]
1309+ post_load = mod.openerp_descriptor['post_load']
1310+ if post_load:
1311+ getattr(mod, post_load)()
1312+
1313 except Exception:
1314 msg = ''
1315 if m == 'web':
1316
1317=== added directory 'openerp/tests/addons/test_base_model_a'
1318=== added file 'openerp/tests/addons/test_base_model_a/__init__.py'
1319--- openerp/tests/addons/test_base_model_a/__init__.py 1970-01-01 00:00:00 +0000
1320+++ openerp/tests/addons/test_base_model_a/__init__.py 2013-07-10 14:52:28 +0000
1321@@ -0,0 +1,3 @@
1322+# -*- coding: utf-8 -*-
1323+import models
1324+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
1325
1326=== added file 'openerp/tests/addons/test_base_model_a/__openerp__.py'
1327--- openerp/tests/addons/test_base_model_a/__openerp__.py 1970-01-01 00:00:00 +0000
1328+++ openerp/tests/addons/test_base_model_a/__openerp__.py 2013-07-10 14:52:28 +0000
1329@@ -0,0 +1,15 @@
1330+# -*- coding: utf-8 -*-
1331+{
1332+ 'name': 'test-base-model-a',
1333+ 'version': '0.1',
1334+ 'category': 'Tests',
1335+ 'description': """A module to test the BaseModel inheritance.""",
1336+ 'author': 'OpenERP SA',
1337+ 'maintainer': 'OpenERP SA',
1338+ 'website': 'http://www.openerp.com',
1339+ 'depends': ['base'],
1340+ 'data': [],
1341+ 'installable': True,
1342+ 'auto_install': False,
1343+}
1344+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
1345
1346=== added file 'openerp/tests/addons/test_base_model_a/models.py'
1347--- openerp/tests/addons/test_base_model_a/models.py 1970-01-01 00:00:00 +0000
1348+++ openerp/tests/addons/test_base_model_a/models.py 2013-07-10 14:52:28 +0000
1349@@ -0,0 +1,26 @@
1350+# -*- coding: utf-8 -*-
1351+import openerp
1352+
1353+class a(openerp.osv.orm.Model):
1354+ """ This model is just a copy of BaseModel.
1355+ In the context of the base.model tests, we will check if the _columns of
1356+ this model is indeed the consolidated _colmuns of base.model.
1357+
1358+ This test module will first add a column_n to base.model and thus we will
1359+ check in the unit tests if test.base.model.a contains it.
1360+
1361+ In the second test_base_model_b addons, we will add a column_o and again
1362+ check if test.base.model.a contains it in addition to column_n.
1363+ """
1364+ _name = 'test.base.model.a'
1365+
1366+class n(openerp.osv.orm.AbstractModel):
1367+ """ This model augments BaseModel.
1368+ """
1369+ _inherit = 'base.model'
1370+
1371+ _columns = {
1372+ 'column_n': openerp.osv.fields.char(),
1373+ }
1374+
1375+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
1376
1377=== added directory 'openerp/tests/addons/test_base_model_a/tests'
1378=== added file 'openerp/tests/addons/test_base_model_a/tests/__init__.py'
1379--- openerp/tests/addons/test_base_model_a/tests/__init__.py 1970-01-01 00:00:00 +0000
1380+++ openerp/tests/addons/test_base_model_a/tests/__init__.py 2013-07-10 14:52:28 +0000
1381@@ -0,0 +1,13 @@
1382+# -*- coding: utf-8 -*-
1383+
1384+from . import test_base_model
1385+
1386+fast_suite = [
1387+]
1388+
1389+checks = [
1390+ # This test assumes to be installed alone.
1391+ # test_base_model,
1392+]
1393+
1394+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
1395
1396=== added file 'openerp/tests/addons/test_base_model_a/tests/test_base_model.py'
1397--- openerp/tests/addons/test_base_model_a/tests/test_base_model.py 1970-01-01 00:00:00 +0000
1398+++ openerp/tests/addons/test_base_model_a/tests/test_base_model.py 2013-07-10 14:52:28 +0000
1399@@ -0,0 +1,40 @@
1400+# -*- coding: utf-8 -*-
1401+import openerp.modules.registry
1402+import openerp
1403+
1404+from openerp.tests import common
1405+from openerp.tools.misc import mute_logger
1406+
1407+class test_base_model_a(common.TransactionCase):
1408+
1409+ def __init__(self, *args, **kwargs):
1410+ super(test_base_model_a, self).__init__(*args, **kwargs)
1411+ self.model = None
1412+
1413+ def setUp(self):
1414+ super(test_base_model_a, self).setUp()
1415+ self.model_a = self.registry('test.base.model.a')
1416+
1417+ def test_model_name(self):
1418+ self.assertEqual(
1419+ self.model_a._name,
1420+ 'test.base.model.a')
1421+
1422+ def test_model_columns(self):
1423+ self.assertEqual(
1424+ self.model_a._columns.keys(),
1425+ ['column_n'])
1426+
1427+ def test_model_inherit(self):
1428+ self.assertEqual(
1429+ self.model_a._inherit,
1430+ 'base.model')
1431+
1432+ def test_model_table(self):
1433+ # Assert no table is backing base.model.
1434+ self.cr.execute("SELECT relname FROM pg_class WHERE relkind = 'r' AND relname=%s", ('base_model',))
1435+ self.assertTrue(not self.cr.dictfetchall())
1436+
1437+ # Make sure the previous assertion works.
1438+ self.cr.execute("SELECT relname FROM pg_class WHERE relkind = 'r' AND relname=%s", ('res_partner',))
1439+ self.assertTrue(self.cr.dictfetchall())
1440
1441=== added directory 'openerp/tests/addons/test_base_model_b'
1442=== added file 'openerp/tests/addons/test_base_model_b/__init__.py'
1443--- openerp/tests/addons/test_base_model_b/__init__.py 1970-01-01 00:00:00 +0000
1444+++ openerp/tests/addons/test_base_model_b/__init__.py 2013-07-10 14:52:28 +0000
1445@@ -0,0 +1,3 @@
1446+# -*- coding: utf-8 -*-
1447+import models
1448+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
1449
1450=== added file 'openerp/tests/addons/test_base_model_b/__openerp__.py'
1451--- openerp/tests/addons/test_base_model_b/__openerp__.py 1970-01-01 00:00:00 +0000
1452+++ openerp/tests/addons/test_base_model_b/__openerp__.py 2013-07-10 14:52:28 +0000
1453@@ -0,0 +1,15 @@
1454+# -*- coding: utf-8 -*-
1455+{
1456+ 'name': 'test-base-model-b',
1457+ 'version': '0.1',
1458+ 'category': 'Tests',
1459+ 'description': """A module to test the BaseModel inheritance.""",
1460+ 'author': 'OpenERP SA',
1461+ 'maintainer': 'OpenERP SA',
1462+ 'website': 'http://www.openerp.com',
1463+ 'depends': ['base', 'test_base_model_a'],
1464+ 'data': [],
1465+ 'installable': True,
1466+ 'auto_install': False,
1467+}
1468+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
1469
1470=== added file 'openerp/tests/addons/test_base_model_b/models.py'
1471--- openerp/tests/addons/test_base_model_b/models.py 1970-01-01 00:00:00 +0000
1472+++ openerp/tests/addons/test_base_model_b/models.py 2013-07-10 14:52:28 +0000
1473@@ -0,0 +1,18 @@
1474+# -*- coding: utf-8 -*-
1475+import openerp
1476+
1477+class b(openerp.osv.orm.Model):
1478+ """ This model is just a copy of BaseModel.
1479+ """
1480+ _name = 'test.base.model.b'
1481+
1482+class o(openerp.osv.orm.AbstractModel):
1483+ """ This model augments BaseModel.
1484+ """
1485+ _inherit = 'base.model'
1486+
1487+ _columns = {
1488+ 'column_o': openerp.osv.fields.char(),
1489+ }
1490+
1491+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
1492
1493=== added directory 'openerp/tests/addons/test_base_model_b/tests'
1494=== added file 'openerp/tests/addons/test_base_model_b/tests/__init__.py'
1495--- openerp/tests/addons/test_base_model_b/tests/__init__.py 1970-01-01 00:00:00 +0000
1496+++ openerp/tests/addons/test_base_model_b/tests/__init__.py 2013-07-10 14:52:28 +0000
1497@@ -0,0 +1,13 @@
1498+# -*- coding: utf-8 -*-
1499+
1500+from . import test_base_model
1501+
1502+fast_suite = [
1503+]
1504+
1505+checks = [
1506+ # This test assumes to be installed alone.
1507+ # test_base_model,
1508+]
1509+
1510+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
1511
1512=== added file 'openerp/tests/addons/test_base_model_b/tests/test_base_model.py'
1513--- openerp/tests/addons/test_base_model_b/tests/test_base_model.py 1970-01-01 00:00:00 +0000
1514+++ openerp/tests/addons/test_base_model_b/tests/test_base_model.py 2013-07-10 14:52:28 +0000
1515@@ -0,0 +1,50 @@
1516+# -*- coding: utf-8 -*-
1517+import openerp.modules.registry
1518+import openerp
1519+
1520+from openerp.tests import common
1521+from openerp.tools.misc import mute_logger
1522+
1523+class test_base_model_b(common.TransactionCase):
1524+
1525+ def __init__(self, *args, **kwargs):
1526+ super(test_base_model_b, self).__init__(*args, **kwargs)
1527+ self.model = None
1528+
1529+ def setUp(self):
1530+ super(test_base_model_b, self).setUp()
1531+ self.model_a = self.registry('test.base.model.a')
1532+ self.model_b = self.registry('test.base.model.b')
1533+
1534+ def test_model_name(self):
1535+ self.assertEqual(
1536+ self.model_a._name,
1537+ 'test.base.model.a')
1538+ self.assertEqual(
1539+ self.model_b._name,
1540+ 'test.base.model.b')
1541+
1542+ def test_model_columns(self):
1543+ self.assertEqual(
1544+ sorted(self.model_a._columns.keys()),
1545+ ['column_n', 'column_o'])
1546+ self.assertEqual(
1547+ sorted(self.model_b._columns.keys()),
1548+ ['column_n', 'column_o'])
1549+
1550+ def test_model_inherit(self):
1551+ self.assertEqual(
1552+ self.model_a._inherit,
1553+ 'base.model')
1554+ self.assertEqual(
1555+ self.model_b._inherit,
1556+ 'base.model')
1557+
1558+ def test_model_table(self):
1559+ # Assert no table is backing base.model.
1560+ self.cr.execute("SELECT relname FROM pg_class WHERE relkind = 'r' AND relname=%s", ('base_model',))
1561+ self.assertTrue(not self.cr.dictfetchall())
1562+
1563+ # Make sure the previous assertion works.
1564+ self.cr.execute("SELECT relname FROM pg_class WHERE relkind = 'r' AND relname=%s", ('res_partner',))
1565+ self.assertTrue(self.cr.dictfetchall())