[MERGE] merged trunk.
[odoo/odoo.git] / openerp / modules / graph.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
6 #    Copyright (C) 2010-2011 OpenERP s.a. (<http://openerp.com>).
7 #
8 #    This program is free software: you can redistribute it and/or modify
9 #    it under the terms of the GNU Affero General Public License as
10 #    published by the Free Software Foundation, either version 3 of the
11 #    License, or (at your option) any later version.
12 #
13 #    This program is distributed in the hope that it will be useful,
14 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
15 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 #    GNU Affero General Public License for more details.
17 #
18 #    You should have received a copy of the GNU Affero General Public License
19 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
20 #
21 ##############################################################################
22
23 """ Modules dependency graph. """
24
25 import os, sys, imp
26 from os.path import join as opj
27 import itertools
28 import zipimport
29
30 import openerp
31
32 import openerp.osv as osv
33 import openerp.tools as tools
34 import openerp.tools.osutil as osutil
35 from openerp.tools.safe_eval import safe_eval as eval
36 import openerp.pooler as pooler
37 from openerp.tools.translate import _
38
39 import openerp.netsvc as netsvc
40
41 import zipfile
42 import openerp.release as release
43
44 import re
45 import base64
46 from zipfile import PyZipFile, ZIP_DEFLATED
47 from cStringIO import StringIO
48
49 import logging
50
51 logger = netsvc.Logger()
52
53
54 class Graph(dict):
55     """ Modules dependency graph.
56
57     The graph is a mapping from module name to Nodes.
58
59     """
60
61     def add_node(self, name, info):
62         max_depth, father = 0, None
63         for n in [Node(x, self, None) for x in info['depends']]:
64             if n.depth >= max_depth:
65                 father = n
66                 max_depth = n.depth
67         if father:
68             return father.add_child(name, info)
69         else:
70             return Node(name, self, info)
71
72     def update_from_db(self, cr):
73         if not len(self):
74             return
75         # update the graph with values from the database (if exist)
76         ## First, we set the default values for each package in graph
77         additional_data = dict.fromkeys(self.keys(), {'id': 0, 'state': 'uninstalled', 'dbdemo': False, 'installed_version': None})
78         ## Then we get the values from the database
79         cr.execute('SELECT name, id, state, demo AS dbdemo, latest_version AS installed_version'
80                    '  FROM ir_module_module'
81                    ' WHERE name IN %s',(tuple(additional_data),)
82                    )
83
84         ## and we update the default values with values from the database
85         additional_data.update(dict([(x.pop('name'), x) for x in cr.dictfetchall()]))
86
87         for package in self.values():
88             for k, v in additional_data[package.name].items():
89                 setattr(package, k, v)
90
91     def add_module(self, cr, module, force_demo=False):
92         self.add_modules(cr, [module], force_demo)
93
94     def add_modules(self, cr, module_list, force_demo=False):
95         packages = []
96         len_graph = len(self)
97         for module in module_list:
98             if force_demo:
99                 cr.execute("""
100                     UPDATE ir_module_module
101                     SET demo='t'
102                     WHERE name = %s""",
103                     (module,))
104             # This will raise an exception if no/unreadable descriptor file.
105             # NOTE The call to load_information_from_description_file is already
106             # done by db.initialize, so it is possible to not do it again here.
107             info = openerp.modules.module.load_information_from_description_file(module)
108             if info and info['installable']:
109                 packages.append((module, info)) # TODO directly a dict, like in get_modules_with_version
110             else:
111                 logger.notifyChannel('init', netsvc.LOG_WARNING, 'module %s: not installable, skipped' % (module))
112
113         dependencies = dict([(p, info['depends']) for p, info in packages])
114         current, later = set([p for p, info in packages]), set()
115
116         while packages and current > later:
117             package, info = packages[0]
118             deps = info['depends']
119
120             # if all dependencies of 'package' are already in the graph, add 'package' in the graph
121             if reduce(lambda x, y: x and y in self, deps, True):
122                 if not package in current:
123                     packages.pop(0)
124                     continue
125                 later.clear()
126                 current.remove(package)
127                 node = self.add_node(package, info)
128                 node.data = info
129             else:
130                 later.add(package)
131                 packages.append((package, info))
132             packages.pop(0)
133
134         self.update_from_db(cr)
135
136         for package in later:
137             unmet_deps = filter(lambda p: p not in self, dependencies[package])
138             logger.notifyChannel('init', netsvc.LOG_ERROR, 'module %s: Unmet dependencies: %s' % (package, ', '.join(unmet_deps)))
139
140         result = len(self) - len_graph
141         if result != len(module_list):
142             logger.notifyChannel('init', netsvc.LOG_WARNING, 'Not all modules have loaded.')
143         return result
144
145
146     def __iter__(self):
147         level = 0
148         done = set(self.keys())
149         while done:
150             level_modules = sorted((name, module) for name, module in self.items() if module.depth==level)
151             for name, module in level_modules:
152                 done.remove(name)
153                 yield module
154             level += 1
155
156
157 class Singleton(object):
158     def __new__(cls, name, graph, info):
159         if name in graph:
160             inst = graph[name]
161         else:
162             inst = object.__new__(cls)
163             inst.name = name
164             inst.info = info
165             graph[name] = inst
166         return inst
167
168
169 class Node(Singleton):
170     """ One module in the modules dependency graph.
171
172     Node acts as a per-module singleton. A node is constructed via
173     Graph.add_module() or Graph.add_modules(). Some of its fields are from
174     ir_module_module (setted by Graph.update_from_db()).
175
176     """
177
178     def __init__(self, name, graph, info):
179         self.graph = graph
180         if not hasattr(self, 'children'):
181             self.children = []
182         if not hasattr(self, 'depth'):
183             self.depth = 0
184
185     def add_child(self, name, info):
186         node = Node(name, self.graph, info)
187         node.depth = self.depth + 1
188         if node not in self.children:
189             self.children.append(node)
190         self.children.sort(lambda x, y: cmp(x.name, y.name))
191         return node
192
193     def __setattr__(self, name, value):
194         super(Singleton, self).__setattr__(name, value)
195         if name == 'depth':
196             for child in self.children:
197                 setattr(child, name, value + 1)
198
199     def __iter__(self):
200         return itertools.chain(iter(self.children), *map(iter, self.children))
201
202     def __str__(self):
203         return self._pprint()
204
205     def _pprint(self, depth=0):
206         s = '%s\n' % self.name
207         for c in self.children:
208             s += '%s`-> %s' % ('   ' * depth, c._pprint(depth+1))
209         return s
210
211
212 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: